diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs new file mode 100644 index 000000000..95cb2539c --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -0,0 +1,169 @@ +using System; +using System.Runtime.InteropServices; + +using Dalamud.Hooking; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonLifecycle; + +/// +/// This class provides events for in-game addon lifecycles. +/// +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycle +{ + private static readonly ModuleLog Log = new("AddonLifecycle"); + private readonly AddonLifecycleAddressResolver address; + private readonly Hook onAddonSetupHook; + private readonly Hook onAddonFinalizeHook; + + [ServiceManager.ServiceConstructor] + private AddonLifecycle(SigScanner sigScanner) + { + this.address = new AddonLifecycleAddressResolver(); + this.address.Setup(sigScanner); + + this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); + this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonFinalize); + } + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate nint AddonSetupDelegate(AtkUnitBase* addon); + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); + + /// + public event Action? AddonPreSetup; + + /// + public event Action? AddonPostSetup; + + /// + public event Action? AddonPreFinalize; + + /// + public event Action? AddonPostFinalize; + + /// + public void Dispose() + { + this.onAddonSetupHook.Dispose(); + this.onAddonFinalizeHook.Dispose(); + } + + [ServiceManager.CallWhenServicesReady] + private void ContinueConstruction() + { + this.onAddonSetupHook.Enable(); + this.onAddonFinalizeHook.Enable(); + } + + private nint OnAddonSetup(AtkUnitBase* addon) + { + try + { + this.AddonPreSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonSetup pre-setup invoke."); + } + + var result = this.onAddonSetupHook.Original(addon); + + try + { + this.AddonPostSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonSetup post-setup invoke."); + } + + return result; + } + + private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) + { + try + { + this.AddonPreFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonFinalize pre-finalize invoke."); + } + + this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); + + try + { + this.AddonPostFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonFinalize post-finalize invoke."); + } + } +} + +/// +/// Plugin-scoped version of a AddonLifecycle service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle +{ + [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; + this.addonLifecycleService.AddonPostFinalize += this.AddonPostFinalizeForward; + } + + /// + public event Action? AddonPreSetup; + + /// + public event Action? AddonPostSetup; + + /// + public event Action? AddonPreFinalize; + + /// + public event Action? AddonPostFinalize; + + /// + public void Dispose() + { + this.addonLifecycleService.AddonPreSetup -= this.AddonPreSetupForward; + this.addonLifecycleService.AddonPostSetup -= this.AddonPostSetupForward; + this.addonLifecycleService.AddonPreFinalize -= this.AddonPreFinalizeForward; + this.addonLifecycleService.AddonPostFinalize -= this.AddonPostFinalizeForward; + } + + 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); + + private void AddonPostFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPostFinalize?.Invoke(args); +} diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs new file mode 100644 index 000000000..ba7b723ec --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Game.AddonLifecycle; + +/// +/// AddonLifecycleService memory address resolver. +/// +internal class AddonLifecycleAddressResolver : BaseAddressResolver +{ + /// + /// 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. + /// + public nint AddonFinalize { get; private set; } + + /// + /// Scan for and setup any configured address pointers. + /// + /// The signature scanner to facilitate setup. + protected override void Setup64Bit(SigScanner sig) + { + this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14"); + this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); + } +} diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs new file mode 100644 index 000000000..7b90cf0cd --- /dev/null +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -0,0 +1,50 @@ +using System; + +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Plugin.Services; + +/// +/// This class provides events for in-game addon lifecycles. +/// +public interface IAddonLifecycle +{ + /// + /// Event that fires before an addon is being setup. + /// + public event Action AddonPreSetup; + + /// + /// Event that fires after an addon is done being setup. + /// + public event Action AddonPostSetup; + + /// + /// Event that fires before an addon is being finalized. + /// + public event Action AddonPreFinalize; + + /// + /// Event that fires after an addon is done being finalized. + /// + public event Action AddonPostFinalize; + + /// + /// 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.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + required public nint Addon { get; init; } + } +}