Refactor Addon Lifecycle

This commit is contained in:
MidoriKami 2025-11-25 17:27:48 -08:00
parent bd427d7b54
commit 9a1fae8246
19 changed files with 543 additions and 825 deletions

View file

@ -1,107 +0,0 @@
using System.Runtime.CompilerServices;
using System.Threading;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
namespace Dalamud.Game.Addon;
/// <summary>Argument pool for Addon Lifecycle services.</summary>
[ServiceManager.EarlyLoadedService]
internal sealed class AddonLifecyclePooledArgs : IServiceType
{
private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64];
private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64];
private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64];
private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64];
private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64];
private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64];
private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64];
[ServiceManager.ServiceConstructor]
private AddonLifecyclePooledArgs()
{
}
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonSetupArgs> Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonFinalizeArgs> Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonDrawArgs> Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonUpdateArgs> Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonRefreshArgs> Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonRequestedUpdateArgs> Rent(out AddonRequestedUpdateArgs arg) =>
new(out arg, this.addonRequestedUpdateArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonReceiveEventArgs> Rent(out AddonReceiveEventArgs arg) =>
new(out arg, this.addonReceiveEventArgPool);
/// <summary>Returns the object to the pool on dispose.</summary>
/// <typeparam name="T">The type.</typeparam>
public readonly ref struct PooledEntry<T>
where T : AddonArgs, new()
{
private readonly Span<T> pool;
private readonly T obj;
/// <summary>Initializes a new instance of the <see cref="PooledEntry{T}"/> struct.</summary>
/// <param name="arg">An instance of the argument.</param>
/// <param name="pool">The pool to rent from and return to.</param>
public PooledEntry(out T arg, Span<T> pool)
{
this.pool = pool;
foreach (ref var item in pool)
{
if (Interlocked.Exchange(ref item, null) is { } v)
{
this.obj = arg = v;
return;
}
}
this.obj = arg = new();
}
/// <summary>Returns the item to the pool.</summary>
public void Dispose()
{
var tmp = this.obj;
foreach (ref var item in this.pool)
{
if (Interlocked.Exchange(ref item, tmp) is not { } tmp2)
return;
tmp = tmp2;
}
}
}
}

View file

@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Base class for AddonLifecycle AddonArgTypes.
/// </summary>
public abstract unsafe class AddonArgs
public abstract class AddonArgs
{
/// <summary>
/// Constant string representing the name of an addon that is invalid.

View file

@ -3,7 +3,7 @@
/// <summary>
/// Addon argument data for Draw events.
/// </summary>
public class AddonDrawArgs : AddonArgs, ICloneable
public class AddonDrawArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonDrawArgs"/> class.
@ -15,10 +15,4 @@ public class AddonDrawArgs : AddonArgs, ICloneable
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Draw;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonFinalizeArgs : AddonArgs, ICloneable
public class AddonFinalizeArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonFinalizeArgs"/> class.
@ -15,10 +15,4 @@ public class AddonFinalizeArgs : AddonArgs, ICloneable
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Finalize;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -0,0 +1,18 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Draw events.
/// </summary>
public class AddonGenericArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonGenericArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonGenericArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Generic;
}

View file

@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonReceiveEventArgs : AddonArgs, ICloneable
public class AddonReceiveEventArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
@ -36,19 +36,13 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable
/// </summary>
public nint Data { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkEventType = default;
this.EventParam = default;
this.AtkEvent = default;
this.Data = default;
this.AtkEventType = 0;
this.EventParam = 0;
this.AtkEvent = 0;
this.Data = 0;
}
}

View file

@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Refresh events.
/// </summary>
public class AddonRefreshArgs : AddonArgs, ICloneable
public class AddonRefreshArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class.
@ -33,17 +33,11 @@ public class AddonRefreshArgs : AddonArgs, ICloneable
/// </summary>
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
this.AtkValueCount = 0;
this.AtkValues = 0;
}
}

View file

@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for OnRequestedUpdate events.
/// </summary>
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
public class AddonRequestedUpdateArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
@ -26,17 +26,11 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
/// </summary>
public nint StringArrayData { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.NumberArrayData = default;
this.StringArrayData = default;
this.NumberArrayData = 0;
this.StringArrayData = 0;
}
}

View file

