Use hooks instead of lifecycle for NamePlateGui (#2060)

This commit is contained in:
nebel 2024-11-04 23:21:07 +09:00 committed by GitHub
parent f3bd83fbe9
commit 9a0bc50e23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 244 additions and 66 deletions

View file

@ -1,14 +1,16 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog;
namespace Dalamud.Game.Gui.NamePlate; namespace Dalamud.Game.Gui.NamePlate;
@ -38,38 +40,44 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
/// </summary> /// </summary>
internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer(); internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer();
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get(); private readonly GameGui gameGui = Service<GameGui>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get(); private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
private readonly AddonLifecycleEventListener preRequestedUpdateListener; private readonly NamePlateGuiAddressResolver address;
private readonly Hook<AtkUnitBase.Delegates.OnRequestedUpdate> onRequestedUpdateHook;
private NamePlateUpdateContext? context; private NamePlateUpdateContext? context;
private NamePlateUpdateHandler[] updateHandlers = []; private NamePlateUpdateHandler[] updateHandlers = [];
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private NamePlateGui() private unsafe NamePlateGui(TargetSigScanner sigScanner)
{ {
this.preRequestedUpdateListener = new AddonLifecycleEventListener( this.address = new NamePlateGuiAddressResolver();
AddonEvent.PreRequestedUpdate, this.address.Setup(sigScanner);
"NamePlate",
this.OnPreRequestedUpdate);
this.addonLifecycle.RegisterListener(this.preRequestedUpdateListener); this.onRequestedUpdateHook = Hook<AtkUnitBase.Delegates.OnRequestedUpdate>.FromAddress(
this.address.OnRequestedUpdate,
this.OnRequestedUpdateDetour);
this.onRequestedUpdateHook.Enable();
} }
/// <inheritdoc/> /// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate; public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate;
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdate;
/// <inheritdoc/> /// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate; public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate;
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdate;
/// <inheritdoc/> /// <inheritdoc/>
public unsafe void RequestRedraw() public unsafe void RequestRedraw()
{ {
@ -91,7 +99,7 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.addonLifecycle.UnregisterListener(this.preRequestedUpdateListener); this.onRequestedUpdateHook.Dispose();
} }
/// <summary> /// <summary>
@ -144,65 +152,124 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
this.updateHandlers = handlers.ToArray(); 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) var calledOriginal = false;
{
return;
}
var reqArgs = (AddonRequestedUpdateArgs)args; try
if (this.context == null)
{ {
this.context = new NamePlateUpdateContext(this.objectTable, reqArgs); if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null && this.OnPostDataUpdate == null &&
this.CreateHandlers(this.context); this.OnPostNamePlateUpdate == null)
}
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)
{ {
handler.ResetState(); return;
} }
this.OnDataUpdate?.Invoke(this.context, activeHandlers); if (this.context == null)
this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers);
if (this.context.HasParts)
this.ApplyBuilders(activeHandlers);
}
else
{
var udpatedHandlers = new List<NamePlateUpdateHandler>(activeNamePlateCount);
foreach (var handler in activeHandlers)
{ {
handler.ResetState(); this.context = new NamePlateUpdateContext(this.objectTable);
if (handler.IsUpdating) this.CreateHandlers(this.context);
udpatedHandlers.Add(handler);
} }
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.OnDataUpdate?.Invoke(this.context, activeHandlers);
this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers); this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers);
if (this.context.HasParts) if (this.context.HasParts)
this.ApplyBuilders(activeHandlers); 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(); var updatedHandlers = new List<NamePlateUpdateHandler>(activeNamePlateCount);
this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers); foreach (var handler in activeHandlers)
if (this.context.HasParts) {
this.ApplyBuilders(changedHandlersSpan); 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<NamePlateUpdateHandler> handlers)
{
foreach (var handler in handlers)
{
if (handler.PartsContainer is { } container)
{
container.ApplyBuilders(handler);
}
}
}
} }
/// <summary> /// <summary>
@ -239,6 +317,7 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
{ {
if (this.OnNamePlateUpdateScoped == null) if (this.OnNamePlateUpdateScoped == null)
this.parentService.OnNamePlateUpdate += this.OnNamePlateUpdateForward; this.parentService.OnNamePlateUpdate += this.OnNamePlateUpdateForward;
this.OnNamePlateUpdateScoped += value; this.OnNamePlateUpdateScoped += value;
} }
@ -250,6 +329,25 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
} }
} }
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate
{ {
@ -257,6 +355,7 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
{ {
if (this.OnDataUpdateScoped == null) if (this.OnDataUpdateScoped == null)
this.parentService.OnDataUpdate += this.OnDataUpdateForward; this.parentService.OnDataUpdate += this.OnDataUpdateForward;
this.OnDataUpdateScoped += value; this.OnDataUpdateScoped += value;
} }
@ -268,10 +367,33 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
} }
} }
/// <inheritdoc/>
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? OnNamePlateUpdateScoped;
private event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdateScoped;
private event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdateScoped; private event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdateScoped;
private event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdateScoped;
/// <inheritdoc/> /// <inheritdoc/>
public void RequestRedraw() public void RequestRedraw()
{ {
@ -284,8 +406,14 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward; this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward;
this.OnNamePlateUpdateScoped = null; this.OnNamePlateUpdateScoped = null;
this.parentService.OnPostNamePlateUpdate -= this.OnPostNamePlateUpdateForward;
this.OnPostNamePlateUpdateScoped = null;
this.parentService.OnDataUpdate -= this.OnDataUpdateForward; this.parentService.OnDataUpdate -= this.OnDataUpdateForward;
this.OnDataUpdateScoped = null; this.OnDataUpdateScoped = null;
this.parentService.OnPostDataUpdate -= this.OnPostDataUpdateForward;
this.OnPostDataUpdateScoped = null;
} }
private void OnNamePlateUpdateForward( private void OnNamePlateUpdateForward(
@ -294,9 +422,21 @@ internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlate
this.OnNamePlateUpdateScoped?.Invoke(context, handlers); this.OnNamePlateUpdateScoped?.Invoke(context, handlers);
} }
private void OnPostNamePlateUpdateForward(
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
{
this.OnPostNamePlateUpdateScoped?.Invoke(context, handlers);
}
private void OnDataUpdateForward( private void OnDataUpdateForward(
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers) INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
{ {
this.OnDataUpdateScoped?.Invoke(context, handlers); this.OnDataUpdateScoped?.Invoke(context, handlers);
} }
private void OnPostDataUpdateForward(
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
{
this.OnPostDataUpdateScoped?.Invoke(context, handlers);
}
} }

View file

@ -0,0 +1,20 @@
namespace Dalamud.Game.Gui.NamePlate;
/// <summary>
/// An address resolver for the <see cref="NamePlateGui"/> class.
/// </summary>
internal class NamePlateGuiAddressResolver : BaseAddressResolver
{
/// <summary>
/// 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.
/// </summary>
public IntPtr OnRequestedUpdate { get; private set; }
/// <inheritdoc/>
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");
}
}

