diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs
new file mode 100644
index 000000000..bf5ee75cf
--- /dev/null
+++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs
@@ -0,0 +1,63 @@
+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,
+
+ // // Events not implemented yet.
+ // ///
+ // /// Event that is fired right before an addon is set to shown.
+ // ///
+ // PreShow,
+ //
+ // ///
+ // /// Event that is fired after an addon has been shown.
+ // ///
+ // PostShow,
+ //
+ // ///
+ // /// Event that is fired right before an addon is set to hidden.
+ // ///
+ // PreHide,
+ //
+ // ///
+ // /// Event that is fired after an addon has been hidden.
+ // ///
+ // PostHide,
+ //
+ // ///
+ // /// 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,
+}
diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs
index 72d1c25ff..d915bbd00 100644
--- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs
@@ -1,4 +1,7 @@
using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
using Dalamud.Hooking;
using Dalamud.IoC;
@@ -14,49 +17,99 @@ 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 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);
}
-
+
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;
-
+
///
public void Dispose()
{
+ this.framework.Update -= this.OnFrameworkUpdate;
+
this.onAddonSetupHook.Dispose();
this.onAddonFinalizeHook.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();
}
+
+ private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.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)
{
@@ -65,7 +118,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl
try
{
- this.AddonPreSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon });
+ this.InvokeListeners(AddonEvent.PreSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon });
}
catch (Exception e)
{
@@ -76,7 +129,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl
try
{
- this.AddonPostSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon });
+ this.InvokeListeners(AddonEvent.PostSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon });
}
catch (Exception e)
{
@@ -96,7 +149,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl
try
{
- this.AddonPreFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] });
+ this.InvokeListeners(AddonEvent.PreFinalize, new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] });
}
catch (Exception e)
{
@@ -118,39 +171,93 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl
#pragma warning restore SA1015
internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle
{
+ private static readonly ModuleLog Log = new("AddonLifecycle:PluginScoped");
+
[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);
+ ///
+ public void RegisterListener(AddonEvent eventType, IEnumerable addonNames, IAddonLifecycle.AddonEventDelegate handler)
+ {
+ foreach (var addonName in addonNames)
+ {
+ this.RegisterListener(eventType, addonName, handler);
+ }
+ }
- private void AddonPostSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPostSetup?.Invoke(args);
+ ///
+ 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);
+ }
+ }
- private void AddonPreFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPreFinalize?.Invoke(args);
+ ///
+ public void UnregisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate? handler = null)
+ {
+ // This style is simpler to read imo. If the handler is null we want all entries,
+ // if they specified a handler then only the specific entries with that handler.
+ var targetListeners = this.eventListeners
+ .Where(entry => entry.EventType == eventType)
+ .Where(entry => entry.AddonName == addonName)
+ .Where(entry => handler is null || entry.FunctionDelegate == handler);
+
+ foreach (var listener in targetListeners)
+ {
+ this.addonLifecycleService.UnregisterListener(listener);
+ this.eventListeners.Remove(listener);
+ }
+ }
+
+ ///
+ public void UnregisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate? handler = null)
+ {
+ this.UnregisterListener(eventType, string.Empty, handler);
+ }
+
+ ///
+ public void UnregisterListener(IAddonLifecycle.AddonEventDelegate handler, params IAddonLifecycle.AddonEventDelegate[] handlers)
+ {
+ foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handler))
+ {
+ this.addonLifecycleService.UnregisterListener(listener);
+ this.eventListeners.Remove(listener);
+ }
+
+ foreach (var handlerParma in handlers)
+ {
+ foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handlerParma))
+ {
+ this.addonLifecycleService.UnregisterListener(listener);
+ this.eventListeners.Remove(listener);
+ }
+ }
+ }
}
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..0f8c681c2
--- /dev/null
+++ b/Dalamud/Hooking/Internal/CallHook.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Runtime.InteropServices;
+
+using Reloaded.Hooks.Definitions;
+
+namespace Dalamud.Hooking.Internal;
+
+///
+/// Hooking class for callsite hooking. This hook does not have capabilities of calling the original function.
+/// The intended use is replacing virtual function calls where you are able to manually invoke the original call using the delegate arguments.
+///
+/// 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/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs
index 43b9fef0a..1e318ae79 100644
--- a/Dalamud/Plugin/Services/IAddonLifecycle.cs
+++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs
@@ -1,5 +1,7 @@
-using System;
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+using Dalamud.Game.AddonLifecycle;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
@@ -11,19 +13,73 @@ 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);
+
+ ///
+ /// Register a listener that will trigger on the specified event for any addon.
+ ///
+ /// 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.
+ ///
+ /// Event handler to remove.
+ /// Additional handlers to remove.
+ void UnregisterListener(AddonEventDelegate handler, params AddonEventDelegate[] handlers);
///
/// Addon argument data for use in event subscribers.