From 4dabd0713171613d1fef4ece7015dd2879decc76 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:08:04 -0700 Subject: [PATCH] Prototype, untested --- Dalamud/Game/AddonLifecycle/AddonEvent.cs | 63 ++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 185 ++++++++++++++---- .../AddonLifecycleEventListener.cs | 38 ++++ Dalamud/Hooking/Internal/CallHook.cs | 83 ++++++++ Dalamud/Plugin/Services/IAddonLifecycle.cs | 70 ++++++- 5 files changed, 393 insertions(+), 46 deletions(-) create mode 100644 Dalamud/Game/AddonLifecycle/AddonEvent.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs create mode 100644 Dalamud/Hooking/Internal/CallHook.cs 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.