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); - } - } - } }