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.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
/// </summary>
internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer();
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
[ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get();
[ServiceManager.ServiceDependency]
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 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<AtkUnitBase.Delegates.OnRequestedUpdate>.FromAddress(
this.address.OnRequestedUpdate,
this.OnRequestedUpdateDetour);
this.onRequestedUpdateHook.Enable();
}
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate;
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdate;
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate;
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdate;
/// <inheritdoc/>
public unsafe void RequestRedraw()
{
@ -91,7 +99,7 @@ internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.addonLifecycle.UnregisterListener(this.preRequestedUpdateListener);
this.onRequestedUpdateHook.Dispose();
}
/// <summary>
@ -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<NamePlateUpdateHandler>(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<NamePlateUpdateHandler>(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<NamePlateUpdateHandler> handlers)
{
foreach (var handler in handlers)
{
if (handler.PartsContainer is { } container)
{
container.ApplyBuilders(handler);
}
}
}
}
/// <summary>
@ -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
}
}
/// <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/>
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
}
}
/// <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? OnPostNamePlateUpdateScoped;
private event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdateScoped;
private event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdateScoped;
/// <inheritdoc/>
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<INamePlateUpdateHandler> handlers)
{
this.OnPostNamePlateUpdateScoped?.Invoke(context, handlers);
}
private void OnDataUpdateForward(
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> 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 FFXIVClientStructs.FFXIV.Client.UI;
@ -54,13 +53,11 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
/// Initializes a new instance of the <see cref="NamePlateUpdateContext"/> class.
/// </summary>
/// <param name="objectTable">An object table.</param>
/// <param name="args">The addon lifecycle arguments for the update request.</param>
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);
}
/// <summary>
@ -137,13 +134,15 @@ internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
/// <summary>
/// Resets the state of the context based on the provided addon lifecycle arguments.
/// </summary>
/// <param name="args">The addon lifecycle arguments for the update request.</param>
internal void ResetState(AddonRequestedUpdateArgs args)
/// <param name="addon">A pointer to the addon.</param>
/// <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.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;

View file

@ -26,6 +26,15 @@ public interface INamePlateGui
/// </remarks>
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>
/// 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
/// </remarks>
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>
/// Requests that all nameplates should be redrawn on the following frame.
/// </summary>