View file

@ -1,4 +1,3 @@
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
@ -54,13 +53,11 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
/// Initializes a new instance of the <see cref="NamePlateUpdateContext"/> class. /// Initializes a new instance of the <see cref="NamePlateUpdateContext"/> class.
/// </summary> /// </summary>
/// <param name="objectTable">An object table.</param> /// <param name="objectTable">An object table.</param>
/// <param name="args">The addon lifecycle arguments for the update request.</param> internal NamePlateUpdateContext(ObjectTable objectTable)
internal NamePlateUpdateContext(ObjectTable objectTable, AddonRequestedUpdateArgs args)
{ {
this.ObjectTable = objectTable; this.ObjectTable = objectTable;
this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance(); this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance();
this.Ui3DModule = UIModule.Instance()->GetUI3DModule(); this.Ui3DModule = UIModule.Instance()->GetUI3DModule();
this.ResetState(args);
} }
/// <summary> /// <summary>
@ -137,13 +134,15 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
/// <summary> /// <summary>
/// Resets the state of the context based on the provided addon lifecycle arguments. /// Resets the state of the context based on the provided addon lifecycle arguments.
/// </summary> /// </summary>
/// <param name="args">The addon lifecycle arguments for the update request.</param> /// <param name="addon">A pointer to the addon.</param>
internal void ResetState(AddonRequestedUpdateArgs args) /// <param name="numberArrayData">A pointer to the global number array data struct.</param>
/// <param name="stringArrayData">A pointer to the global string array data struct.</param>
public void ResetState(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{ {
this.Addon = (AddonNamePlate*)args.Addon; this.Addon = (AddonNamePlate*)addon;
this.NumberData = ((NumberArrayData**)args.NumberArrayData)![NamePlateGui.NumberArrayIndex]; this.NumberData = numberArrayData[NamePlateGui.NumberArrayIndex];
this.NumberStruct = (AddonNamePlate.AddonNamePlateNumberArray*)this.NumberData->IntArray; this.NumberStruct = (AddonNamePlate.AddonNamePlateNumberArray*)this.NumberData->IntArray;
this.StringData = ((StringArrayData**)args.StringArrayData)![NamePlateGui.StringArrayIndex]; this.StringData = stringArrayData[NamePlateGui.StringArrayIndex];
this.HasParts = false; this.HasParts = false;
this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount; this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount;

View file

@ -26,6 +26,15 @@ public interface INamePlateGui
/// </remarks> /// </remarks>
event OnPlateUpdateDelegate? OnNamePlateUpdate; event OnPlateUpdateDelegate? OnNamePlateUpdate;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Fires before <see cref="OnPostDataUpdate"/>.
/// </remarks>
event OnPlateUpdateDelegate? OnPostNamePlateUpdate;
/// <summary> /// <summary>
/// An event which fires when nameplate data is updated. The subscriber is provided with a list of handlers for all /// An event which fires when nameplate data is updated. The subscriber is provided with a list of handlers for all
/// nameplates. /// nameplates.
@ -36,6 +45,16 @@ public interface INamePlateGui
/// </remarks> /// </remarks>
event OnPlateUpdateDelegate? OnDataUpdate; event OnPlateUpdateDelegate? OnDataUpdate;
/// <summary>
/// An event which fires after nameplate data is updated. The subscriber is provided with a list of handlers for all
/// nameplates.
/// </summary>
/// <remarks>
/// This event is likely to fire every frame even when no nameplates are actually updated, so in most cases
/// <see cref="OnNamePlateUpdate"/> is preferred. Fires after <see cref="OnPostNamePlateUpdate"/>.
/// </remarks>
event OnPlateUpdateDelegate? OnPostDataUpdate;
/// <summary> /// <summary>
/// Requests that all nameplates should be redrawn on the following frame. /// Requests that all nameplates should be redrawn on the following frame.
/// </summary> /// </summary>