From 9a0bc50e2396c450e8bec14589dd5be1d09008e9 Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Mon, 4 Nov 2024 23:21:07 +0900 Subject: [PATCH] Use hooks instead of lifecycle for NamePlateGui (#2060) --- Dalamud/Game/Gui/NamePlate/NamePlateGui.cs | 254 ++++++++++++++---- .../NamePlate/NamePlateGuiAddressResolver.cs | 20 ++ .../Gui/NamePlate/NamePlateUpdateContext.cs | 17 +- Dalamud/Plugin/Services/INamePlateGui.cs | 19 ++ 4 files changed, 244 insertions(+), 66 deletions(-) create mode 100644 Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs index 28e2c36eb..2def0ea00 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs @@ -1,14 +1,16 @@ using System.Collections.Generic; using System.Runtime.InteropServices; -using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.ClientState.Objects; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.GUI; + +using Serilog; namespace Dalamud.Game.Gui.NamePlate; @@ -38,38 +40,44 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui /// internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer(); - [ServiceManager.ServiceDependency] - private readonly AddonLifecycle addonLifecycle = Service.Get(); - [ServiceManager.ServiceDependency] private readonly GameGui gameGui = Service.Get(); [ServiceManager.ServiceDependency] private readonly ObjectTable objectTable = Service.Get(); - private readonly AddonLifecycleEventListener preRequestedUpdateListener; + private readonly NamePlateGuiAddressResolver address; + + private readonly Hook onRequestedUpdateHook; private NamePlateUpdateContext? context; private NamePlateUpdateHandler[] updateHandlers = []; [ServiceManager.ServiceConstructor] - private NamePlateGui() + private unsafe NamePlateGui(TargetSigScanner sigScanner) { - this.preRequestedUpdateListener = new AddonLifecycleEventListener( - AddonEvent.PreRequestedUpdate, - "NamePlate", - this.OnPreRequestedUpdate); + this.address = new NamePlateGuiAddressResolver(); + this.address.Setup(sigScanner); - this.addonLifecycle.RegisterListener(this.preRequestedUpdateListener); + this.onRequestedUpdateHook = Hook.FromAddress( + this.address.OnRequestedUpdate, + this.OnRequestedUpdateDetour); + this.onRequestedUpdateHook.Enable(); } /// public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate; + /// + public event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdate; + /// public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate; + /// + public event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdate; + /// public unsafe void RequestRedraw() { @@ -91,7 +99,7 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui /// void IInternalDisposableService.DisposeService() { - this.addonLifecycle.UnregisterListener(this.preRequestedUpdateListener); + this.onRequestedUpdateHook.Dispose(); } /// @@ -144,65 +152,124 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui this.updateHandlers = handlers.ToArray(); } - private void OnPreRequestedUpdate(AddonEvent type, AddonArgs args) + private unsafe void OnRequestedUpdateDetour( + AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null) - { - return; - } + var calledOriginal = false; - var reqArgs = (AddonRequestedUpdateArgs)args; - if (this.context == null) + try { - this.context = new NamePlateUpdateContext(this.objectTable, reqArgs); - this.CreateHandlers(this.context); - } - else - { - this.context.ResetState(reqArgs); - } - - var activeNamePlateCount = this.context.ActiveNamePlateCount; - if (activeNamePlateCount == 0) - return; - - var activeHandlers = this.updateHandlers[..activeNamePlateCount]; - - if (this.context.IsFullUpdate) - { - foreach (var handler in activeHandlers) + if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null && this.OnPostDataUpdate == null && + this.OnPostNamePlateUpdate == null) { - handler.ResetState(); + return; } - this.OnDataUpdate?.Invoke(this.context, activeHandlers); - this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers); - if (this.context.HasParts) - this.ApplyBuilders(activeHandlers); - } - else - { - var udpatedHandlers = new List(activeNamePlateCount); - foreach (var handler in activeHandlers) + if (this.context == null) { - handler.ResetState(); - if (handler.IsUpdating) - udpatedHandlers.Add(handler); + this.context = new NamePlateUpdateContext(this.objectTable); + this.CreateHandlers(this.context); } - if (this.OnDataUpdate is not null) + this.context.ResetState(addon, numberArrayData, stringArrayData); + + var activeNamePlateCount = this.context!.ActiveNamePlateCount; + if (activeNamePlateCount == 0) + return; + + var activeHandlers = this.updateHandlers[..activeNamePlateCount]; + + if (this.context.IsFullUpdate) { + foreach (var handler in activeHandlers) + { + handler.ResetState(); + } + this.OnDataUpdate?.Invoke(this.context, activeHandlers); - this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers); + this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers); + if (this.context.HasParts) this.ApplyBuilders(activeHandlers); + + try + { + calledOriginal = true; + this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate."); + } + + this.OnPostNamePlateUpdate?.Invoke(this.context, activeHandlers); + this.OnPostDataUpdate?.Invoke(this.context, activeHandlers); } - else if (udpatedHandlers.Count != 0) + else { - var changedHandlersSpan = udpatedHandlers.ToArray().AsSpan(); - this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers); - if (this.context.HasParts) - this.ApplyBuilders(changedHandlersSpan); + var updatedHandlers = new List(activeNamePlateCount); + foreach (var handler in activeHandlers) + { + handler.ResetState(); + if (handler.IsUpdating) + updatedHandlers.Add(handler); + } + + if (this.OnDataUpdate is not null) + { + this.OnDataUpdate?.Invoke(this.context, activeHandlers); + this.OnNamePlateUpdate?.Invoke(this.context, updatedHandlers); + + if (this.context.HasParts) + this.ApplyBuilders(activeHandlers); + + try + { + calledOriginal = true; + this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate."); + } + + this.OnPostNamePlateUpdate?.Invoke(this.context, updatedHandlers); + this.OnPostDataUpdate?.Invoke(this.context, activeHandlers); + } + else if (updatedHandlers.Count != 0) + { + this.OnNamePlateUpdate?.Invoke(this.context, updatedHandlers); + + if (this.context.HasParts) + this.ApplyBuilders(updatedHandlers); + + try + { + calledOriginal = true; + this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate."); + } + + this.OnPostNamePlateUpdate?.Invoke(this.context, updatedHandlers); + this.OnPostDataUpdate?.Invoke(this.context, activeHandlers); + } + } + } + finally + { + if (!calledOriginal) + { + try + { + this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate."); + } } } } @@ -217,6 +284,17 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui } } } + + private void ApplyBuilders(List handlers) + { + foreach (var handler in handlers) + { + if (handler.PartsContainer is { } container) + { + container.ApplyBuilders(handler); + } + } + } } /// @@ -239,6 +317,7 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate { if (this.OnNamePlateUpdateScoped == null) this.parentService.OnNamePlateUpdate += this.OnNamePlateUpdateForward; + this.OnNamePlateUpdateScoped += value; } @@ -250,6 +329,25 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate } } + /// + public event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdate + { + add + { + if (this.OnPostNamePlateUpdateScoped == null) + this.parentService.OnPostNamePlateUpdate += this.OnPostNamePlateUpdateForward; + + this.OnPostNamePlateUpdateScoped += value; + } + + remove + { + this.OnPostNamePlateUpdateScoped -= value; + if (this.OnPostNamePlateUpdateScoped == null) + this.parentService.OnPostNamePlateUpdate -= this.OnPostNamePlateUpdateForward; + } + } + /// public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate { @@ -257,6 +355,7 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate { if (this.OnDataUpdateScoped == null) this.parentService.OnDataUpdate += this.OnDataUpdateForward; + this.OnDataUpdateScoped += value; } @@ -268,10 +367,33 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate } } + /// + public event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdate + { + add + { + if (this.OnPostDataUpdateScoped == null) + this.parentService.OnPostDataUpdate += this.OnPostDataUpdateForward; + + this.OnPostDataUpdateScoped += value; + } + + remove + { + this.OnPostDataUpdateScoped -= value; + if (this.OnPostDataUpdateScoped == null) + this.parentService.OnPostDataUpdate -= this.OnPostDataUpdateForward; + } + } + private event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdateScoped; + private event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdateScoped; + private event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdateScoped; + private event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdateScoped; + /// public void RequestRedraw() { @@ -284,8 +406,14 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward; this.OnNamePlateUpdateScoped = null; + this.parentService.OnPostNamePlateUpdate -= this.OnPostNamePlateUpdateForward; + this.OnPostNamePlateUpdateScoped = null; + this.parentService.OnDataUpdate -= this.OnDataUpdateForward; this.OnDataUpdateScoped = null; + + this.parentService.OnPostDataUpdate -= this.OnPostDataUpdateForward; + this.OnPostDataUpdateScoped = null; } private void OnNamePlateUpdateForward( @@ -294,9 +422,21 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate this.OnNamePlateUpdateScoped?.Invoke(context, handlers); } + private void OnPostNamePlateUpdateForward( + INamePlateUpdateContext context, IReadOnlyList handlers) + { + this.OnPostNamePlateUpdateScoped?.Invoke(context, handlers); + } + private void OnDataUpdateForward( INamePlateUpdateContext context, IReadOnlyList handlers) { this.OnDataUpdateScoped?.Invoke(context, handlers); } + + private void OnPostDataUpdateForward( + INamePlateUpdateContext context, IReadOnlyList handlers) + { + this.OnPostDataUpdateScoped?.Invoke(context, handlers); + } } diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs new file mode 100644 index 000000000..450e1fa9f --- /dev/null +++ b/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs @@ -0,0 +1,20 @@ +namespace Dalamud.Game.Gui.NamePlate; + +/// +/// An address resolver for the class. +/// +internal class NamePlateGuiAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the AddonNamePlate OnRequestedUpdate method. We need to use a hook for this because + /// AddonNamePlate.Show calls OnRequestedUpdate directly, bypassing the AddonLifecycle callsite hook. + /// + public IntPtr OnRequestedUpdate { get; private set; } + + /// + protected override void Setup64Bit(ISigScanner sig) + { + this.OnRequestedUpdate = sig.ScanText( + "4C 8B DC 41 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 49 8B 40 20"); + } +} diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs index 2d5633bcb..876c4c2e0 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs @@ -1,4 +1,3 @@ -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.UI; @@ -54,13 +53,11 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext /// Initializes a new instance of the class. /// /// An object table. - /// The addon lifecycle arguments for the update request. - internal NamePlateUpdateContext(ObjectTable objectTable, AddonRequestedUpdateArgs args) + internal NamePlateUpdateContext(ObjectTable objectTable) { this.ObjectTable = objectTable; this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance(); this.Ui3DModule = UIModule.Instance()->GetUI3DModule(); - this.ResetState(args); } /// @@ -137,13 +134,15 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext /// /// Resets the state of the context based on the provided addon lifecycle arguments. /// - /// The addon lifecycle arguments for the update request. - internal void ResetState(AddonRequestedUpdateArgs args) + /// A pointer to the addon. + /// A pointer to the global number array data struct. + /// A pointer to the global string array data struct. + public void ResetState(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.Addon = (AddonNamePlate*)args.Addon; - this.NumberData = ((NumberArrayData**)args.NumberArrayData)![NamePlateGui.NumberArrayIndex]; + this.Addon = (AddonNamePlate*)addon; + this.NumberData = numberArrayData[NamePlateGui.NumberArrayIndex]; this.NumberStruct = (AddonNamePlate.AddonNamePlateNumberArray*)this.NumberData->IntArray; - this.StringData = ((StringArrayData**)args.StringArrayData)![NamePlateGui.StringArrayIndex]; + this.StringData = stringArrayData[NamePlateGui.StringArrayIndex]; this.HasParts = false; this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount; diff --git a/Dalamud/Plugin/Services/INamePlateGui.cs b/Dalamud/Plugin/Services/INamePlateGui.cs index 713d9120b..eb2579bae 100644 --- a/Dalamud/Plugin/Services/INamePlateGui.cs +++ b/Dalamud/Plugin/Services/INamePlateGui.cs @@ -26,6 +26,15 @@ public interface INamePlateGui /// event OnPlateUpdateDelegate? OnNamePlateUpdate; + /// + /// An event which fires after nameplate data is updated and at least one nameplate had important updates. The + /// subscriber is provided with a list of handlers for nameplates with important updates. + /// + /// + /// Fires before . + /// + event OnPlateUpdateDelegate? OnPostNamePlateUpdate; + /// /// An event which fires when nameplate data is updated. The subscriber is provided with a list of handlers for all /// nameplates. @@ -36,6 +45,16 @@ public interface INamePlateGui /// event OnPlateUpdateDelegate? OnDataUpdate; + /// + /// An event which fires after nameplate data is updated. The subscriber is provided with a list of handlers for all + /// nameplates. + /// + /// + /// This event is likely to fire every frame even when no nameplates are actually updated, so in most cases + /// is preferred. Fires after . + /// + event OnPlateUpdateDelegate? OnPostDataUpdate; + /// /// Requests that all nameplates should be redrawn on the following frame. ///