@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Setup events.
/// </summary>
public class AddonSetupArgs : AddonArgs, ICloneable
public class AddonSetupArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
@ -33,17 +33,11 @@ public class AddonSetupArgs : AddonArgs, ICloneable
/// </summary>
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
this.AtkValueCount = 0;
this.AtkValues = 0;
}
}

View file

@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Update events.
/// </summary>
public class AddonUpdateArgs : AddonArgs, ICloneable
public class AddonUpdateArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonUpdateArgs"/> class.
@ -30,16 +30,10 @@ public class AddonUpdateArgs : AddonArgs, ICloneable
/// </summary>
internal float TimeDeltaInternal { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.TimeDeltaInternal = default;
this.TimeDeltaInternal = 0;
}
}

View file

@ -39,4 +39,9 @@ public enum AddonArgsType
/// Contains argument data for ReceiveEvent.
/// </summary>
ReceiveEvent,
/// <summary>
/// Generic arg type that contains no meaningful data
/// </summary>
Generic,
}

View file

@ -118,4 +118,44 @@ public enum AddonEvent
/// See <see cref="PreReceiveEvent"/> for more information.
/// </summary>
PostReceiveEvent,
/// <summary>
/// An event that is fired before an addon processes its open method.
/// </summary>
PreOpen,
/// <summary>
/// An event that is fired after an addon has processed its open method.
/// </summary>
PostOpen,
/// <summary>
/// An even that is fired before an addon processes its close method.
/// </summary>
PreClose,
/// <summary>
/// An event that is fired after an addon has processed its close method.
/// </summary>
PostClose,
/// <summary>
/// An event that is fired before an addon processes its show method.
/// </summary>
PreShow,
/// <summary>
/// An event that is fired after an addon has processed its show method.
/// </summary>
PostShow,
/// <summary>
/// An event that is fired before an addon processes its hide method.
/// </summary>
PreHide,
/// <summary>
/// An event that is fired after an addon has processed its hide method.
/// </summary>
PostHide,
}

View file

