mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-13 12:14:16 +01:00
Less footguns in service dependency handling (#1560)
This commit is contained in:
parent
5777745ab3
commit
a0f4baf8fa
23 changed files with 659 additions and 302 deletions
|
|
@ -57,6 +57,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
|
|||
|
||||
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
|
||||
this.addonLifecycle.RegisterListener(this.finalizeEventListener);
|
||||
|
||||
this.onUpdateCursor.Enable();
|
||||
}
|
||||
|
||||
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
|
||||
|
|
@ -149,12 +151,6 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction()
|
||||
{
|
||||
this.onUpdateCursor.Enable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When an addon finalizes, check it for any registered events, and unregister them.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
this.onAddonUpdateHook = new CallHook<AddonUpdateDelegate>(this.address.AddonUpdate, this.OnAddonUpdate);
|
||||
this.onAddonRefreshHook = Hook<AddonOnRefreshDelegate>.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh);
|
||||
this.onAddonRequestedUpdateHook = new CallHook<AddonOnRequestedUpdateDelegate>(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
|
||||
|
||||
this.onAddonSetupHook.Enable();
|
||||
this.onAddonSetup2Hook.Enable();
|
||||
this.onAddonFinalizeHook.Enable();
|
||||
this.onAddonDrawHook.Enable();
|
||||
this.onAddonUpdateHook.Enable();
|
||||
this.onAddonRefreshHook.Enable();
|
||||
this.onAddonRequestedUpdateHook.Enable();
|
||||
}
|
||||
|
||||
private delegate void AddonSetupDelegate(AtkUnitBase* addon, uint valueCount, AtkValue* values);
|
||||
|
|
@ -181,18 +189,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction()
|
||||
{
|
||||
this.onAddonSetupHook.Enable();
|
||||
this.onAddonSetup2Hook.Enable();
|
||||
this.onAddonFinalizeHook.Enable();
|
||||
this.onAddonDrawHook.Enable();
|
||||
this.onAddonUpdateHook.Enable();
|
||||
this.onAddonRefreshHook.Enable();
|
||||
this.onAddonRequestedUpdateHook.Enable();
|
||||
}
|
||||
|
||||
private void RegisterReceiveEventHook(AtkUnitBase* addon)
|
||||
{
|
||||
// Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
|
|||
this.framework.Update += this.FrameworkOnOnUpdateEvent;
|
||||
|
||||
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
|
||||
|
||||
this.setupTerritoryTypeHook.Enable();
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
|
|
@ -120,12 +122,6 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState
|
|||
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction()
|
||||
{
|
||||
this.setupTerritoryTypeHook.Enable();
|
||||
}
|
||||
|
||||
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType)
|
||||
{
|
||||
this.TerritoryType = terriType;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ internal sealed partial class Condition : IServiceType, ICondition
|
|||
/// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
|
||||
/// </summary>
|
||||
internal const int MaxConditionEntries = 104;
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
private readonly bool[] cache = new bool[MaxConditionEntries];
|
||||
|
||||
|
|
@ -24,6 +27,12 @@ internal sealed partial class Condition : IServiceType, ICondition
|
|||
{
|
||||
var resolver = clientState.AddressResolver;
|
||||
this.Address = resolver.ConditionFlags;
|
||||
|
||||
// Initialization
|
||||
for (var i = 0; i < MaxConditionEntries; i++)
|
||||
this.cache[i] = this[i];
|
||||
|
||||
this.framework.Update += this.FrameworkUpdate;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -80,17 +89,7 @@ internal sealed partial class Condition : IServiceType, ICondition
|
|||
return false;
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction(Framework framework)
|
||||
{
|
||||
// Initialization
|
||||
for (var i = 0; i < MaxConditionEntries; i++)
|
||||
this.cache[i] = this[i];
|
||||
|
||||
framework.Update += this.FrameworkUpdate;
|
||||
}
|
||||
|
||||
private void FrameworkUpdate(IFramework framework)
|
||||
private void FrameworkUpdate(IFramework unused)
|
||||
{
|
||||
for (var i = 0; i < MaxConditionEntries; i++)
|
||||
{
|
||||
|
|
@ -144,7 +143,7 @@ internal sealed partial class Condition : IDisposable
|
|||
|
||||
if (disposing)
|
||||
{
|
||||
Service<Framework>.Get().Update -= this.FrameworkUpdate;
|
||||
this.framework.Update -= this.FrameworkUpdate;
|
||||
}
|
||||
|
||||
this.isDisposed = true;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
|
|||
var resolver = clientState.AddressResolver;
|
||||
Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}");
|
||||
this.gamepadPoll = Hook<ControllerPoll>.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour);
|
||||
this.gamepadPoll?.Enable();
|
||||
}
|
||||
|
||||
private delegate int ControllerPoll(IntPtr controllerInput);
|
||||
|
|
@ -114,12 +115,6 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState
|
|||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction()
|
||||
{
|
||||
this.gamepadPoll?.Enable();
|
||||
}
|
||||
|
||||
private int GamepadPollDetour(IntPtr gamepadInput)
|
||||
{
|
||||
var original = this.gamepadPoll!.Original(gamepadInput);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
|
|||
|
||||
this.framework.Update += this.FrameworkOnUpdateEvent;
|
||||
this.clientState.TerritoryChanged += this.TerritoryOnChangedEvent;
|
||||
|
||||
this.contentDirectorNetworkMessageHook.Enable();
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
|
|
@ -67,12 +69,6 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
|
|||
this.clientState.TerritoryChanged -= this.TerritoryOnChangedEvent;
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction()
|
||||
{
|
||||
this.contentDirectorNetworkMessageHook.Enable();
|
||||
}
|
||||
|
||||
private byte ContentDirectorNetworkMessageDetour(IntPtr a1, IntPtr a2, ushort* a3)
|
||||
{
|
||||
var category = *a3;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
|
||||
this.updateHook = Hook<OnUpdateDetour>.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate);
|
||||
this.destroyHook = Hook<OnRealDestroyDelegate>.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy);
|
||||
|
||||
this.updateHook.Enable();
|
||||
this.destroyHook.Enable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -330,13 +333,6 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
|
|||
}
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction()
|
||||
{
|
||||
this.updateHook.Enable();
|
||||
this.destroyHook.Enable();
|
||||
}
|
||||
|
||||
private void RunPendingTickTasks()
|
||||
{
|
||||
if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
|||
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour);
|
||||
this.populateItemLinkHook = Hook<PopulateItemLinkDelegate>.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour);
|
||||
this.interactableLinkClickedHook = Hook<InteractableLinkClickedDelegate>.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour);
|
||||
|
||||
this.printMessageHook.Enable();
|
||||
this.populateItemLinkHook.Enable();
|
||||
this.interactableLinkClickedHook.Enable();
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
|
|
@ -182,14 +186,6 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui
|
|||
this.dalamudLinkHandlers.Remove((pluginName, commandId));
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction()
|
||||
{
|
||||
this.printMessageHook.Enable();
|
||||
this.populateItemLinkHook.Enable();
|
||||
this.interactableLinkClickedHook.Enable();
|
||||
}
|
||||
|
||||
private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color)
|
||||
{
|
||||
var builder = new SeStringBuilder();
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
|
|||
|
||||
this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer<AddFlyTextDelegate>(this.Address.AddFlyText);
|
||||
this.createFlyTextHook = Hook<CreateFlyTextDelegate>.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour);
|
||||
|
||||
this.createFlyTextHook.Enable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -143,12 +145,6 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui
|
|||
return terminated;
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction(GameGui gameGui)
|
||||
{
|
||||
this.createFlyTextHook.Enable();
|
||||
}
|
||||
|
||||
private IntPtr CreateFlyTextDetour(
|
||||
IntPtr addonFlyText,
|
||||
FlyTextKind kind,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,15 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
|
|||
this.toggleUiHideHook = Hook<ToggleUiHideDelegate>.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour);
|
||||
|
||||
this.utf8StringFromSequenceHook = Hook<Utf8StringFromSequenceDelegate>.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour);
|
||||
|
||||
this.setGlobalBgmHook.Enable();
|
||||
this.handleItemHoverHook.Enable();
|
||||
this.handleItemOutHook.Enable();
|
||||
this.handleImmHook.Enable();
|
||||
this.toggleUiHideHook.Enable();
|
||||
this.handleActionHoverHook.Enable();
|
||||
this.handleActionOutHook.Enable();
|
||||
this.utf8StringFromSequenceHook.Enable();
|
||||
}
|
||||
|
||||
// Marshaled delegates
|
||||
|
|
@ -376,19 +385,6 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui
|
|||
this.GameUiHidden = false;
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction()
|
||||
{
|
||||
this.setGlobalBgmHook.Enable();
|
||||
this.handleItemHoverHook.Enable();
|
||||
this.handleItemOutHook.Enable();
|
||||
this.handleImmHook.Enable();
|
||||
this.toggleUiHideHook.Enable();
|
||||
this.handleActionHoverHook.Enable();
|
||||
this.handleActionOutHook.Enable();
|
||||
this.utf8StringFromSequenceHook.Enable();
|
||||
}
|
||||
|
||||
private IntPtr HandleSetGlobalBgmDetour(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6)
|
||||
{
|
||||
var retVal = this.setGlobalBgmHook.Original(bgmKey, a2, a3, a4, a5, a6);
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ internal unsafe class DalamudIME : IDisposable, IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
[ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")]
|
||||
private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu
|
|||
this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize);
|
||||
|
||||
this.receiveListingHook = Hook<ReceiveListingDelegate>.FromAddress(this.address.ReceiveListing, this.HandleReceiveListingDetour);
|
||||
this.receiveListingHook.Enable();
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
|
|
@ -60,12 +61,6 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu
|
|||
}
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction(GameGui gameGui)
|
||||
{
|
||||
this.receiveListingHook.Enable();
|
||||
}
|
||||
|
||||
private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui
|
|||
this.showNormalToastHook = Hook<ShowNormalToastDelegate>.FromAddress(this.address.ShowNormalToast, this.HandleNormalToastDetour);
|
||||
this.showQuestToastHook = Hook<ShowQuestToastDelegate>.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour);
|
||||
this.showErrorToastHook = Hook<ShowErrorToastDelegate>.FromAddress(this.address.ShowErrorToast, this.HandleErrorToastDetour);
|
||||
|
||||
this.showNormalToastHook.Enable();
|
||||
this.showQuestToastHook.Enable();
|
||||
this.showErrorToastHook.Enable();
|
||||
}
|
||||
|
||||
#region Marshal delegates
|
||||
|
|
@ -109,14 +113,6 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui
|
|||
return terminated;
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction(GameGui gameGui)
|
||||
{
|
||||
this.showNormalToastHook.Enable();
|
||||
this.showQuestToastHook.Enable();
|
||||
this.showErrorToastHook.Enable();
|
||||
}
|
||||
|
||||
private SeString ParseString(IntPtr text)
|
||||
{
|
||||
var bytes = new List<byte>();
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
|||
this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings");
|
||||
|
||||
// this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened;
|
||||
|
||||
this.hookAgentHudOpenSystemMenu.Enable();
|
||||
this.hookUiModuleRequestMainCommand.Enable();
|
||||
this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
|
||||
}
|
||||
|
||||
private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize);
|
||||
|
|
@ -75,14 +79,6 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType
|
|||
|
||||
private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5);
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction(DalamudInterface dalamudInterface)
|
||||
{
|
||||
this.hookAgentHudOpenSystemMenu.Enable();
|
||||
this.hookUiModuleRequestMainCommand.Enable();
|
||||
this.hookAtkUnitBaseReceiveGlobalEvent.Enable();
|
||||
}
|
||||
|
||||
/*
|
||||
private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
|
|||
|
||||
this.processZonePacketDownHook = Hook<ProcessZonePacketDownDelegate>.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour);
|
||||
this.processZonePacketUpHook = Hook<ProcessZonePacketUpDelegate>.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
|
||||
|
||||
this.processZonePacketDownHook.Enable();
|
||||
this.processZonePacketUpHook.Enable();
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
|
||||
|
|
@ -62,13 +65,6 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork
|
|||
this.processZonePacketUpHook.Dispose();
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction()
|
||||
{
|
||||
this.processZonePacketDownHook.Enable();
|
||||
this.processZonePacketUpHook.Enable();
|
||||
}
|
||||
|
||||
private void ProcessZonePacketDownDetour(IntPtr a, uint targetId, IntPtr dataPtr)
|
||||
{
|
||||
this.baseAddress = a;
|
||||
|
|
|
|||
|
|
@ -1063,14 +1063,10 @@ internal class InterfaceManager : IDisposable, IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
[ServiceManager.CallWhenServicesReady]
|
||||
private void ContinueConstruction(
|
||||
TargetSigScanner sigScanner,
|
||||
DalamudAssetManager dalamudAssetManager,
|
||||
DalamudConfiguration configuration)
|
||||
[ServiceManager.CallWhenServicesReady(
|
||||
"InterfaceManager accepts event registration and stuff even when the game window is not ready.")]
|
||||
private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration)
|
||||
{
|
||||
dalamudAssetManager.WaitForAllRequiredAssets().Wait();
|
||||
|
||||
this.address.Setup(sigScanner);
|
||||
this.framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
|
@ -13,6 +15,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
|||
/// </summary>
|
||||
internal class ServicesWidget : IDataWindowWidget
|
||||
{
|
||||
private readonly Dictionary<ServiceDependencyNode, Vector4> nodeRects = new();
|
||||
private readonly HashSet<Type> selectedNodes = new();
|
||||
private readonly HashSet<Type> tempRelatedNodes = new();
|
||||
|
||||
private bool includeUnloadDependencies;
|
||||
private List<List<ServiceDependencyNode>>? dependencyNodes;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; } = { "services" };
|
||||
|
||||
|
|
@ -33,27 +42,294 @@ internal class ServicesWidget : IDataWindowWidget
|
|||
{
|
||||
var container = Service<ServiceContainer>.Get();
|
||||
|
||||
foreach (var instance in container.Instances)
|
||||
if (ImGui.CollapsingHeader("Dependencies"))
|
||||
{
|
||||
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
|
||||
var isPublic = instance.Key.IsPublic;
|
||||
if (ImGui.Button("Clear selection"))
|
||||
this.selectedNodes.Clear();
|
||||
|
||||
ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})");
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
|
||||
ImGui.SameLine();
|
||||
switch (this.includeUnloadDependencies)
|
||||
{
|
||||
ImGui.Text(hasInterface
|
||||
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
|
||||
: "\t => NO INTERFACE!!!");
|
||||
case true when ImGui.Button("Show load-time dependencies"):
|
||||
this.includeUnloadDependencies = false;
|
||||
this.dependencyNodes = null;
|
||||
break;
|
||||
case false when ImGui.Button("Show unload-time dependencies"):
|
||||
this.includeUnloadDependencies = true;
|
||||
this.dependencyNodes = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (isPublic)
|
||||
this.dependencyNodes ??= ServiceDependencyNode.CreateTreeByLevel(this.includeUnloadDependencies);
|
||||
var cellPad = ImGui.CalcTextSize("WW");
|
||||
var margin = ImGui.CalcTextSize("W\nW\nW");
|
||||
var rowHeight = cellPad.Y * 3;
|
||||
var width = ImGui.GetContentRegionAvail().X;
|
||||
if (ImGui.BeginChild(
|
||||
"dependency-graph",
|
||||
new(width, (this.dependencyNodes.Count * (rowHeight + margin.Y)) + cellPad.Y),
|
||||
false,
|
||||
ImGuiWindowFlags.HorizontalScrollbar))
|
||||
{
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
|
||||
ImGui.Text("\t => PUBLIC!!!");
|
||||
const uint rectBaseBorderColor = 0xFFFFFFFF;
|
||||
const uint rectHoverFillColor = 0xFF404040;
|
||||
const uint rectHoverRelatedFillColor = 0xFF802020;
|
||||
const uint rectSelectedFillColor = 0xFF20A020;
|
||||
const uint rectSelectedRelatedFillColor = 0xFF204020;
|
||||
const uint lineBaseColor = 0xFF808080;
|
||||
const uint lineHoverColor = 0xFFFF8080;
|
||||
const uint lineHoverNotColor = 0xFF404040;
|
||||
const uint lineSelectedColor = 0xFF80FF00;
|
||||
const uint lineInvalidColor = 0xFFFF0000;
|
||||
|
||||
ServiceDependencyNode? hoveredNode = null;
|
||||
|
||||
var pos = ImGui.GetCursorScreenPos();
|
||||
var dl = ImGui.GetWindowDrawList();
|
||||
var mouse = ImGui.GetMousePos();
|
||||
var maxRowWidth = 0f;
|
||||
|
||||
// 1. Layout
|
||||
for (var level = 0; level < this.dependencyNodes.Count; level++)
|
||||
{
|
||||
var levelNodes = this.dependencyNodes[level];
|
||||
|
||||
var rowWidth = 0f;
|
||||
foreach (var node in levelNodes)
|
||||
rowWidth += ImGui.CalcTextSize(node.TypeName).X + cellPad.X + margin.X;
|
||||
|
||||
var off = cellPad / 2;
|
||||
if (rowWidth < width)
|
||||
off.X += ImGui.GetScrollX() + ((width - rowWidth) / 2);
|
||||
else if (rowWidth - ImGui.GetScrollX() < width)
|
||||
off.X += width - (rowWidth - ImGui.GetScrollX());
|
||||
off.Y = (rowHeight + margin.Y) * level;
|
||||
|
||||
foreach (var node in levelNodes)
|
||||
{
|
||||
var textSize = ImGui.CalcTextSize(node.TypeName);
|
||||
var cellSize = textSize + cellPad;
|
||||
|
||||
var rc = new Vector4(pos + off, pos.X + off.X + cellSize.X, pos.Y + off.Y + cellSize.Y);
|
||||
this.nodeRects[node] = rc;
|
||||
if (rc.X <= mouse.X && mouse.X < rc.Z && rc.Y <= mouse.Y && mouse.Y < rc.W)
|
||||
{
|
||||
hoveredNode = node;
|
||||
if (ImGui.IsMouseClicked(ImGuiMouseButton.Left))
|
||||
{
|
||||
if (this.selectedNodes.Contains(node.Type))
|
||||
this.selectedNodes.Remove(node.Type);
|
||||
else
|
||||
this.selectedNodes.Add(node.Type);
|
||||
}
|
||||
}
|
||||
|
||||
off.X += cellSize.X + margin.X;
|
||||
}
|
||||
|
||||
maxRowWidth = Math.Max(maxRowWidth, rowWidth);
|
||||
}
|
||||
|
||||
// 2. Draw non-hovered lines
|
||||
foreach (var levelNodes in this.dependencyNodes)
|
||||
{
|
||||
foreach (var node in levelNodes)
|
||||
{
|
||||
var rect = this.nodeRects[node];
|
||||
var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y);
|
||||
|
||||
foreach (var parent in node.InvalidParents)
|
||||
{
|
||||
rect = this.nodeRects[parent];
|
||||
var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W);
|
||||
if (node == hoveredNode || parent == hoveredNode)
|
||||
continue;
|
||||
|
||||
dl.AddLine(point1, point2, lineInvalidColor, 2f * ImGuiHelpers.GlobalScale);
|
||||
}
|
||||
|
||||
foreach (var parent in node.Parents)
|
||||
{
|
||||
rect = this.nodeRects[parent];
|
||||
var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W);
|
||||
if (node == hoveredNode || parent == hoveredNode)
|
||||
continue;
|
||||
|
||||
var isSelected = this.selectedNodes.Contains(node.Type) ||
|
||||
this.selectedNodes.Contains(parent.Type);
|
||||
dl.AddLine(
|
||||
point1,
|
||||
point2,
|
||||
isSelected
|
||||
? lineSelectedColor
|
||||
: hoveredNode is not null
|
||||
? lineHoverNotColor
|
||||
: lineBaseColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Draw boxes
|
||||
foreach (var levelNodes in this.dependencyNodes)
|
||||
{
|
||||
foreach (var node in levelNodes)
|
||||
{
|
||||
var textSize = ImGui.CalcTextSize(node.TypeName);
|
||||
var cellSize = textSize + cellPad;
|
||||
|
||||
var rc = this.nodeRects[node];
|
||||
if (hoveredNode == node)
|
||||
dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectHoverFillColor);
|
||||
else if (this.selectedNodes.Contains(node.Type))
|
||||
dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectSelectedFillColor);
|
||||
else if (node.Relatives.Any(x => this.selectedNodes.Contains(x.Type)))
|
||||
dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectSelectedRelatedFillColor);
|
||||
else if (hoveredNode?.Relatives.Select(x => x.Type).Contains(node.Type) is true)
|
||||
dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectHoverRelatedFillColor);
|
||||
|
||||
dl.AddRect(new(rc.X, rc.Y), new(rc.Z, rc.W), rectBaseBorderColor);
|
||||
ImGui.SetCursorPos((new Vector2(rc.X, rc.Y) - pos) + ((cellSize - textSize) / 2));
|
||||
ImGui.TextUnformatted(node.TypeName);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Draw hovered lines
|
||||
if (hoveredNode is not null)
|
||||
{
|
||||
foreach (var levelNodes in this.dependencyNodes)
|
||||
{
|
||||
foreach (var node in levelNodes)
|
||||
{
|
||||
var rect = this.nodeRects[node];
|
||||
var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y);
|
||||
foreach (var parent in node.Parents)
|
||||
{
|
||||
if (node == hoveredNode || parent == hoveredNode)
|
||||
{
|
||||
rect = this.nodeRects[parent];
|
||||
var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W);
|
||||
dl.AddLine(
|
||||
point1,
|
||||
point2,
|
||||
lineHoverColor,
|
||||
2 * ImGuiHelpers.GlobalScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SetCursorPos(default);
|
||||
ImGui.Dummy(new(maxRowWidth, this.dependencyNodes.Count * rowHeight));
|
||||
ImGui.EndChild();
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(2);
|
||||
}
|
||||
|
||||
if (ImGui.CollapsingHeader("Plugin-facing Services"))
|
||||
{
|
||||
foreach (var instance in container.Instances)
|
||||
{
|
||||
var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key);
|
||||
var isPublic = instance.Key.IsPublic;
|
||||
|
||||
ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})");
|
||||
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface))
|
||||
{
|
||||
ImGui.Text(
|
||||
hasInterface
|
||||
? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}"
|
||||
: "\t => NO INTERFACE!!!");
|
||||
}
|
||||
|
||||
if (isPublic)
|
||||
{
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
|
||||
ImGui.Text("\t => PUBLIC!!!");
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ServiceDependencyNode
|
||||
{
|
||||
private readonly List<ServiceDependencyNode> parents = new();
|
||||
private readonly List<ServiceDependencyNode> children = new();
|
||||
private readonly List<ServiceDependencyNode> invalidParents = new();
|
||||
|
||||
private ServiceDependencyNode(Type t) => this.Type = t;
|
||||
|
||||
public Type Type { get; }
|
||||
|
||||
public string TypeName => this.Type.Name;
|
||||
|
||||
public IReadOnlyList<ServiceDependencyNode> Parents => this.parents;
|
||||
|
||||
public IReadOnlyList<ServiceDependencyNode> Children => this.children;
|
||||
|
||||
public IReadOnlyList<ServiceDependencyNode> InvalidParents => this.invalidParents;
|
||||
|
||||
public IEnumerable<ServiceDependencyNode> Relatives =>
|
||||
this.parents.Concat(this.children).Concat(this.invalidParents);
|
||||
|
||||
public int Level { get; private set; }
|
||||
|
||||
public static List<ServiceDependencyNode> CreateTree(bool includeUnloadDependencies)
|
||||
{
|
||||
var nodes = new Dictionary<Type, ServiceDependencyNode>();
|
||||
foreach (var t in ServiceManager.GetConcreteServiceTypes())
|
||||
nodes.Add(typeof(Service<>).MakeGenericType(t), new(t));
|
||||
foreach (var t in ServiceManager.GetConcreteServiceTypes())
|
||||
{
|
||||
var st = typeof(Service<>).MakeGenericType(t);
|
||||
var node = nodes[st];
|
||||
foreach (var depType in ServiceHelpers.GetDependencies(st, includeUnloadDependencies))
|
||||
{
|
||||
var depServiceType = typeof(Service<>).MakeGenericType(depType);
|
||||
var depNode = nodes[depServiceType];
|
||||
if (node.IsAncestorOf(depType))
|
||||
{
|
||||
node.invalidParents.Add(depNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
depNode.UpdateNodeLevel(1);
|
||||
node.UpdateNodeLevel(depNode.Level + 1);
|
||||
node.parents.Add(depNode);
|
||||
depNode.children.Add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nodes.Values.OrderBy(x => x.Level).ThenBy(x => x.Type.Name).ToList();
|
||||
}
|
||||
|
||||
public static List<List<ServiceDependencyNode>> CreateTreeByLevel(bool includeUnloadDependencies)
|
||||
{
|
||||
var res = new List<List<ServiceDependencyNode>>();
|
||||
foreach (var n in CreateTree(includeUnloadDependencies))
|
||||
{
|
||||
while (res.Count <= n.Level)
|
||||
res.Add(new());
|
||||
res[n.Level].Add(n);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private bool IsAncestorOf(Type type) =>
|
||||
this.children.Any(x => x.Type == type) || this.children.Any(x => x.IsAncestorOf(type));
|
||||
|
||||
private void UpdateNodeLevel(int newLevel)
|
||||
{
|
||||
if (this.Level >= newLevel)
|
||||
return;
|
||||
|
||||
this.Level = newLevel;
|
||||
foreach (var c in this.children)
|
||||
c.UpdateNodeLevel(newLevel + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ using Dalamud.Game.Text.SeStringHandling;
|
|||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Networking.Http;
|
||||
|
|
@ -29,6 +30,7 @@ using Dalamud.Plugin.Internal.Profiles;
|
|||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Plugin.Internal.Types.Manifest;
|
||||
using Dalamud.Plugin.Ipc.Internal;
|
||||
using Dalamud.Support;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Timing;
|
||||
using Newtonsoft.Json;
|
||||
|
|
@ -93,7 +95,9 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
}
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private PluginManager()
|
||||
private PluginManager(
|
||||
ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker,
|
||||
ServiceManager.RegisterUnloadAfterDelegate registerUnloadAfter)
|
||||
{
|
||||
this.pluginDirectory = new DirectoryInfo(this.dalamud.StartInfo.PluginDirectory!);
|
||||
|
||||
|
|
@ -142,6 +146,14 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient);
|
||||
|
||||
this.ApplyPatches();
|
||||
|
||||
registerStartupBlocker(
|
||||
Task.Run(this.LoadAndStartLoadSyncPlugins),
|
||||
"Waiting for plugins that asked to be loaded before the game.");
|
||||
|
||||
registerUnloadAfter(
|
||||
ResolvePossiblePluginDependencyServices(),
|
||||
"See the attached comment for the called function.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1201,6 +1213,49 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
/// <returns>The calling plugin, or null.</returns>
|
||||
public LocalPlugin? FindCallingPlugin() => this.FindCallingPlugin(new StackTrace());
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the services that a plugin may have a dependency on.<br />
|
||||
/// This is required, as the lifetime of a plugin cannot be longer than PluginManager,
|
||||
/// and we want to ensure that dependency services to be kept alive at least until all the plugins, and thus
|
||||
/// PluginManager to be gone.
|
||||
/// </summary>
|
||||
/// <returns>The dependency services.</returns>
|
||||
private static IEnumerable<Type> ResolvePossiblePluginDependencyServices()
|
||||
{
|
||||
foreach (var serviceType in ServiceManager.GetConcreteServiceTypes())
|
||||
{
|
||||
if (serviceType == typeof(PluginManager))
|
||||
continue;
|
||||
|
||||
// Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away.
|
||||
// Nonetheless, their direct dependencies must be considered.
|
||||
if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService)
|
||||
{
|
||||
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
||||
var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT, false);
|
||||
ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count);
|
||||
|
||||
foreach (var scopedDep in dependencies)
|
||||
{
|
||||
if (scopedDep == typeof(PluginManager))
|
||||
throw new Exception("Scoped plugin services cannot depend on PluginManager.");
|
||||
|
||||
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!);
|
||||
yield return scopedDep;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var pluginInterfaceAttribute = serviceType.GetCustomAttribute<PluginInterfaceAttribute>(true);
|
||||
if (pluginInterfaceAttribute == null)
|
||||
continue;
|
||||
|
||||
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!);
|
||||
yield return serviceType;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Stream> DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting)
|
||||
{
|
||||
var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
|
||||
|
|
@ -1590,6 +1645,38 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
private void LoadAndStartLoadSyncPlugins()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (Timings.Start("PM Load Plugin Repos"))
|
||||
{
|
||||
_ = this.SetPluginReposFromConfigAsync(false);
|
||||
this.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting);
|
||||
|
||||
Log.Information("[T3] PM repos OK!");
|
||||
}
|
||||
|
||||
using (Timings.Start("PM Cleanup Plugins"))
|
||||
{
|
||||
this.CleanupPlugins();
|
||||
Log.Information("[T3] PMC OK!");
|
||||
}
|
||||
|
||||
using (Timings.Start("PM Load Sync Plugins"))
|
||||
{
|
||||
this.LoadAllPlugins().Wait();
|
||||
Log.Information("[T3] PML OK!");
|
||||
}
|
||||
|
||||
_ = Task.Run(Troubleshooting.LogTroubleshooting);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Plugin load failed");
|
||||
}
|
||||
}
|
||||
|
||||
private static class Locs
|
||||
{
|
||||
public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version);
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Support;
|
||||
using Dalamud.Utility.Timing;
|
||||
|
||||
namespace Dalamud.Plugin.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Class responsible for loading plugins on startup.
|
||||
/// </summary>
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
public class StartupPluginLoader : IServiceType
|
||||
{
|
||||
private static readonly ModuleLog Log = new("SPL");
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private StartupPluginLoader(PluginManager pluginManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (Timings.Start("PM Load Plugin Repos"))
|
||||
{
|
||||
_ = pluginManager.SetPluginReposFromConfigAsync(false);
|
||||
pluginManager.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting);
|
||||
|
||||
Log.Information("[T3] PM repos OK!");
|
||||
}
|
||||
|
||||
using (Timings.Start("PM Cleanup Plugins"))
|
||||
{
|
||||
pluginManager.CleanupPlugins();
|
||||
Log.Information("[T3] PMC OK!");
|
||||
}
|
||||
|
||||
using (Timings.Start("PM Load Sync Plugins"))
|
||||
{
|
||||
pluginManager.LoadAllPlugins().Wait();
|
||||
Log.Information("[T3] PML OK!");
|
||||
}
|
||||
|
||||
Task.Run(Troubleshooting.LogTroubleshooting);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Plugin load failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ using Dalamud.Game;
|
|||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Storage;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Timing;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
|
|
@ -21,7 +22,7 @@ namespace Dalamud;
|
|||
// - Visualize/output .dot or imgui thing
|
||||
|
||||
/// <summary>
|
||||
/// Class to initialize Service<T>s.
|
||||
/// Class to initialize <see cref="Service{T}"/>.
|
||||
/// </summary>
|
||||
internal static class ServiceManager
|
||||
{
|
||||
|
|
@ -43,6 +44,26 @@ internal static class ServiceManager
|
|||
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
|
||||
|
||||
private static ManualResetEvent unloadResetEvent = new(false);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for registering startup blocker task.<br />
|
||||
/// Do not use this delegate outside the constructor.
|
||||
/// </summary>
|
||||
/// <param name="t">The blocker task.</param>
|
||||
/// <param name="justification">The justification for using this feature.</param>
|
||||
[InjectableType]
|
||||
public delegate void RegisterStartupBlockerDelegate(Task t, string justification);
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for registering services that should be unloaded before self.<br />
|
||||
/// Intended for use with <see cref="Plugin.Internal.PluginManager"/>. If you think you need to use this outside
|
||||
/// of that, consider having a discussion first.<br />
|
||||
/// Do not use this delegate outside the constructor.
|
||||
/// </summary>
|
||||
/// <param name="unloadAfter">Services that should be unloaded first.</param>
|
||||
/// <param name="justification">The justification for using this feature.</param>
|
||||
[InjectableType]
|
||||
public delegate void RegisterUnloadAfterDelegate(IEnumerable<Type> unloadAfter, string justification);
|
||||
|
||||
/// <summary>
|
||||
/// Kinds of services.
|
||||
|
|
@ -125,6 +146,15 @@ internal static class ServiceManager
|
|||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the concrete types of services, i.e. the non-abstract non-interface types.
|
||||
/// </summary>
|
||||
/// <returns>The enumerable of service types, that may be enumerated only once per call.</returns>
|
||||
public static IEnumerable<Type> GetConcreteServiceTypes() =>
|
||||
Assembly.GetExecutingAssembly()
|
||||
.GetTypes()
|
||||
.Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract);
|
||||
|
||||
/// <summary>
|
||||
/// Kicks off construction of services that can handle early loading.
|
||||
/// </summary>
|
||||
|
|
@ -141,7 +171,7 @@ internal static class ServiceManager
|
|||
|
||||
var serviceContainer = Service<ServiceContainer>.Get();
|
||||
|
||||
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract))
|
||||
foreach (var serviceType in GetConcreteServiceTypes())
|
||||
{
|
||||
var serviceKind = serviceType.GetServiceKind();
|
||||
Debug.Assert(serviceKind != ServiceKind.None, $"Service<{serviceType.FullName}> did not specify a kind");
|
||||
|
|
@ -157,7 +187,7 @@ internal static class ServiceManager
|
|||
|
||||
var getTask = (Task)genericWrappedServiceType
|
||||
.InvokeMember(
|
||||
"GetAsync",
|
||||
nameof(Service<IServiceType>.GetAsync),
|
||||
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
|
||||
null,
|
||||
null,
|
||||
|
|
@ -184,17 +214,42 @@ internal static class ServiceManager
|
|||
}
|
||||
|
||||
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
||||
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT)
|
||||
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, false)
|
||||
.Select(x => typeof(Service<>).MakeGenericType(x))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var blockerTasks = new List<Task>();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var whenBlockingComplete = Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]));
|
||||
while (await Task.WhenAny(whenBlockingComplete, Task.Delay(120000)) != whenBlockingComplete)
|
||||
// Wait for all blocking constructors to complete first.
|
||||
await WaitWithTimeoutConsent(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]));
|
||||
|
||||
// All the BlockingEarlyLoadedService constructors have been run,
|
||||
// and blockerTasks now will not change. Now wait for them.
|
||||
// Note that ServiceManager.CallWhenServicesReady does not get to register a blocker.
|
||||
await WaitWithTimeoutConsent(blockerTasks);
|
||||
|
||||
BlockingServicesLoadedTaskCompletionSource.SetResult();
|
||||
Timings.Event("BlockingServices Initialized");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BlockingServicesLoadedTaskCompletionSource.SetException(e);
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
async Task WaitWithTimeoutConsent(IEnumerable<Task> tasksEnumerable)
|
||||
{
|
||||
var tasks = tasksEnumerable.AsReadOnlyCollection();
|
||||
if (tasks.Count == 0)
|
||||
return;
|
||||
|
||||
var aggregatedTask = Task.WhenAll(tasks);
|
||||
while (await Task.WhenAny(aggregatedTask, Task.Delay(120000)) != aggregatedTask)
|
||||
{
|
||||
if (NativeFunctions.MessageBoxW(
|
||||
IntPtr.Zero,
|
||||
|
|
@ -208,13 +263,6 @@ internal static class ServiceManager
|
|||
"and the user chose to continue without Dalamud.");
|
||||
}
|
||||
}
|
||||
|
||||
BlockingServicesLoadedTaskCompletionSource.SetResult();
|
||||
Timings.Event("BlockingServices Initialized");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
BlockingServicesLoadedTaskCompletionSource.SetException(e);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
|
|
@ -249,6 +297,25 @@ internal static class ServiceManager
|
|||
if (!hasDeps)
|
||||
continue;
|
||||
|
||||
// This object will be used in a task. Each task must receive a new object.
|
||||
var startLoaderArgs = new List<object>();
|
||||
if (serviceType.GetCustomAttribute<BlockingEarlyLoadedServiceAttribute>() is not null)
|
||||
{
|
||||
startLoaderArgs.Add(
|
||||
new RegisterStartupBlockerDelegate(
|
||||
(task, justification) =>
|
||||
{
|
||||
#if DEBUG
|
||||
if (CurrentConstructorServiceType.Value != serviceType)
|
||||
throw new InvalidOperationException("Forbidden.");
|
||||
#endif
|
||||
blockerTasks.Add(task);
|
||||
|
||||
// No need to store the justification; the fact that the reason is specified is good enough.
|
||||
_ = justification;
|
||||
}));
|
||||
}
|
||||
|
||||
tasks.Add((Task)typeof(Service<>)
|
||||
.MakeGenericType(serviceType)
|
||||
.InvokeMember(
|
||||
|
|
@ -256,7 +323,7 @@ internal static class ServiceManager
|
|||
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic,
|
||||
null,
|
||||
null,
|
||||
null));
|
||||
new object[] { startLoaderArgs }));
|
||||
servicesToLoad.Remove(serviceType);
|
||||
|
||||
#if DEBUG
|
||||
|
|
@ -328,13 +395,13 @@ internal static class ServiceManager
|
|||
|
||||
unloadResetEvent.Reset();
|
||||
|
||||
var dependencyServicesMap = new Dictionary<Type, List<Type>>();
|
||||
var dependencyServicesMap = new Dictionary<Type, IReadOnlyCollection<Type>>();
|
||||
var allToUnload = new HashSet<Type>();
|
||||
var unloadOrder = new List<Type>();
|
||||
|
||||
Log.Information("==== COLLECTING SERVICES TO UNLOAD ====");
|
||||
|
||||
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes())
|
||||
foreach (var serviceType in GetConcreteServiceTypes())
|
||||
{
|
||||
if (!serviceType.IsAssignableTo(typeof(IServiceType)))
|
||||
continue;
|
||||
|
|
@ -347,7 +414,7 @@ internal static class ServiceManager
|
|||
Log.Verbose("Calling GetDependencyServices for '{ServiceName}'", serviceType.FullName!);
|
||||
|
||||
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
||||
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT);
|
||||
dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, true);
|
||||
|
||||
allToUnload.Add(serviceType);
|
||||
}
|
||||
|
|
@ -541,11 +608,35 @@ internal static class ServiceManager
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the method should be called when the services given in the constructor are ready.
|
||||
/// Indicates that the method should be called when the services given in the marked method's parameters are ready.
|
||||
/// This will be executed immediately after the constructor has run, if all services specified as its parameters
|
||||
/// are already ready, or no parameter is given.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
[MeansImplicitUse]
|
||||
public class CallWhenServicesReady : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CallWhenServicesReady"/> class.
|
||||
/// </summary>
|
||||
/// <param name="justification">Specify the reason here.</param>
|
||||
public CallWhenServicesReady(string justification)
|
||||
{
|
||||
// No need to store the justification; the fact that the reason is specified is good enough.
|
||||
_ = justification;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that something is a candidate for being considered as an injected parameter for constructors.
|
||||
/// </summary>
|
||||
[AttributeUsage(
|
||||
AttributeTargets.Delegate
|
||||
| AttributeTargets.Class
|
||||
| AttributeTargets.Struct
|
||||
| AttributeTargets.Enum
|
||||
| AttributeTargets.Interface)]
|
||||
public class InjectableTypeAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
|||
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.IoC.Internal;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Utility.Timing;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
|
|
@ -25,6 +24,7 @@ internal static class Service<T> where T : IServiceType
|
|||
private static readonly ServiceManager.ServiceAttribute ServiceAttribute;
|
||||
private static TaskCompletionSource<T> instanceTcs = new();
|
||||
private static List<Type>? dependencyServices;
|
||||
private static List<Type>? dependencyServicesForUnload;
|
||||
|
||||
static Service()
|
||||
{
|
||||
|
|
@ -95,7 +95,7 @@ internal static class Service<T> where T : IServiceType
|
|||
if (ServiceAttribute.Kind != ServiceManager.ServiceKind.ProvidedService
|
||||
&& ServiceManager.CurrentConstructorServiceType.Value is { } currentServiceType)
|
||||
{
|
||||
var deps = ServiceHelpers.GetDependencies(currentServiceType);
|
||||
var deps = ServiceHelpers.GetDependencies(typeof(Service<>).MakeGenericType(currentServiceType), false);
|
||||
if (!deps.Contains(typeof(T)))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
|
|
@ -115,7 +115,6 @@ internal static class Service<T> where T : IServiceType
|
|||
/// Pull the instance out of the service locator, waiting if necessary.
|
||||
/// </summary>
|
||||
/// <returns>The object.</returns>
|
||||
[UsedImplicitly]
|
||||
public static Task<T> GetAsync() => instanceTcs.Task;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -141,11 +140,15 @@ internal static class Service<T> where T : IServiceType
|
|||
/// <summary>
|
||||
/// Gets an enumerable containing <see cref="Service{T}"/>s that are required for this Service to initialize
|
||||
/// without blocking.
|
||||
/// These are NOT returned as <see cref="Service{T}"/> types; raw types will be returned.
|
||||
/// </summary>
|
||||
/// <param name="includeUnloadDependencies">Whether to include the unload dependencies.</param>
|
||||
/// <returns>List of dependency services.</returns>
|
||||
[UsedImplicitly]
|
||||
public static List<Type> GetDependencyServices()
|
||||
public static IReadOnlyCollection<Type> GetDependencyServices(bool includeUnloadDependencies)
|
||||
{
|
||||
if (includeUnloadDependencies && dependencyServicesForUnload is not null)
|
||||
return dependencyServicesForUnload;
|
||||
|
||||
if (dependencyServices is not null)
|
||||
return dependencyServices;
|
||||
|
||||
|
|
@ -158,7 +161,8 @@ internal static class Service<T> where T : IServiceType
|
|||
{
|
||||
res.AddRange(ctor
|
||||
.GetParameters()
|
||||
.Select(x => x.ParameterType));
|
||||
.Select(x => x.ParameterType)
|
||||
.Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None));
|
||||
}
|
||||
|
||||
res.AddRange(typeof(T)
|
||||
|
|
@ -171,50 +175,8 @@ internal static class Service<T> where T : IServiceType
|
|||
.OfType<InherentDependencyAttribute>()
|
||||
.Select(x => x.GetType().GetGenericArguments().First()));
|
||||
|
||||
// HACK: PluginManager needs to depend on ALL plugin exposed services
|
||||
if (typeof(T) == typeof(PluginManager))
|
||||
{
|
||||
foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes())
|
||||
{
|
||||
if (!serviceType.IsAssignableTo(typeof(IServiceType)))
|
||||
continue;
|
||||
|
||||
if (serviceType == typeof(PluginManager))
|
||||
continue;
|
||||
|
||||
// Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away.
|
||||
// Nonetheless, their direct dependencies must be considered.
|
||||
if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService)
|
||||
{
|
||||
var typeAsServiceT = ServiceHelpers.GetAsService(serviceType);
|
||||
var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT);
|
||||
ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count);
|
||||
|
||||
foreach (var scopedDep in dependencies)
|
||||
{
|
||||
if (scopedDep == typeof(PluginManager))
|
||||
throw new Exception("Scoped plugin services cannot depend on PluginManager.");
|
||||
|
||||
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!);
|
||||
res.Add(scopedDep);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var pluginInterfaceAttribute = serviceType.GetCustomAttribute<PluginInterfaceAttribute>(true);
|
||||
if (pluginInterfaceAttribute == null)
|
||||
continue;
|
||||
|
||||
ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!);
|
||||
res.Add(serviceType);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var type in res)
|
||||
{
|
||||
ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name);
|
||||
}
|
||||
|
||||
var deps = res
|
||||
.Distinct()
|
||||
|
|
@ -244,8 +206,9 @@ internal static class Service<T> where T : IServiceType
|
|||
/// <summary>
|
||||
/// Starts the service loader. Only to be called from <see cref="ServiceManager"/>.
|
||||
/// </summary>
|
||||
/// <param name="additionalProvidedTypedObjects">Additional objects available to constructors.</param>
|
||||
/// <returns>The loader task.</returns>
|
||||
internal static Task<T> StartLoader()
|
||||
internal static Task<T> StartLoader(IReadOnlyCollection<object> additionalProvidedTypedObjects)
|
||||
{
|
||||
if (instanceTcs.Task.IsCompleted)
|
||||
throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed.");
|
||||
|
|
@ -256,10 +219,27 @@ internal static class Service<T> where T : IServiceType
|
|||
|
||||
return Task.Run(Timings.AttachTimingHandle(async () =>
|
||||
{
|
||||
var ctorArgs = new List<object>(additionalProvidedTypedObjects.Count + 1);
|
||||
ctorArgs.AddRange(additionalProvidedTypedObjects);
|
||||
ctorArgs.Add(
|
||||
new ServiceManager.RegisterUnloadAfterDelegate(
|
||||
(additionalDependencies, justification) =>
|
||||
{
|
||||
#if DEBUG
|
||||
if (ServiceManager.CurrentConstructorServiceType.Value != typeof(T))
|
||||
throw new InvalidOperationException("Forbidden.");
|
||||
#endif
|
||||
dependencyServicesForUnload ??= new(GetDependencyServices(false));
|
||||
dependencyServicesForUnload.AddRange(additionalDependencies);
|
||||
|
||||
// No need to store the justification; the fact that the reason is specified is good enough.
|
||||
_ = justification;
|
||||
}));
|
||||
|
||||
ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name);
|
||||
try
|
||||
{
|
||||
var instance = await ConstructObject();
|
||||
var instance = await ConstructObject(ctorArgs).ConfigureAwait(false);
|
||||
instanceTcs.SetResult(instance);
|
||||
|
||||
List<Task>? tasks = null;
|
||||
|
|
@ -270,8 +250,17 @@ internal static class Service<T> where T : IServiceType
|
|||
continue;
|
||||
|
||||
ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name);
|
||||
var args = await Task.WhenAll(method.GetParameters().Select(
|
||||
x => ResolveServiceFromTypeAsync(x.ParameterType)));
|
||||
var args = await ResolveInjectedParameters(
|
||||
method.GetParameters(),
|
||||
Array.Empty<object>()).ConfigureAwait(false);
|
||||
if (args.Length == 0)
|
||||
{
|
||||
ServiceManager.Log.Warning(
|
||||
"Service<{0}>: Method {1} does not have any arguments. Consider merging it with the ctor.",
|
||||
typeof(T).Name,
|
||||
method.Name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (method.Invoke(instance, args) is Task task)
|
||||
|
|
@ -331,24 +320,6 @@ internal static class Service<T> where T : IServiceType
|
|||
instanceTcs.SetException(new UnloadedException());
|
||||
}
|
||||
|
||||
private static async Task<object?> ResolveServiceFromTypeAsync(Type type)
|
||||
{
|
||||
var task = (Task)typeof(Service<>)
|
||||
.MakeGenericType(type)
|
||||
.InvokeMember(
|
||||
"GetAsync",
|
||||
BindingFlags.InvokeMethod |
|
||||
BindingFlags.Static |
|
||||
BindingFlags.Public,
|
||||
null,
|
||||
null,
|
||||
null)!;
|
||||
await task;
|
||||
return typeof(Task<>).MakeGenericType(type)
|
||||
.GetProperty("Result", BindingFlags.Instance | BindingFlags.Public)!
|
||||
.GetValue(task);
|
||||
}
|
||||
|
||||
private static ConstructorInfo? GetServiceConstructor()
|
||||
{
|
||||
const BindingFlags ctorBindingFlags =
|
||||
|
|
@ -359,18 +330,18 @@ internal static class Service<T> where T : IServiceType
|
|||
.SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any());
|
||||
}
|
||||
|
||||
private static async Task<T> ConstructObject()
|
||||
private static async Task<T> ConstructObject(IReadOnlyCollection<object> additionalProvidedTypedObjects)
|
||||
{
|
||||
var ctor = GetServiceConstructor();
|
||||
if (ctor == null)
|
||||
throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor");
|
||||
|
||||
var args = await Task.WhenAll(
|
||||
ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType)));
|
||||
var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects)
|
||||
.ConfigureAwait(false);
|
||||
using (Timings.Start($"{typeof(T).Name} Construct"))
|
||||
{
|
||||
#if DEBUG
|
||||
ServiceManager.CurrentConstructorServiceType.Value = typeof(Service<T>);
|
||||
ServiceManager.CurrentConstructorServiceType.Value = typeof(T);
|
||||
try
|
||||
{
|
||||
return (T)ctor.Invoke(args)!;
|
||||
|
|
@ -385,6 +356,43 @@ internal static class Service<T> where T : IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
private static Task<object[]> ResolveInjectedParameters(
|
||||
IReadOnlyList<ParameterInfo> argDefs,
|
||||
IReadOnlyCollection<object> additionalProvidedTypedObjects)
|
||||
{
|
||||
var argTasks = new Task<object>[argDefs.Count];
|
||||
for (var i = 0; i < argDefs.Count; i++)
|
||||
{
|
||||
var argType = argDefs[i].ParameterType;
|
||||
ref var argTask = ref argTasks[i];
|
||||
|
||||
if (argType.GetCustomAttribute<ServiceManager.InjectableTypeAttribute>() is not null)
|
||||
{
|
||||
argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType));
|
||||
continue;
|
||||
}
|
||||
|
||||
argTask = (Task<object>)typeof(Service<>)
|
||||
.MakeGenericType(argType)
|
||||
.InvokeMember(
|
||||
nameof(GetAsyncAsObject),
|
||||
BindingFlags.InvokeMethod |
|
||||
BindingFlags.Static |
|
||||
BindingFlags.NonPublic,
|
||||
null,
|
||||
null,
|
||||
null)!;
|
||||
}
|
||||
|
||||
return Task.WhenAll(argTasks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pull the instance out of the service locator, waiting if necessary.
|
||||
/// </summary>
|
||||
/// <returns>The object.</returns>
|
||||
private static Task<object> GetAsyncAsObject() => instanceTcs.Task.ContinueWith(r => (object)r.Result);
|
||||
|
||||
/// <summary>
|
||||
/// Exception thrown when service is attempted to be retrieved when it's unloaded.
|
||||
/// </summary>
|
||||
|
|
@ -407,11 +415,12 @@ internal static class ServiceHelpers
|
|||
{
|
||||
/// <summary>
|
||||
/// Get a list of dependencies for a service. Only accepts <see cref="Service{T}"/> types.
|
||||
/// These are returned as <see cref="Service{T}"/> types.
|
||||
/// These are NOT returned as <see cref="Service{T}"/> types; raw types will be returned.
|
||||
/// </summary>
|
||||
/// <param name="serviceType">The dependencies for this service.</param>
|
||||
/// <param name="includeUnloadDependencies">Whether to include the unload dependencies.</param>
|
||||
/// <returns>A list of dependencies.</returns>
|
||||
public static List<Type> GetDependencies(Type serviceType)
|
||||
public static IReadOnlyCollection<Type> GetDependencies(Type serviceType, bool includeUnloadDependencies)
|
||||
{
|
||||
#if DEBUG
|
||||
if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>))
|
||||
|
|
@ -422,12 +431,12 @@ internal static class ServiceHelpers
|
|||
}
|
||||
#endif
|
||||
|
||||
return (List<Type>)serviceType.InvokeMember(
|
||||
return (IReadOnlyCollection<Type>)serviceType.InvokeMember(
|
||||
nameof(Service<IServiceType>.GetDependencyServices),
|
||||
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
|
||||
null,
|
||||
null,
|
||||
null) ?? new List<Type>();
|
||||
new object?[] { includeUnloadDependencies }) ?? new List<Type>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
private bool isDisposed;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private DalamudAssetManager(Dalamud dalamud, HappyHttpClient httpClient)
|
||||
private DalamudAssetManager(
|
||||
Dalamud dalamud,
|
||||
HappyHttpClient httpClient,
|
||||
ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker)
|
||||
{
|
||||
this.dalamud = dalamud;
|
||||
this.httpClient = httpClient;
|
||||
|
|
@ -55,8 +58,17 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
this.fileStreams = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<FileStream>?)null);
|
||||
this.textureWraps = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<IDalamudTextureWrap>?)null);
|
||||
|
||||
// Block until all the required assets to be ready.
|
||||
var loadTimings = Timings.Start("DAM LoadAll");
|
||||
this.WaitForAllRequiredAssets().ContinueWith(_ => loadTimings.Dispose());
|
||||
registerStartupBlocker(
|
||||
Task.WhenAll(
|
||||
Enum.GetValues<DalamudAsset>()
|
||||
.Where(x => x is not DalamudAsset.Empty4X4)
|
||||
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is true)
|
||||
.Select(this.CreateStreamAsync)
|
||||
.Select(x => x.ToContentDisposedTask()))
|
||||
.ContinueWith(_ => loadTimings.Dispose()),
|
||||
"Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -83,25 +95,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
this.scopedFinalizer.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for all the required assets to be ready. Will result in a faulted task, if any of the required assets
|
||||
/// has failed to load.
|
||||
/// </summary>
|
||||
/// <returns>The task.</returns>
|
||||
[Pure]
|
||||
public Task WaitForAllRequiredAssets()
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
return Task.WhenAll(
|
||||
Enum.GetValues<DalamudAsset>()
|
||||
.Where(x => x is not DalamudAsset.Empty4X4)
|
||||
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is true)
|
||||
.Select(this.CreateStreamAsync)
|
||||
.Select(x => x.ToContentDisposedTask()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Pure]
|
||||
public bool IsStreamImmediatelyAvailable(DalamudAsset asset) =>
|
||||
|
|
|
|||
|
|
@ -87,4 +87,14 @@ internal static class ArrayExtensions
|
|||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interprets the given array as an <see cref="IReadOnlyCollection{T}"/>, so that you can enumerate it multiple
|
||||
/// times, and know the number of elements within.
|
||||
/// </summary>
|
||||
/// <param name="array">The enumerable.</param>
|
||||
/// <typeparam name="T">The element type.</typeparam>
|
||||
/// <returns><paramref name="array"/> casted as a <see cref="IReadOnlyCollection{T}"/> if it is one; otherwise the result of <see cref="Enumerable.ToArray{TSource}"/>.</returns>
|
||||
public static IReadOnlyCollection<T> AsReadOnlyCollection<T>(this IEnumerable<T> array) =>
|
||||
array as IReadOnlyCollection<T> ?? array.ToArray();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue