diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index d8f3427ef..23f3b1a6d 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -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(); - } - /// /// When an addon finalizes, check it for any registered events, and unregister them. /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 08a2d59ef..3528de562 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -58,6 +58,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); this.onAddonRefreshHook = Hook.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh); this.onAddonRequestedUpdateHook = new CallHook(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. diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 3b3f65128..d387c2e2d 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -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; diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 2db47ea4d..a298b1502 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -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. /// internal const int MaxConditionEntries = 104; + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.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; } /// @@ -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.Get().Update -= this.FrameworkUpdate; + this.framework.Update -= this.FrameworkUpdate; } this.isDisposed = true; diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index b03db6df2..40e632113 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -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.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); diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 66356033b..c4bda0d19 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -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; diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 6db9f7312..ce34f2c06 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -58,6 +58,9 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework this.updateHook = Hook.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate); this.destroyHook = Hook.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy); + + this.updateHook.Enable(); + this.destroyHook.Enable(); } /// @@ -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) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 50c5b2908..8f2a617cf 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -50,6 +50,10 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui this.printMessageHook = Hook.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour); this.populateItemLinkHook = Hook.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); this.interactableLinkClickedHook = Hook.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(); diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 36056883e..2383b4e53 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -36,6 +36,8 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer(this.Address.AddFlyText); this.createFlyTextHook = Hook.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour); + + this.createFlyTextHook.Enable(); } /// @@ -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, diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index a1a17436e..a97e19a0a 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -75,6 +75,15 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui this.toggleUiHideHook = Hook.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour); this.utf8StringFromSequenceHook = Hook.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); diff --git a/Dalamud/Game/Gui/Internal/DalamudIME.cs b/Dalamud/Game/Gui/Internal/DalamudIME.cs index 37c072806..a9f6991ae 100644 --- a/Dalamud/Game/Gui/Internal/DalamudIME.cs +++ b/Dalamud/Game/Gui/Internal/DalamudIME.cs @@ -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 diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 61c0f62e4..4a8332d24 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -35,6 +35,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize); this.receiveListingHook = Hook.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 diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index 362edb3be..7491b7f13 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -41,6 +41,10 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui this.showNormalToastHook = Hook.FromAddress(this.address.ShowNormalToast, this.HandleNormalToastDetour); this.showQuestToastHook = Hook.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour); this.showErrorToastHook = Hook.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(); diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 0013dca4d..4eb605a76 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -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) { diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index 9ea3e491e..4099f228e 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -44,6 +44,9 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork this.processZonePacketDownHook = Hook.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour); this.processZonePacketUpHook = Hook.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; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 52e849c0e..1b12fd853 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -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(() => { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs index 49f3c1b90..22b53cdaa 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs @@ -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; /// internal class ServicesWidget : IDataWindowWidget { + private readonly Dictionary nodeRects = new(); + private readonly HashSet selectedNodes = new(); + private readonly HashSet tempRelatedNodes = new(); + + private bool includeUnloadDependencies; + private List>? dependencyNodes; + /// public string[]? CommandShortcuts { get; init; } = { "services" }; @@ -33,27 +42,294 @@ internal class ServicesWidget : IDataWindowWidget { var container = Service.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 parents = new(); + private readonly List children = new(); + private readonly List invalidParents = new(); + + private ServiceDependencyNode(Type t) => this.Type = t; + + public Type Type { get; } + + public string TypeName => this.Type.Name; + + public IReadOnlyList Parents => this.parents; + + public IReadOnlyList Children => this.children; + + public IReadOnlyList InvalidParents => this.invalidParents; + + public IEnumerable Relatives => + this.parents.Concat(this.children).Concat(this.invalidParents); + + public int Level { get; private set; } + + public static List CreateTree(bool includeUnloadDependencies) + { + var nodes = new Dictionary(); + 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> CreateTreeByLevel(bool includeUnloadDependencies) + { + var res = new List>(); + 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); } } } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 363d01f26..0ef3d49f8 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -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."); } /// @@ -1201,6 +1213,49 @@ internal partial class PluginManager : IDisposable, IServiceType /// The calling plugin, or null. public LocalPlugin? FindCallingPlugin() => this.FindCallingPlugin(new StackTrace()); + /// + /// Resolves the services that a plugin may have a dependency on.
+ /// 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. + ///
+ /// The dependency services. + private static IEnumerable 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(true); + if (pluginInterfaceAttribute == null) + continue; + + ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!); + yield return serviceType; + } + } + private async Task 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); diff --git a/Dalamud/Plugin/Internal/StartupPluginLoader.cs b/Dalamud/Plugin/Internal/StartupPluginLoader.cs deleted file mode 100644 index 4f68d39fc..000000000 --- a/Dalamud/Plugin/Internal/StartupPluginLoader.cs +++ /dev/null @@ -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; - -/// -/// Class responsible for loading plugins on startup. -/// -[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"); - } - } -} diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 21c08ce72..3ff7cde76 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -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 /// -/// Class to initialize Service<T>s. +/// Class to initialize . /// internal static class ServiceManager { @@ -43,6 +44,26 @@ internal static class ServiceManager private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); private static ManualResetEvent unloadResetEvent = new(false); + + /// + /// Delegate for registering startup blocker task.
+ /// Do not use this delegate outside the constructor. + ///
+ /// The blocker task. + /// The justification for using this feature. + [InjectableType] + public delegate void RegisterStartupBlockerDelegate(Task t, string justification); + + /// + /// Delegate for registering services that should be unloaded before self.
+ /// Intended for use with . If you think you need to use this outside + /// of that, consider having a discussion first.
+ /// Do not use this delegate outside the constructor. + ///
+ /// Services that should be unloaded first. + /// The justification for using this feature. + [InjectableType] + public delegate void RegisterUnloadAfterDelegate(IEnumerable unloadAfter, string justification); /// /// Kinds of services. @@ -125,6 +146,15 @@ internal static class ServiceManager #endif } + /// + /// Gets the concrete types of services, i.e. the non-abstract non-interface types. + /// + /// The enumerable of service types, that may be enumerated only once per call. + public static IEnumerable GetConcreteServiceTypes() => + Assembly.GetExecutingAssembly() + .GetTypes() + .Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract); + /// /// Kicks off construction of services that can handle early loading. /// @@ -141,7 +171,7 @@ internal static class ServiceManager var serviceContainer = Service.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.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.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 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(); + if (serviceType.GetCustomAttribute() 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>(); + var dependencyServicesMap = new Dictionary>(); var allToUnload = new HashSet(); var unloadOrder = new List(); 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 } /// - /// 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. /// [AttributeUsage(AttributeTargets.Method)] [MeansImplicitUse] public class CallWhenServicesReady : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// Specify the reason here. + public CallWhenServicesReady(string justification) + { + // No need to store the justification; the fact that the reason is specified is good enough. + _ = justification; + } + } + + /// + /// Indicates that something is a candidate for being considered as an injected parameter for constructors. + /// + [AttributeUsage( + AttributeTargets.Delegate + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Enum + | AttributeTargets.Interface)] + public class InjectableTypeAttribute : Attribute { } } diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 9c7f0411d..08f592826 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -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 where T : IServiceType private static readonly ServiceManager.ServiceAttribute ServiceAttribute; private static TaskCompletionSource instanceTcs = new(); private static List? dependencyServices; + private static List? dependencyServicesForUnload; static Service() { @@ -95,7 +95,7 @@ internal static class Service 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 where T : IServiceType /// Pull the instance out of the service locator, waiting if necessary. /// /// The object. - [UsedImplicitly] public static Task GetAsync() => instanceTcs.Task; /// @@ -141,11 +140,15 @@ internal static class Service where T : IServiceType /// /// Gets an enumerable containing s that are required for this Service to initialize /// without blocking. + /// These are NOT returned as types; raw types will be returned. /// + /// Whether to include the unload dependencies. /// List of dependency services. - [UsedImplicitly] - public static List GetDependencyServices() + public static IReadOnlyCollection 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 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 where T : IServiceType .OfType() .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(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 where T : IServiceType /// /// Starts the service loader. Only to be called from . /// + /// Additional objects available to constructors. /// The loader task. - internal static Task StartLoader() + internal static Task StartLoader(IReadOnlyCollection additionalProvidedTypedObjects) { if (instanceTcs.Task.IsCompleted) throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed."); @@ -256,10 +219,27 @@ internal static class Service where T : IServiceType return Task.Run(Timings.AttachTimingHandle(async () => { + var ctorArgs = new List(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? tasks = null; @@ -270,8 +250,17 @@ internal static class Service 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()).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 where T : IServiceType instanceTcs.SetException(new UnloadedException()); } - private static async Task 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 where T : IServiceType .SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any()); } - private static async Task ConstructObject() + private static async Task ConstructObject(IReadOnlyCollection 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); + ServiceManager.CurrentConstructorServiceType.Value = typeof(T); try { return (T)ctor.Invoke(args)!; @@ -385,6 +356,43 @@ internal static class Service where T : IServiceType } } + private static Task ResolveInjectedParameters( + IReadOnlyList argDefs, + IReadOnlyCollection additionalProvidedTypedObjects) + { + var argTasks = new Task[argDefs.Count]; + for (var i = 0; i < argDefs.Count; i++) + { + var argType = argDefs[i].ParameterType; + ref var argTask = ref argTasks[i]; + + if (argType.GetCustomAttribute() is not null) + { + argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType)); + continue; + } + + argTask = (Task)typeof(Service<>) + .MakeGenericType(argType) + .InvokeMember( + nameof(GetAsyncAsObject), + BindingFlags.InvokeMethod | + BindingFlags.Static | + BindingFlags.NonPublic, + null, + null, + null)!; + } + + return Task.WhenAll(argTasks); + } + + /// + /// Pull the instance out of the service locator, waiting if necessary. + /// + /// The object. + private static Task GetAsyncAsObject() => instanceTcs.Task.ContinueWith(r => (object)r.Result); + /// /// Exception thrown when service is attempted to be retrieved when it's unloaded. /// @@ -407,11 +415,12 @@ internal static class ServiceHelpers { /// /// Get a list of dependencies for a service. Only accepts types. - /// These are returned as types. + /// These are NOT returned as types; raw types will be returned. /// /// The dependencies for this service. + /// Whether to include the unload dependencies. /// A list of dependencies. - public static List GetDependencies(Type serviceType) + public static IReadOnlyCollection 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)serviceType.InvokeMember( + return (IReadOnlyCollection)serviceType.InvokeMember( nameof(Service.GetDependencyServices), BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, - null) ?? new List(); + new object?[] { includeUnloadDependencies }) ?? new List(); } /// diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 30441f479..70a91c4bf 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -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().ToDictionary(x => x, _ => (Task?)null); this.textureWraps = Enum.GetValues().ToDictionary(x => x, _ => (Task?)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() + .Where(x => x is not DalamudAsset.Empty4X4) + .Where(x => x.GetAttribute()?.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."); } /// @@ -83,25 +95,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA this.scopedFinalizer.Dispose(); } - /// - /// 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. - /// - /// The task. - [Pure] - public Task WaitForAllRequiredAssets() - { - lock (this.syncRoot) - { - return Task.WhenAll( - Enum.GetValues() - .Where(x => x is not DalamudAsset.Empty4X4) - .Where(x => x.GetAttribute()?.Required is true) - .Select(this.CreateStreamAsync) - .Select(x => x.ToContentDisposedTask())); - } - } - /// [Pure] public bool IsStreamImmediatelyAvailable(DalamudAsset asset) => diff --git a/Dalamud/Utility/ArrayExtensions.cs b/Dalamud/Utility/ArrayExtensions.cs index afb1511e3..fa6e3dbe9 100644 --- a/Dalamud/Utility/ArrayExtensions.cs +++ b/Dalamud/Utility/ArrayExtensions.cs @@ -87,4 +87,14 @@ internal static class ArrayExtensions result = default; return false; } + + /// + /// Interprets the given array as an , so that you can enumerate it multiple + /// times, and know the number of elements within. + /// + /// The enumerable. + /// The element type. + /// casted as a if it is one; otherwise the result of . + public static IReadOnlyCollection AsReadOnlyCollection(this IEnumerable array) => + array as IReadOnlyCollection ?? array.ToArray(); }