@ -1,16 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using Dalamud.Hooking.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
@ -26,69 +24,33 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
private readonly Dictionary<string, AddonVirtualTable> modifiedTables = [];
private readonly nint disallowedReceiveEventAddress;
private readonly AddonLifecycleAddressResolver address;
private readonly AddonSetupHook<AtkUnitBase.Delegates.OnSetup> onAddonSetupHook;
private readonly Hook<AddonFinalizeDelegate> onAddonFinalizeHook;
private readonly CallHook<AtkUnitBase.Delegates.Draw> onAddonDrawHook;
private readonly CallHook<AtkUnitBase.Delegates.Update> onAddonUpdateHook;
private readonly Hook<AtkUnitManager.Delegates.RefreshAddon> onAddonRefreshHook;
private readonly CallHook<AtkUnitBase.Delegates.OnRequestedUpdate> onAddonRequestedUpdateHook;
private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
[ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner)
{
this.address = new AddonLifecycleAddressResolver();
this.address.Setup(sigScanner);
this.onInitializeAddonHook = Hook<AtkUnitBase.Delegates.Initialize>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
this.onInitializeAddonHook.Enable();
this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent;
var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon;
this.onAddonSetupHook = new AddonSetupHook<AtkUnitBase.Delegates.OnSetup>(this.address.AddonSetup, this.OnAddonSetup);
this.onAddonFinalizeHook = Hook<AddonFinalizeDelegate>.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
this.onAddonDrawHook = new CallHook<AtkUnitBase.Delegates.Draw>(this.address.AddonDraw, this.OnAddonDraw);
this.onAddonUpdateHook = new CallHook<AtkUnitBase.Delegates.Update>(this.address.AddonUpdate, this.OnAddonUpdate);
this.onAddonRefreshHook = Hook<AtkUnitManager.Delegates.RefreshAddon>.FromAddress(refreshAddonAddress, this.OnAddonRefresh);
this.onAddonRequestedUpdateHook = new CallHook<AtkUnitBase.Delegates.OnRequestedUpdate>(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
this.onAddonSetupHook.Enable();
this.onAddonFinalizeHook.Enable();
this.onAddonDrawHook.Enable();
this.onAddonUpdateHook.Enable();
this.onAddonRefreshHook.Enable();
this.onAddonRequestedUpdateHook.Enable();
Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}");
}
private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
/// <summary>
/// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
/// </summary>
internal List<AddonLifecycleReceiveEventListener> ReceiveEventListeners { get; } = new();
/// <summary>
/// Gets a list of all AddonLifecycle Event Listeners.
/// </summary>
internal List<AddonLifecycleEventListener> EventListeners { get; } = new();
internal List<AddonLifecycleEventListener> EventListeners { get; } = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.onAddonSetupHook.Dispose();
this.onAddonFinalizeHook.Dispose();
this.onAddonDrawHook.Dispose();
this.onAddonUpdateHook.Dispose();
this.onAddonRefreshHook.Dispose();
this.onAddonRequestedUpdateHook.Dispose();
this.onInitializeAddonHook?.Dispose();
this.onInitializeAddonHook = null;
foreach (var receiveEventListener in this.ReceiveEventListeners)
foreach (var virtualTable in this.modifiedTables.Values)
{
receiveEventListener.Dispose();
virtualTable.Dispose();
}
}
@ -101,16 +63,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
this.framework.RunOnTick(() =>
{
this.EventListeners.Add(listener);
// If we want receive event messages have an already active addon, enable the receive event hook.
// If the addon isn't active yet, we'll grab the hook when it sets up.
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
{
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
{
receiveEventListener.TryEnable();
}
}
});
}
@ -126,20 +78,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
this.framework.RunOnTick(() =>
{
this.EventListeners.Remove(listener);
// If we are disabling an ReceiveEvent listener, check if we should disable the hook.
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
{
// Get the ReceiveEvent Listener for this addon
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
{
// If there are no other listeners listening for this event, disable the hook.
if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
{
receiveEventListener.Disable();
}
}
}
});
}
@ -176,201 +114,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
}
}
private void RegisterReceiveEventHook(AtkUnitBase* addon)
{
// Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
// Disallows hooking the core internal event handler.
var addonName = addon->NameString;
var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent;
if (receiveEventAddress != this.disallowedReceiveEventAddress)
{
// If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener)
{
if (!existingListener.AddonNames.Contains(addonName))
{
existingListener.AddonNames.Add(addonName);
}
}
// Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
else
{
this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress));
}
// If we have an active listener for this addon already, we need to activate this hook.
if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
{
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
{
receiveEventListener.TryEnable();
}
}
}
}
private void UnregisterReceiveEventHook(string addonName)
{
// Remove this addons ReceiveEvent Registration
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
{
eventListener.AddonNames.Remove(addonName);
// If there are no more listeners let's remove and dispose.
if (eventListener.AddonNames.Count is 0)
{
this.ReceiveEventListeners.Remove(eventListener);
eventListener.Dispose();
}
}
}
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
private void OnAddonInitialize(AtkUnitBase* addon)
{
try
{
this.RegisterReceiveEventHook(addon);
this.LogInitialize(addon->NameString);
if (!this.modifiedTables.ContainsKey(addon->NameString))
{
// AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
var managedVirtualTableEntry = new AddonVirtualTable(addon, this)
{
// This event is invoked when the game itself has disposed of an addon
// We can use this to know when to remove our virtual table entry
OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString),
};
this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry);
}
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize.");
}
using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
valueCount = arg.AtkValueCount;
values = (AtkValue*)arg.AtkValues;
try
{
addon->OnSetup(valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
this.onInitializeAddonHook!.Original(addon);
}
this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
}
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
[Conditional("DEBUG")]
private void LogInitialize(string addonName)
{
try
{
var addonName = atkUnitBase[0]->NameString;
this.UnregisterReceiveEventHook(addonName);
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
}
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
arg.Clear();
arg.Addon = (nint)atkUnitBase[0];
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
try
{
this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
}
}
private void OnAddonDraw(AtkUnitBase* addon)
{
using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
try
{
addon->Draw();
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.TimeDeltaInternal = delta;
this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
try
{
addon->Update(delta);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
}
private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
var result = false;
using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
valueCount = arg.AtkValueCount;
values = (AtkValue*)arg.AtkValues;
try
{
result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.NumberArrayData = (nint)numberArrayData;
arg.StringArrayData = (nint)stringArrayData;
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
numberArrayData = (NumberArrayData**)arg.NumberArrayData;
stringArrayData = (StringArrayData**)arg.StringArrayData;
try
{
addon->OnRequestedUpdate(numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
Log.Debug($"Initializing {addonName}");
}
}
@ -387,7 +161,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
private readonly List<AddonLifecycleEventListener> eventListeners = new();
private readonly List<AddonLifecycleEventListener> eventListeners = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()

View file

@ -1,56 +1,24 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using Dalamud.Utility;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// AddonLifecycleService memory address resolver.
/// </summary>
internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")]
internal class AddonLifecycleAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the addon setup hook invoked by the AtkUnitManager.
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
/// This is called for a majority of all addon OnSetup's.
/// </summary>
public nint AddonSetup { get; private set; }
/// <summary>
/// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
/// This seems to be called rarely for specific addons.
/// </summary>
public nint AddonSetup2 { get; private set; }
/// <summary>
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
/// </summary>
public nint AddonFinalize { get; private set; }
/// <summary>
/// Gets the address of the addon draw hook invoked by virtual function call.
/// </summary>
public nint AddonDraw { get; private set; }
/// <summary>
/// Gets the address of the addon update hook invoked by virtual function call.
/// </summary>
public nint AddonUpdate { get; private set; }
/// <summary>
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
/// </summary>
public nint AddonOnRequestedUpdate { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig)
{
this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB");
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01");
this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2");
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30");
}
}

View file

@ -1,112 +0,0 @@
using System.Collections.Generic;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent.
/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly.
/// </summary>
internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
{
private static readonly ModuleLog Log = new("AddonLifecycle");
[ServiceManager.ServiceDependency]
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
/// </summary>
/// <param name="service">AddonLifecycle service instance.</param>
/// <param name="addonName">Initial Addon Requesting this listener.</param>
/// <param name="receiveEventAddress">Address of Addon's ReceiveEvent function.</param>
internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
{
this.AddonLifecycle = service;
this.AddonNames = [addonName];
this.FunctionAddress = receiveEventAddress;
}
/// <summary>
/// Gets the list of addons that use this receive event hook.
/// </summary>
public List<string> AddonNames { get; init; }
/// <summary>
/// Gets the address of the ReceiveEvent function as provided by the vtable on setup.
/// </summary>
public nint FunctionAddress { get; init; }
/// <summary>
/// Gets the contained hook for these addons.
/// </summary>
public Hook<AtkUnitBase.Delegates.ReceiveEvent>? Hook { get; private set; }
/// <summary>
/// Gets or sets the Reference to AddonLifecycle service instance.
/// </summary>
private AddonLifecycle AddonLifecycle { get; set; }
/// <summary>
/// Try to hook and enable this receive event handler.
/// </summary>
public void TryEnable()
{
this.Hook ??= Hook<AtkUnitBase.Delegates.ReceiveEvent>.FromAddress(this.FunctionAddress, this.OnReceiveEvent);
this.Hook?.Enable();
}
/// <summary>
/// Disable the hook for this receive event handler.
/// </summary>
public void Disable()
{
this.Hook?.Disable();
}
/// <inheritdoc/>
public void Dispose()
{
this.Hook?.Dispose();
}
private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
// Check that we didn't get here through a call to another addons handler.
var addonName = addon->NameString;
if (!this.AddonNames.Contains(addonName))
{
this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
return;
}
using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkEventType = (byte)eventType;
arg.EventParam = eventParam;
arg.AtkEvent = (IntPtr)atkEvent;
arg.Data = (nint)atkEventData;
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
eventType = (AtkEventType)arg.AtkEventType;
eventParam = arg.EventParam;
atkEvent = (AtkEvent*)arg.AtkEvent;
atkEventData = (AtkEventData*)arg.Data;
try
{
this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
}
}

View file

@ -1,80 +0,0 @@
using System.Runtime.InteropServices;
using Reloaded.Hooks.Definitions;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// This class represents a callsite hook used to replace the address of the OnSetup function in r9.
/// </summary>
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
internal class AddonSetupHook<T> : IDisposable where T : Delegate
{
private readonly Reloaded.Hooks.AsmHook asmHook;
private T? detour;
private bool activated;
/// <summary>
/// Initializes a new instance of the <see cref="AddonSetupHook{T}"/> class.
/// </summary>
/// <param name="address">Address of the instruction to replace.</param>
/// <param name="detour">Delegate to invoke.</param>
internal AddonSetupHook(nint address, T detour)
{
this.detour = detour;
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
var code = new[]
{
"use64",
$"mov r9, 0x{detourPtr:X8}",
};
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);
}
/// <summary>
/// Gets a value indicating whether the hook is enabled.
/// </summary>
public bool IsEnabled => this.asmHook.IsEnabled;
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
public void Enable()
{
if (!this.activated)
{
this.activated = true;
this.asmHook.Activate();
return;
}
this.asmHook.Enable();
}
/// <summary>
/// Stops intercepting a call to the function.
/// </summary>
public void Disable()
{
this.asmHook.Disable();
}
/// <summary>
/// Remove a hook from the current process.
/// </summary>
public void Dispose()
{
this.asmHook.Disable();
this.detour = null;
}
}

View file

@ -0,0 +1,405 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// Represents a class that holds references to an addons original and modified virtual table entries.
/// </summary>
internal unsafe class AddonVirtualTable : IDisposable
{
// This need to be at minimum the largest virtual table size of all addons
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 200;
private const bool EnableAdvancedLogging = true;
private const bool EnableSpammyLogging = false;
private static readonly ModuleLog Log = new("LifecycleVT");
private readonly AddonLifecycle lifecycleService;
// Obsolete warning is only to prevent users from creating their own event objects.
#pragma warning disable CS0618 // Type or member is obsolete
private readonly AddonSetupArgs addonSetupArg = new();
private readonly AddonFinalizeArgs addonFinalizeArg = new();
private readonly AddonDrawArgs addonDrawArg = new();
private readonly AddonUpdateArgs addonUpdateArg = new();
private readonly AddonRefreshArgs addonRefreshArg = new();
private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new();
private readonly AddonReceiveEventArgs addonReceiveEventArg = new();
private readonly AddonGenericArgs addonGenericArg = new();
#pragma warning restore CS0618 // Type or member is obsolete
private readonly AtkUnitBase* atkUnitBase;
private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable;
private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable;
// Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
// the CLR needs to know they are in use, or it will invalidate them causing random crashing.
private readonly AtkUnitBase.Delegates.Dtor destructorFunction;
private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction;
private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction;
private readonly AtkUnitBase.Delegates.Draw drawFunction;
private readonly AtkUnitBase.Delegates.Update updateFunction;
private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction;
private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction;
private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction;
private readonly AtkUnitBase.Delegates.Open openFunction;
private readonly AtkUnitBase.Delegates.Close closeFunction;
private readonly AtkUnitBase.Delegates.Show showFunction;
private readonly AtkUnitBase.Delegates.Hide hideFunction;
/// <summary>
/// Initializes a new instance of the <see cref="AddonVirtualTable"/> class.
/// </summary>
/// <param name="addon">AtkUnitBase* for the addon to replace the table of.</param>
/// <param name="lifecycleService">Reference to AddonLifecycle service to callback and invoke listeners.</param>
internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService)
{
this.atkUnitBase = addon;
this.lifecycleService = lifecycleService;
// Save original virtual table
this.originalVirtualTable = addon->VirtualTable;
// Create copy of original table
// Note this will copy any derived/overriden functions that this specific addon has.
// Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
// Overwrite the addons existing virtual table with our own
addon->VirtualTable = this.modifiedVirtualTable;
// Pin each of our listener functions
this.destructorFunction = this.OnAddonDestructor;
this.onSetupFunction = this.OnAddonSetup;
this.finalizerFunction = this.OnAddonFinalize;
this.drawFunction = this.OnAddonDraw;
this.updateFunction = this.OnAddonUpdate;
this.onRefreshFunction = this.OnAddonRefresh;
this.onRequestedUpdateFunction = this.OnRequestedUpdate;
this.onReceiveEventFunction = this.OnAddonReceiveEvent;
this.openFunction = this.OnAddonOpen;
this.closeFunction = this.OnAddonClose;
this.showFunction = this.OnAddonShow;
this.hideFunction = this.OnAddonHide;
// Overwrite specific virtual table entries
this.modifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
this.modifiedVirtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
this.modifiedVirtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
this.modifiedVirtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
this.modifiedVirtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AtkUnitBase*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
this.modifiedVirtualTable->Open = (delegate* unmanaged<AtkUnitBase*, uint, bool>)Marshal.GetFunctionPointerForDelegate(this.openFunction);
this.modifiedVirtualTable->Close = (delegate* unmanaged<AtkUnitBase*, bool, bool>)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
this.modifiedVirtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
this.modifiedVirtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
}
/// <summary>
/// Gets an event that is invoked when this addon's Finalize method is called from native.
/// </summary>
public required Action OnAddonFinalized { get; init; }
/// <summary>
/// <em>WARNING!</em> This should not be called at any time except during dalamud unload.
/// </summary>
public void Dispose()
{
this.atkUnitBase->VirtualTable = this.originalVirtualTable;
IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
}
private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags)
{
this.LogEvent();
var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags);
if ((freeFlags & 1) == 1)
{
IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount);
this.OnAddonFinalized();
}
return result;
}
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
this.LogEvent();
this.addonSetupArg.Clear();
this.addonSetupArg.Addon = addon;
this.addonSetupArg.AtkValueCount = valueCount;
this.addonSetupArg.AtkValues = (nint)values;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.addonSetupArg);
valueCount = this.addonSetupArg.AtkValueCount;
values = (AtkValue*)this.addonSetupArg.AtkValues;
try
{
this.originalVirtualTable->OnSetup(addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.addonSetupArg);
}
private void OnAddonFinalize(AtkUnitBase* thisPtr)
{
this.LogEvent();
this.addonFinalizeArg.Clear();
this.addonFinalizeArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg);
try
{
this.originalVirtualTable->Finalizer(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
}
}
private void OnAddonDraw(AtkUnitBase* addon)
{
this.LogEvent();
this.addonDrawArg.Clear();
this.addonDrawArg.Addon = addon;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg);
try
{
this.originalVirtualTable->Draw(addon);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.addonDrawArg);
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
this.LogEvent();
this.addonUpdateArg.Clear();
this.addonUpdateArg.Addon = addon;
this.addonUpdateArg.TimeDeltaInternal = delta;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg);
try
{
this.originalVirtualTable->Update(addon, delta);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.addonUpdateArg);
}
private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
this.LogEvent();
var result = false;
this.addonRefreshArg.Clear();
this.addonRefreshArg.Addon = addon;
this.addonRefreshArg.AtkValueCount = valueCount;
this.addonRefreshArg.AtkValues = (nint)values;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.addonRefreshArg);
valueCount = this.addonRefreshArg.AtkValueCount;
values = (AtkValue*)this.addonRefreshArg.AtkValues;
try
{
result = this.originalVirtualTable->OnRefresh(addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.addonRefreshArg);
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
this.LogEvent();
this.addonRequestedUpdateArg.Clear();
this.addonRequestedUpdateArg.Addon = addon;
this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData;
this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.addonRequestedUpdateArg);
numberArrayData = (NumberArrayData**)this.addonRequestedUpdateArg.NumberArrayData;
stringArrayData = (StringArrayData**)this.addonRequestedUpdateArg.StringArrayData;
try
{
this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.addonRequestedUpdateArg);
}
private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
this.LogEvent();
this.addonReceiveEventArg.Clear();
this.addonReceiveEventArg.Addon = (nint)addon;
this.addonReceiveEventArg.AtkEventType = (byte)eventType;
this.addonReceiveEventArg.EventParam = eventParam;
this.addonReceiveEventArg.AtkEvent = (IntPtr)atkEvent;
this.addonReceiveEventArg.Data = (nint)atkEventData;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.addonReceiveEventArg);
eventType = (AtkEventType)this.addonReceiveEventArg.AtkEventType;
eventParam = this.addonReceiveEventArg.EventParam;
atkEvent = (AtkEvent*)this.addonReceiveEventArg.AtkEvent;
atkEventData = (AtkEventData*)this.addonReceiveEventArg.Data;
try
{
this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.addonReceiveEventArg);
}
private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
{
this.LogEvent();
var result = false;
this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg);
try
{
result = this.originalVirtualTable->Open(thisPtr, depthLayer);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.addonGenericArg);
return result;
}
private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback)
{
this.LogEvent();
var result = false;
this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg);
try
{
result = this.originalVirtualTable->Close(thisPtr, fireCallback);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.addonGenericArg);
return result;
}
private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags)
{
this.LogEvent();
this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg);
try
{
this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.addonGenericArg);
}
private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags)
{
this.LogEvent();
this.addonGenericArg.Clear();
this.addonGenericArg.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg);
try
{
this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.addonGenericArg);
}
[Conditional("DEBUG")]
private void LogEvent([CallerMemberName] string caller = "")
{
if (EnableAdvancedLogging)
{
if (!EnableSpammyLogging)
{
if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
return;
}
Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}");
}
}
}

