diff --git a/Dalamud/Game/AddonLifecycle/AddonArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgs.cs new file mode 100644 index 000000000..50c995abb --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgs.cs @@ -0,0 +1,22 @@ +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonLifecycle; + +/// +/// Addon argument data for use in event subscribers. +/// +public unsafe class AddonArgs +{ + private string? addonName; + + /// + /// Gets the name of the addon this args referrers to. + /// + public string AddonName => this.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + required public nint Addon { get; init; } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs new file mode 100644 index 000000000..faef30c88 --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -0,0 +1,62 @@ +namespace Dalamud.Game.AddonLifecycle; + +/// +/// Enumeration for available AddonLifecycle events. +/// +public enum AddonEvent +{ + /// + /// Event that is fired before an addon begins it's setup process. + /// + PreSetup, + + /// + /// Event that is fired after an addon has completed it's setup process. + /// + PostSetup, + + /// + /// Event that is fired before an addon begins update. + /// + PreUpdate, + + /// + /// Event that is fired after an addon has completed update. + /// + PostUpdate, + + /// + /// Event that is fired before an addon begins draw. + /// + PreDraw, + + /// + /// Event that is fired after an addon has completed draw. + /// + PostDraw, + + /// + /// Event that is fired before an addon is finalized. + /// + PreFinalize, + + /// + /// Event that is fired before an addon begins a requested update. + /// + PreRequestedUpdate, + + /// + /// Event that is fired after an addon finishes a requested update. + /// + PostRequestedUpdate, + + /// + /// Event that is fired before an addon begins a refresh. + /// + PreRefresh, + + /// + /// Event that is fired after an addon has finished a refresh. + /// + PostRefresh, +} diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 72d1c25ff..5fc1c7d2b 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -1,6 +1,10 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using Dalamud.Hooking; +using Dalamud.Hooking.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -14,58 +18,129 @@ namespace Dalamud.Game.AddonLifecycle; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycle +internal unsafe class AddonLifecycle : IDisposable, IServiceType { private static readonly ModuleLog Log = new("AddonLifecycle"); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private readonly AddonLifecycleAddressResolver address; private readonly Hook onAddonSetupHook; private readonly Hook onAddonFinalizeHook; - + private readonly CallHook onAddonDrawHook; + private readonly CallHook onAddonUpdateHook; + private readonly Hook onAddonRefreshHook; + private readonly CallHook onAddonRequestedUpdateHook; + + private readonly ConcurrentBag newEventListeners = new(); + private readonly ConcurrentBag removeEventListeners = new(); + private readonly List eventListeners = new(); + [ServiceManager.ServiceConstructor] private AddonLifecycle(SigScanner sigScanner) { this.address = new AddonLifecycleAddressResolver(); this.address.Setup(sigScanner); + this.framework.Update += this.OnFrameworkUpdate; + this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); + this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); + 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); } private delegate nint AddonSetupDelegate(AtkUnitBase* addon); private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); - - /// - public event Action? AddonPreSetup; - - /// - public event Action? AddonPostSetup; - - /// - public event Action? AddonPreFinalize; - + + private delegate void AddonDrawDelegate(AtkUnitBase* addon); + + private delegate void AddonUpdateDelegate(AtkUnitBase* addon, float delta); + + private delegate void AddonOnRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); + + private delegate void AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values); + /// public void Dispose() { + this.framework.Update -= this.OnFrameworkUpdate; + this.onAddonSetupHook.Dispose(); this.onAddonFinalizeHook.Dispose(); + this.onAddonDrawHook.Dispose(); + this.onAddonUpdateHook.Dispose(); + this.onAddonRefreshHook.Dispose(); + this.onAddonRequestedUpdateHook.Dispose(); } - + + /// + /// Register a listener for the target event and addon. + /// + /// The listener to register. + internal void RegisterListener(AddonLifecycleEventListener listener) + { + this.newEventListeners.Add(listener); + } + + /// + /// Unregisters the listener from events. + /// + /// The listener to unregister. + internal void UnregisterListener(AddonLifecycleEventListener listener) + { + this.removeEventListeners.Add(listener); + } + + // Used to prevent concurrency issues if plugins try to register during iteration of listeners. + private void OnFrameworkUpdate(Framework unused) + { + if (this.newEventListeners.Any()) + { + this.eventListeners.AddRange(this.newEventListeners); + this.newEventListeners.Clear(); + } + + if (this.removeEventListeners.Any()) + { + foreach (var toRemoveListener in this.removeEventListeners) + { + this.eventListeners.Remove(toRemoveListener); + } + + this.removeEventListeners.Clear(); + } + } + [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { this.onAddonSetupHook.Enable(); this.onAddonFinalizeHook.Enable(); + this.onAddonDrawHook.Enable(); + this.onAddonUpdateHook.Enable(); + this.onAddonRefreshHook.Enable(); + this.onAddonRequestedUpdateHook.Enable(); + } + + private void InvokeListeners(AddonEvent eventType, AddonArgs args) + { + // Match on string.empty for listeners that want events for all addons. + foreach (var listener in this.eventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) + { + listener.FunctionDelegate.Invoke(eventType, args); + } } private nint OnAddonSetup(AtkUnitBase* addon) { - if (addon is null) - return this.onAddonSetupHook.Original(addon); - try { - this.AddonPreSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreSetup, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -76,7 +151,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl try { - this.AddonPostSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostSetup, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -88,15 +163,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { - if (atkUnitBase is null || atkUnitBase[0] is null) - { - this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); - return; - } - try { - this.AddonPreFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + this.InvokeListeners(AddonEvent.PreFinalize, new AddonArgs { Addon = (nint)atkUnitBase[0] }); } catch (Exception e) { @@ -105,6 +174,98 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); } + + private void OnAddonDraw(AtkUnitBase* addon) + { + try + { + this.InvokeListeners(AddonEvent.PreDraw, new AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); + } + + addon->Draw(); + + try + { + this.InvokeListeners(AddonEvent.PostDraw, new AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonDraw post-draw invoke."); + } + } + + private void OnAddonUpdate(AtkUnitBase* addon, float delta) + { + try + { + this.InvokeListeners(AddonEvent.PreUpdate, new AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonUpdate pre-update invoke."); + } + + addon->Update(delta); + + try + { + this.InvokeListeners(AddonEvent.PostUpdate, new AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonUpdate post-update invoke."); + } + } + + private void OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + try + { + this.InvokeListeners(AddonEvent.PreRefresh, new AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke."); + } + + this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values); + + try + { + this.InvokeListeners(AddonEvent.PostRefresh, new AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke."); + } + } + + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + try + { + this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke."); + } + + addon->OnUpdate(numberArrayData, stringArrayData); + + try + { + this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke."); + } + } } /// @@ -121,36 +282,81 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); - /// - /// Initializes a new instance of the class. - /// - public AddonLifecyclePluginScoped() - { - this.addonLifecycleService.AddonPreSetup += this.AddonPreSetupForward; - this.addonLifecycleService.AddonPostSetup += this.AddonPostSetupForward; - this.addonLifecycleService.AddonPreFinalize += this.AddonPreFinalizeForward; - } - - /// - public event Action? AddonPreSetup; - - /// - public event Action? AddonPostSetup; - - /// - public event Action? AddonPreFinalize; - + private readonly List eventListeners = new(); + /// public void Dispose() { - this.addonLifecycleService.AddonPreSetup -= this.AddonPreSetupForward; - this.addonLifecycleService.AddonPostSetup -= this.AddonPostSetupForward; - this.addonLifecycleService.AddonPreFinalize -= this.AddonPreFinalizeForward; + foreach (var listener in this.eventListeners) + { + this.addonLifecycleService.UnregisterListener(listener); + } } - private void AddonPreSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPreSetup?.Invoke(args); - - private void AddonPostSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPostSetup?.Invoke(args); - - private void AddonPreFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPreFinalize?.Invoke(args); + /// + public void RegisterListener(AddonEvent eventType, IEnumerable addonNames, IAddonLifecycle.AddonEventDelegate handler) + { + foreach (var addonName in addonNames) + { + this.RegisterListener(eventType, addonName, handler); + } + } + + /// + public void RegisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate handler) + { + var listener = new AddonLifecycleEventListener(eventType, addonName, handler); + this.eventListeners.Add(listener); + this.addonLifecycleService.RegisterListener(listener); + } + + /// + public void RegisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate handler) + { + this.RegisterListener(eventType, string.Empty, handler); + } + + /// + public void UnregisterListener(AddonEvent eventType, IEnumerable addonNames, IAddonLifecycle.AddonEventDelegate? handler = null) + { + foreach (var addonName in addonNames) + { + this.UnregisterListener(eventType, addonName, handler); + } + } + + /// + public void UnregisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate? handler = null) + { + this.eventListeners.RemoveAll(entry => + { + if (entry.EventType != eventType) return false; + if (entry.AddonName != addonName) return false; + if (handler is not null && entry.FunctionDelegate != handler) return false; + + this.addonLifecycleService.UnregisterListener(entry); + return true; + }); + } + + /// + public void UnregisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate? handler = null) + { + this.UnregisterListener(eventType, string.Empty, handler); + } + + /// + public void UnregisterListener(params IAddonLifecycle.AddonEventDelegate[] handlers) + { + foreach (var handler in handlers) + { + this.eventListeners.RemoveAll(entry => + { + if (entry.FunctionDelegate != handler) return false; + + this.addonLifecycleService.UnregisterListener(entry); + return true; + }); + } + } } diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index ba7b723ec..079e09c80 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -6,14 +6,34 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// - /// Gets the address of the addon setup hook invoked by the atkunitmanager. + /// Gets the address of the addon setup hook invoked by the AtkUnitManager. /// public nint AddonSetup { get; private set; } /// - /// Gets the address of the addon finalize hook invoked by the atkunitmanager. + /// Gets the address of the addon finalize hook invoked by the AtkUnitManager. /// public nint AddonFinalize { get; private set; } + + /// + /// Gets the address of the addon draw hook invoked by virtual function call. + /// + public nint AddonDraw { get; private set; } + + /// + /// Gets the address of the addon update hook invoked by virtual function call. + /// + public nint AddonUpdate { get; private set; } + + /// + /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call. + /// + public nint AddonOnRequestedUpdate { get; private set; } + + /// + /// Gets the address of AtkUnitManager_vf10 which triggers addon onRefresh. + /// + public nint AddonOnRefresh { get; private set; } /// /// Scan for and setup any configured address pointers. @@ -23,5 +43,9 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver { this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); + this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); + this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); + this.AddonOnRequestedUpdate = sig.ScanText("FF 90 90 01 00 00 48 8B 5C 24 30 48 83 C4 20"); + this.AddonOnRefresh = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 41 8B F8 48 8B DA"); } } diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs new file mode 100644 index 000000000..0f088362d --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs @@ -0,0 +1,38 @@ +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.AddonLifecycle; + +/// +/// This class is a helper for tracking and invoking listener delegates. +/// +internal class AddonLifecycleEventListener +{ + /// + /// Initializes a new instance of the class. + /// + /// Event type to listen for. + /// Addon name to listen for. + /// Delegate to invoke. + internal AddonLifecycleEventListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate functionDelegate) + { + this.EventType = eventType; + this.AddonName = addonName; + this.FunctionDelegate = functionDelegate; + } + + /// + /// Gets the name of the addon this listener is looking for. + /// string.Empty if it wants to be called for any addon. + /// + public string AddonName { get; init; } + + /// + /// Gets the event type this listener is looking for. + /// + public AddonEvent EventType { get; init; } + + /// + /// Gets the delegate this listener invokes. + /// + public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; } +} diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs new file mode 100644 index 000000000..2bef59c86 --- /dev/null +++ b/Dalamud/Hooking/Internal/CallHook.cs @@ -0,0 +1,89 @@ +using System; +using System.Runtime.InteropServices; + +using Reloaded.Hooks.Definitions; + +namespace Dalamud.Hooking.Internal; + +/// +/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook. +/// This is a destructive operation, no other callsite hooks can coexist at the same address. +/// +/// There's no .Original for this hook type. +/// This is only intended for be for functions where the parameters provided allow you to invoke the original call. +/// +/// This class was specifically added for hooking virtual function callsites. +/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered. +/// +/// Delegate signature for this hook. +internal class CallHook : IDisposable where T : Delegate +{ + private readonly Reloaded.Hooks.AsmHook asmHook; + + private T? detour; + private bool activated; + + /// + /// Initializes a new instance of the class. + /// + /// Address of the instruction to replace. + /// Delegate to invoke. + internal CallHook(nint address, T detour) + { + this.detour = detour; + + var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); + var code = new[] + { + "use64", + $"mov rax, 0x{detourPtr:X8}", + "call rax", + }; + + var opt = new AsmHookOptions + { + PreferRelativeJump = true, + Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, + MaxOpcodeSize = 5, + }; + + this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); + } + + /// + /// Gets a value indicating whether or not the hook is enabled. + /// + public bool IsEnabled => this.asmHook.IsEnabled; + + /// + /// Starts intercepting a call to the function. + /// + public void Enable() + { + if (!this.activated) + { + this.activated = true; + this.asmHook.Activate(); + return; + } + + this.asmHook.Enable(); + } + + /// + /// Stops intercepting a call to the function. + /// + public void Disable() + { + this.asmHook.Disable(); + } + + /// + /// Remove a hook from the current process. + /// + public void Dispose() + { + this.asmHook.Disable(); + this.detour = null; + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs new file mode 100644 index 000000000..3a1cb0e77 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs @@ -0,0 +1,132 @@ +using System.Collections.Generic; + +using Dalamud.Game.AddonLifecycle; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; + +/// +/// Test setup AddonLifecycle Service. +/// +internal class AddonLifecycleAgingStep : IAgingStep +{ + private readonly List listeners; + + private AddonLifecycle? service; + private TestStep currentStep = TestStep.CharacterRefresh; + private bool listenersRegistered; + + /// + /// Initializes a new instance of the class. + /// + public AddonLifecycleAgingStep() + { + this.listeners = new List + { + new(AddonEvent.PostSetup, "Character", this.PostSetup), + new(AddonEvent.PostUpdate, "Character", this.PostUpdate), + new(AddonEvent.PostDraw, "Character", this.PostDraw), + new(AddonEvent.PostRefresh, "Character", this.PostRefresh), + new(AddonEvent.PostRequestedUpdate, "Character", this.PostRequestedUpdate), + new(AddonEvent.PreFinalize, "Character", this.PreFinalize), + }; + } + + private enum TestStep + { + CharacterRefresh, + CharacterSetup, + CharacterRequestedUpdate, + CharacterUpdate, + CharacterDraw, + CharacterFinalize, + Complete, + } + + /// + public string Name => "Test AddonLifecycle"; + + /// + public SelfTestStepResult RunStep() + { + this.service ??= Service.Get(); + if (this.service is null) return SelfTestStepResult.Fail; + + if (!this.listenersRegistered) + { + foreach (var listener in this.listeners) + { + this.service.RegisterListener(listener); + } + + this.listenersRegistered = true; + } + + switch (this.currentStep) + { + case TestStep.CharacterRefresh: + ImGui.Text("Open Character Window."); + break; + + case TestStep.CharacterSetup: + ImGui.Text("Open Character Window."); + break; + + case TestStep.CharacterRequestedUpdate: + ImGui.Text("Change tabs, or un-equip/equip gear."); + break; + + case TestStep.CharacterFinalize: + ImGui.Text("Close Character Window."); + break; + + case TestStep.CharacterUpdate: + case TestStep.CharacterDraw: + case TestStep.Complete: + default: + // Nothing to report to tester. + break; + } + + return this.currentStep is TestStep.Complete ? SelfTestStepResult.Pass : SelfTestStepResult.Waiting; + } + + /// + public void CleanUp() + { + foreach (var listener in this.listeners) + { + this.service?.UnregisterListener(listener); + } + } + + private void PostSetup(AddonEvent eventType, AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterSetup) this.currentStep++; + } + + private void PostUpdate(AddonEvent eventType, AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterUpdate) this.currentStep++; + } + + private void PostDraw(AddonEvent eventType, AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterDraw) this.currentStep++; + } + + private void PostRefresh(AddonEvent eventType, AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterRefresh) this.currentStep++; + } + + private void PostRequestedUpdate(AddonEvent eventType, AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterRequestedUpdate) this.currentStep++; + } + + private void PreFinalize(AddonEvent eventType, AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterFinalize) this.currentStep++; + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index 4a7bb0413..8e43d30a6 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -40,6 +40,7 @@ internal class SelfTestWindow : Window new ChatAgingStep(), new HoverAgingStep(), new LuminaAgingStep(), + new AddonLifecycleAgingStep(), new PartyFinderAgingStep(), new HandledExceptionAgingStep(), new DutyStateAgingStep(), diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 43b9fef0a..1dc792660 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -1,7 +1,7 @@ -using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; -using Dalamud.Memory; -using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Game.AddonLifecycle; namespace Dalamud.Plugin.Services; @@ -11,35 +11,70 @@ namespace Dalamud.Plugin.Services; public interface IAddonLifecycle { /// - /// Event that fires before an addon is being setup. + /// Delegate for receiving addon lifecycle event messages. /// - public event Action AddonPreSetup; + /// The event type that triggered the message. + /// Information about what addon triggered the message. + public delegate void AddonEventDelegate(AddonEvent eventType, AddonArgs addonInfo); /// - /// Event that fires after an addon is done being setup. + /// Register a listener that will trigger on the specified event and any of the specified addons. /// - public event Action AddonPostSetup; + /// Event type to trigger on. + /// Addon names that will trigger the handler to be invoked. + /// The handler to invoke. + void RegisterListener(AddonEvent eventType, IEnumerable addonNames, AddonEventDelegate handler); /// - /// Event that fires before an addon is being finalized. + /// Register a listener that will trigger on the specified event only for the specified addon. /// - public event Action AddonPreFinalize; + /// Event type to trigger on. + /// The addon name that will trigger the handler to be invoked. + /// The handler to invoke. + void RegisterListener(AddonEvent eventType, string addonName, AddonEventDelegate handler); /// - /// Addon argument data for use in event subscribers. + /// Register a listener that will trigger on the specified event for any addon. /// - public unsafe class AddonArgs - { - private string? addonName; - - /// - /// Gets the name of the addon this args referrers to. - /// - public string AddonName => this.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); - - /// - /// Gets the pointer to the addons AtkUnitBase. - /// - required public nint Addon { get; init; } - } + /// Event type to trigger on. + /// The handler to invoke. + void RegisterListener(AddonEvent eventType, AddonEventDelegate handler); + + /// + /// Unregister listener from specified event type and specified addon names. + /// + /// + /// If a specific handler is not provided, all handlers for the event type and addon names will be unregistered. + /// + /// Event type to deregister. + /// Addon names to deregister. + /// Optional specific handler to remove. + void UnregisterListener(AddonEvent eventType, IEnumerable addonNames, [Optional] AddonEventDelegate handler); + + /// + /// Unregister all listeners for the specified event type and addon name. + /// + /// + /// If a specific handler is not provided, all handlers for the event type and addons will be unregistered. + /// + /// Event type to deregister. + /// Addon name to deregister. + /// Optional specific handler to remove. + void UnregisterListener(AddonEvent eventType, string addonName, [Optional] AddonEventDelegate handler); + + /// + /// Unregister an event type handler.
This will only remove a handler that is added via . + ///
+ /// + /// If a specific handler is not provided, all handlers for the event type and addons will be unregistered. + /// + /// Event type to deregister. + /// Optional specific handler to remove. + void UnregisterListener(AddonEvent eventType, [Optional] AddonEventDelegate handler); + + /// + /// Unregister all events that use the specified handlers. + /// + /// Handlers to remove. + void UnregisterListener(params AddonEventDelegate[] handlers); }