diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
deleted file mode 100644
index 14def2036..000000000
--- a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs
+++ /dev/null
@@ -1,107 +0,0 @@
-using System.Runtime.CompilerServices;
-using System.Threading;
-
-using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
-
-namespace Dalamud.Game.Addon;
-
-/// Argument pool for Addon Lifecycle services.
-[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()
- {
- }
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonRequestedUpdateArgs arg) =>
- new(out arg, this.addonRequestedUpdateArgPool);
-
- /// Rents an instance of an argument.
- /// The rented instance.
- /// The returner.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public PooledEntry Rent(out AddonReceiveEventArgs arg) =>
- new(out arg, this.addonReceiveEventArgPool);
-
- /// Returns the object to the pool on dispose.
- /// The type.
- public readonly ref struct PooledEntry
- where T : AddonArgs, new()
- {
- private readonly Span pool;
- private readonly T obj;
-
- /// Initializes a new instance of the struct.
- /// An instance of the argument.
- /// The pool to rent from and return to.
- public PooledEntry(out T arg, Span 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();
- }
-
- /// Returns the item to the pool.
- 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;
- }
- }
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
index c008db08f..0b2ae1178 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Base class for AddonLifecycle AddonArgTypes.
///
-public abstract unsafe class AddonArgs
+public abstract class AddonArgs
{
///
/// Constant string representing the name of an addon that is invalid.
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
index 989e11912..7254ba7b3 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
@@ -3,7 +3,7 @@
///
/// Addon argument data for Draw events.
///
-public class AddonDrawArgs : AddonArgs, ICloneable
+public class AddonDrawArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -15,10 +15,4 @@ public class AddonDrawArgs : AddonArgs, ICloneable
///
public override AddonArgsType Type => AddonArgsType.Draw;
-
- ///
- public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
index d9401b414..12def3ad3 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for ReceiveEvent events.
///
-public class AddonFinalizeArgs : AddonArgs, ICloneable
+public class AddonFinalizeArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -15,10 +15,4 @@ public class AddonFinalizeArgs : AddonArgs, ICloneable
///
public override AddonArgsType Type => AddonArgsType.Finalize;
-
- ///
- public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
new file mode 100644
index 000000000..f3078af69
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs
@@ -0,0 +1,18 @@
+namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+
+///
+/// Addon argument data for Draw events.
+///
+public class AddonGenericArgs : AddonArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ [Obsolete("Not intended for public construction.", false)]
+ public AddonGenericArgs()
+ {
+ }
+
+ ///
+ public override AddonArgsType Type => AddonArgsType.Generic;
+}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
index 980fe4f2f..05f51b118 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for ReceiveEvent events.
///
-public class AddonReceiveEventArgs : AddonArgs, ICloneable
+public class AddonReceiveEventArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -36,19 +36,13 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable
///
public nint Data { get; set; }
- ///
- public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
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;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
index d28631c3c..c01c065c1 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Refresh events.
///
-public class AddonRefreshArgs : AddonArgs, ICloneable
+public class AddonRefreshArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -33,17 +33,11 @@ public class AddonRefreshArgs : AddonArgs, ICloneable
///
public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
- ///
- public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.AtkValueCount = default;
- this.AtkValues = default;
+ this.AtkValueCount = 0;
+ this.AtkValues = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
index e87a980fd..bf00c5d6e 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for OnRequestedUpdate events.
///
-public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
+public class AddonRequestedUpdateArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -26,17 +26,11 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
///
public nint StringArrayData { get; set; }
- ///
- public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.NumberArrayData = default;
- this.StringArrayData = default;
+ this.NumberArrayData = 0;
+ this.StringArrayData = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
index 0dd9ecee2..9b7e86a61 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs
@@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Setup events.
///
-public class AddonSetupArgs : AddonArgs, ICloneable
+public class AddonSetupArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -33,17 +33,11 @@ public class AddonSetupArgs : AddonArgs, ICloneable
///
public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
- ///
- public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.AtkValueCount = default;
- this.AtkValues = default;
+ this.AtkValueCount = 0;
+ this.AtkValues = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
index a263f6ae4..bab62fc89 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
@@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
///
/// Addon argument data for Update events.
///
-public class AddonUpdateArgs : AddonArgs, ICloneable
+public class AddonUpdateArgs : AddonArgs
{
///
/// Initializes a new instance of the class.
@@ -30,16 +30,10 @@ public class AddonUpdateArgs : AddonArgs, ICloneable
///
internal float TimeDeltaInternal { get; set; }
- ///
- public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
-
- ///
- object ICloneable.Clone() => this.Clone();
-
///
internal override void Clear()
{
base.Clear();
- this.TimeDeltaInternal = default;
+ this.TimeDeltaInternal = 0;
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
index b58b5f4c7..95dc5f718 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
@@ -9,34 +9,39 @@ public enum AddonArgsType
/// Contains argument data for Setup.
///
Setup,
-
+
///
/// Contains argument data for Update.
///
Update,
-
+
///
/// Contains argument data for Draw.
- ///
+ ///
Draw,
-
+
///
/// Contains argument data for Finalize.
- ///
+ ///
Finalize,
-
+
///
/// Contains argument data for RequestedUpdate.
- ///
+ ///
RequestedUpdate,
-
+
///
/// Contains argument data for Refresh.
- ///
+ ///
Refresh,
-
+
///
/// Contains argument data for ReceiveEvent.
///
ReceiveEvent,
+
+ ///
+ /// Generic arg type that contains no meaningful data
+ ///
+ Generic,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
index 5fd0ac964..7738d6c6a 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs
@@ -16,7 +16,7 @@ public enum AddonEvent
///
///
PreSetup,
-
+
///
/// An event that is fired after an addon has finished its initial setup. This event is particularly useful for
/// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data
@@ -64,7 +64,7 @@ public enum AddonEvent
///
///
PreFinalize,
-
+
///
/// An event that is fired before a call to is made in response to a
/// change in the subscribed or
@@ -81,13 +81,13 @@ public enum AddonEvent
/// to the Free Company's overview.
///
PreRequestedUpdate,
-
+
///
/// An event that is fired after an addon has finished processing an ArrayData update.
/// See for more information.
///
PostRequestedUpdate,
-
+
///
/// An event that is fired before an addon calls its method. Refreshes are
/// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to
@@ -96,13 +96,13 @@ public enum AddonEvent
///
///
PreRefresh,
-
+
///
/// An event that is fired after an addon has finished its refresh.
/// See for more information.
///
PostRefresh,
-
+
///
/// An event that is fired before an addon begins processing a user-driven event via
/// , such as mousing over an element or clicking a button. This event
@@ -112,10 +112,50 @@ public enum AddonEvent
///
///
PreReceiveEvent,
-
+
///
/// An event that is fired after an addon finishes calling its method.
/// See for more information.
///
PostReceiveEvent,
+
+ ///
+ /// An event that is fired before an addon processes its open method.
+ ///
+ PreOpen,
+
+ ///
+ /// An event that is fired after an addon has processed its open method.
+ ///
+ PostOpen,
+
+ ///
+ /// An even that is fired before an addon processes its close method.
+ ///
+ PreClose,
+
+ ///
+ /// An event that is fired after an addon has processed its close method.
+ ///
+ PostClose,
+
+ ///
+ /// An event that is fired before an addon processes its show method.
+ ///
+ PreShow,
+
+ ///
+ /// An event that is fired after an addon has processed its show method.
+ ///
+ PostShow,
+
+ ///
+ /// An event that is fired before an addon processes its hide method.
+ ///
+ PreHide,
+
+ ///
+ /// An event that is fired after an addon has processed its hide method.
+ ///
+ PostHide,
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index b44ab8764..cea30d6be 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -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.Get();
- [ServiceManager.ServiceDependency]
- private readonly AddonLifecyclePooledArgs argsPool = Service.Get();
+ private readonly Dictionary modifiedTables = [];
- private readonly nint disallowedReceiveEventAddress;
-
- private readonly AddonLifecycleAddressResolver address;
- private readonly AddonSetupHook onAddonSetupHook;
- private readonly Hook onAddonFinalizeHook;
- private readonly CallHook onAddonDrawHook;
- private readonly CallHook onAddonUpdateHook;
- private readonly Hook onAddonRefreshHook;
- private readonly CallHook onAddonRequestedUpdateHook;
+ private Hook? onInitializeAddonHook;
[ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner)
{
- this.address = new AddonLifecycleAddressResolver();
- this.address.Setup(sigScanner);
+ this.onInitializeAddonHook = Hook.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(this.address.AddonSetup, this.OnAddonSetup);
- this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
- this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw);
- this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate);
- this.onAddonRefreshHook = Hook.FromAddress(refreshAddonAddress, this.OnAddonRefresh);
- this.onAddonRequestedUpdateHook = new CallHook(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);
-
- ///
- /// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
- ///
- internal List ReceiveEventListeners { get; } = new();
-
///
/// Gets a list of all AddonLifecycle Event Listeners.
///
- internal List EventListeners { get; } = new();
+ internal List EventListeners { get; } = [];
///
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();
- }
- }
});
}
@@ -122,24 +74,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
{
// Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update.
listener.Removed = true;
-
+
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();
- }
- }
- }
});
}
@@ -160,7 +98,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
// If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener.
if (listener.Removed)
continue;
-
+
// Match on string.empty for listeners that want events for all addons.
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
continue;
@@ -176,201 +114,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
}
}
- private void RegisterReceiveEventHook(AtkUnitBase* addon)
+ private void OnAddonInitialize(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)
+ try
{
- // 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)
+ this.LogInitialize(addon->NameString);
+
+ if (!this.modifiedTables.ContainsKey(addon->NameString))
{
- if (!existingListener.AddonNames.Contains(addonName))
+ // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
+ var managedVirtualTableEntry = new AddonVirtualTable(addon, this)
{
- existingListener.AddonNames.Add(addonName);
- }
- }
+ // 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),
+ };
- // 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();
- }
+ this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry);
}
}
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize.");
+ }
+
+ this.onInitializeAddonHook!.Original(addon);
}
- private void UnregisterReceiveEventHook(string addonName)
+ [Conditional("DEBUG")]
+ private void LogInitialize(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)
- {
- try
- {
- this.RegisterReceiveEventHook(addon);
- }
- catch (Exception e)
- {
- Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
- }
-
- 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.InvokeListenersSafely(AddonEvent.PostSetup, arg);
- }
-
- private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
- {
- 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.Get();
- private readonly List eventListeners = new();
+ private readonly List eventListeners = [];
///
void IInternalDisposableService.DisposeService()
@@ -458,7 +232,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
this.eventListeners.RemoveAll(entry =>
{
if (entry.FunctionDelegate != handler) return false;
-
+
this.addonLifecycleService.UnregisterListener(entry);
return true;
});
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
index 854d666fd..1d767aac4 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
@@ -1,56 +1,24 @@
-using FFXIVClientStructs.FFXIV.Component.GUI;
+using Dalamud.Utility;
namespace Dalamud.Game.Addon.Lifecycle;
///
/// AddonLifecycleService memory address resolver.
///
-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
{
- ///
- /// 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.
- ///
- public nint AddonSetup { get; private set; }
-
- ///
- /// 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.
- ///
- public nint AddonSetup2 { get; private set; }
-
///
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
///
public nint AddonFinalize { get; private set; }
- ///
- /// Gets the address of the addon draw hook invoked by virtual function call.
- ///
- public nint AddonDraw { get; private set; }
-
- ///
- /// Gets the address of the addon update hook invoked by virtual function call.
- ///
- public nint AddonUpdate { get; private set; }
-
- ///
- /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
- ///
- public nint AddonOnRequestedUpdate { get; private set; }
-
///
/// Scan for and setup any configured address pointers.
///
/// The signature scanner to facilitate setup.
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");
}
}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
deleted file mode 100644
index 0d2bcc7f2..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
+++ /dev/null
@@ -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;
-
-///
-/// 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.
-///
-internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
-{
- private static readonly ModuleLog Log = new("AddonLifecycle");
-
- [ServiceManager.ServiceDependency]
- private readonly AddonLifecyclePooledArgs argsPool = Service.Get();
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// AddonLifecycle service instance.
- /// Initial Addon Requesting this listener.
- /// Address of Addon's ReceiveEvent function.
- internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
- {
- this.AddonLifecycle = service;
- this.AddonNames = [addonName];
- this.FunctionAddress = receiveEventAddress;
- }
-
- ///
- /// Gets the list of addons that use this receive event hook.
- ///
- public List AddonNames { get; init; }
-
- ///
- /// Gets the address of the ReceiveEvent function as provided by the vtable on setup.
- ///
- public nint FunctionAddress { get; init; }
-
- ///
- /// Gets the contained hook for these addons.
- ///
- public Hook? Hook { get; private set; }
-
- ///
- /// Gets or sets the Reference to AddonLifecycle service instance.
- ///
- private AddonLifecycle AddonLifecycle { get; set; }
-
- ///
- /// Try to hook and enable this receive event handler.
- ///
- public void TryEnable()
- {
- this.Hook ??= Hook.FromAddress(this.FunctionAddress, this.OnReceiveEvent);
- this.Hook?.Enable();
- }
-
- ///
- /// Disable the hook for this receive event handler.
- ///
- public void Disable()
- {
- this.Hook?.Disable();
- }
-
- ///
- 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);
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs
deleted file mode 100644
index 297323b8f..000000000
--- a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System.Runtime.InteropServices;
-
-using Reloaded.Hooks.Definitions;
-
-namespace Dalamud.Game.Addon.Lifecycle;
-
-///
-/// This class represents a callsite hook used to replace the address of the OnSetup function in r9.
-///
-/// Delegate signature for this hook.
-internal class AddonSetupHook : IDisposable where T : Delegate
-{
- private readonly Reloaded.Hooks.AsmHook asmHook;
-
- private T? detour;
- private bool activated;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Address of the instruction to replace.
- /// Delegate to invoke.
- 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);
- }
-
- ///
- /// Gets a value indicating whether the hook is enabled.
- ///
- public bool IsEnabled => this.asmHook.IsEnabled;
-
- ///
- /// Starts intercepting a call to the function.
- ///
- public void Enable()
- {
- if (!this.activated)
- {
- this.activated = true;
- this.asmHook.Activate();
- return;
- }
-
- this.asmHook.Enable();
- }
-
- ///
- /// Stops intercepting a call to the function.
- ///
- public void Disable()
- {
- this.asmHook.Disable();
- }
-
- ///
- /// Remove a hook from the current process.
- ///
- public void Dispose()
- {
- this.asmHook.Disable();
- this.detour = null;
- }
-}
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
new file mode 100644
index 000000000..58e32a252
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
@@ -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;
+
+///
+/// Represents a class that holds references to an addons original and modified virtual table entries.
+///
+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;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// AtkUnitBase* for the addon to replace the table of.
+ /// Reference to AddonLifecycle service to callback and invoke listeners.
+ 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)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
+ this.modifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
+ this.modifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
+ this.modifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
+ this.modifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
+ this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
+ this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
+ this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
+ this.modifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction);
+ this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
+ this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction);
+ this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
+ }
+
+ ///
+ /// Gets an event that is invoked when this addon's Finalize method is called from native.
+ ///
+ public required Action OnAddonFinalized { get; init; }
+
+ ///
+ /// WARNING! This should not be called at any time except during dalamud unload.
+ ///
+ 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}");
+ }
+ }
+}
diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs
deleted file mode 100644
index 92bc6e31a..000000000
--- a/Dalamud/Hooking/Internal/CallHook.cs
+++ /dev/null
@@ -1,100 +0,0 @@
-using System.Runtime.InteropServices;
-
-using Reloaded.Hooks.Definitions;
-
-namespace Dalamud.Hooking.Internal;
-
-///
-/// 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.
-///
-/// Delegate signature for this hook.
-internal class CallHook : IDalamudHook where T : Delegate
-{
- private readonly Reloaded.Hooks.AsmHook asmHook;
-
- private T? detour;
- private bool activated;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Address of the instruction to replace.
- /// Delegate to invoke.
- 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);
- }
-
- ///
- /// Gets a value indicating whether the hook is enabled.
- ///
- public bool IsEnabled => this.asmHook.IsEnabled;
-
- ///
- public IntPtr Address { get; }
-
- ///
- public string BackendName => "Reloaded AsmHook";
-
- ///
- public bool IsDisposed => this.detour == null;
-
- ///
- /// Starts intercepting a call to the function.
- ///
- public void Enable()
- {
- if (!this.activated)
- {
- this.activated = true;
- this.asmHook.Activate();
- return;
- }
-
- this.asmHook.Enable();
- }
-
- ///
- /// Stops intercepting a call to the function.
- ///
- public void Disable()
- {
- this.asmHook.Disable();
- }
-
- ///
- /// Remove a hook from the current process.
- ///
- public void Dispose()
- {
- this.asmHook.Disable();
- this.detour = null;
- }
-}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
index b58166e89..c336f895e 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
@@ -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);
- }
- }
- }
}