View file

@ -1,100 +0,0 @@
using System.Runtime.InteropServices;
using Reloaded.Hooks.Definitions;
namespace Dalamud.Hooking.Internal;
/// <summary>
/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook.
/// This is a destructive operation, no other callsite hooks can coexist at the same address.
///
/// There's no .Original for this hook type.
/// This is only intended for be for functions where the parameters provided allow you to invoke the original call.
///
/// This class was specifically added for hooking virtual function callsites.
/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered.
/// </summary>
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
internal class CallHook<T> : IDalamudHook where T : Delegate
{
private readonly Reloaded.Hooks.AsmHook asmHook;
private T? detour;
private bool activated;
/// <summary>
/// Initializes a new instance of the <see cref="CallHook{T}"/> class.
/// </summary>
/// <param name="address">Address of the instruction to replace.</param>
/// <param name="detour">Delegate to invoke.</param>
internal CallHook(nint address, T detour)
{
ArgumentNullException.ThrowIfNull(detour);
this.detour = detour;
this.Address = address;
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);
}
/// <summary>
/// Gets a value indicating whether the hook is enabled.
/// </summary>
public bool IsEnabled => this.asmHook.IsEnabled;
/// <inheritdoc/>
public IntPtr Address { get; }
/// <inheritdoc/>
public string BackendName => "Reloaded AsmHook";
/// <inheritdoc/>
public bool IsDisposed => this.detour == null;
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
public void Enable()
{
if (!this.activated)
{
this.activated = true;
this.asmHook.Activate();
return;
}
this.asmHook.Enable();
}
/// <summary>
/// Stops intercepting a call to the function.
/// </summary>
public void Disable()
{
this.asmHook.Disable();
}
/// <summary>
/// Remove a hook from the current process.
/// </summary>
public void Dispose()
{
this.asmHook.Disable();
this.detour = null;
}
}

