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