Less footguns in service dependency handling (#1560)

This commit is contained in:
srkizer 2023-12-07 14:29:46 +09:00 committed by GitHub
parent 5777745ab3
commit a0f4baf8fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 659 additions and 302 deletions

View file

@ -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>

View file

@ -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.

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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)

View file

@ -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();

View file

@ -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,

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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>();

View file

@ -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)
{

View file

@ -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;

View file

@ -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(() =>
{

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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");
}
}
}

View file

@ -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&lt;T&gt;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
{
}
}

View file

@ -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>

View file

@ -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) =>

View file

@ -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();
}