View file

@ -1,10 +1,8 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@ -54,13 +52,6 @@ public class AddonLifecycleWidget : IDataWindowWidget
this.DrawEventListeners();
ImGui.Unindent();
}
if (ImGui.CollapsingHeader("ReceiveEvent Hooks"u8))
{
ImGui.Indent();
this.DrawReceiveEventHooks();
ImGui.Unindent();
}
}
private void DrawEventListeners()
@ -100,46 +91,4 @@ public class AddonLifecycleWidget : IDataWindowWidget
}
}
}
private void DrawReceiveEventHooks()
{
if (!this.Ready) return;
var listeners = this.AddonLifecycle.ReceiveEventListeners;
if (listeners.Count == 0)
{
ImGui.Text("No ReceiveEvent Hooks are Registered"u8);
}
foreach (var receiveEventListener in this.AddonLifecycle.ReceiveEventListeners)
{
if (ImGui.CollapsingHeader(string.Join(", ", receiveEventListener.AddonNames)))
{
ImGui.Columns(2);
var functionAddress = receiveEventListener.FunctionAddress;
ImGui.Text("Hook Address"u8);
ImGui.NextColumn();
ImGui.Text($"0x{functionAddress:X} (ffxiv_dx11.exe+{functionAddress - Process.GetCurrentProcess().MainModule!.BaseAddress:X})");
ImGui.NextColumn();
ImGui.Text("Hook Status"u8);
ImGui.NextColumn();
if (receiveEventListener.Hook is null)
{
ImGui.Text("Hook is null"u8);
}
else
{
var color = receiveEventListener.Hook.IsEnabled ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed;
var text = receiveEventListener.Hook.IsEnabled ? "Enabled"u8 : "Disabled"u8;
ImGui.TextColored(color, text);
}
ImGui.Columns(1);
}
}
}
}