From 9a1fae8246d0d55cf9d52b0c31c28956b456c6d8 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 17:27:48 -0800 Subject: [PATCH 01/11] Refactor Addon Lifecycle --- .../Game/Addon/AddonLifecyclePooledArgs.cs | 107 ----- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 8 +- .../AddonArgTypes/AddonFinalizeArgs.cs | 8 +- .../AddonArgTypes/AddonGenericArgs.cs | 18 + .../AddonArgTypes/AddonReceiveEventArgs.cs | 16 +- .../AddonArgTypes/AddonRefreshArgs.cs | 12 +- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 12 +- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 12 +- .../AddonArgTypes/AddonUpdateArgs.cs | 10 +- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 25 +- Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 54 ++- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 298 ++----------- .../AddonLifecycleAddressResolver.cs | 38 +- .../AddonLifecycleReceiveEventListener.cs | 112 ----- .../Game/Addon/Lifecycle/AddonSetupHook.cs | 80 ---- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 405 ++++++++++++++++++ Dalamud/Hooking/Internal/CallHook.cs | 100 ----- .../Data/Widgets/AddonLifecycleWidget.cs | 51 --- 19 files changed, 543 insertions(+), 825 deletions(-) delete mode 100644 Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs delete mode 100644 Dalamud/Hooking/Internal/CallHook.cs 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); - } - } - } } From 2c1bb7664331d975c9398342b89cba88651df0b5 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 18:56:34 -0800 Subject: [PATCH 02/11] Minor cleanup --- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 11 ++++++----- .../Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 95dc5f718..de32bd254 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -41,7 +41,7 @@ public enum AddonArgsType ReceiveEvent, /// - /// Generic arg type that contains no meaningful data + /// Generic arg type that contains no meaningful data. /// Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index cea30d6be..0c23f5661 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -33,8 +33,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); this.onInitializeAddonHook.Enable(); - - Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}"); } /// @@ -48,10 +46,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.onInitializeAddonHook?.Dispose(); this.onInitializeAddonHook = null; - foreach (var virtualTable in this.modifiedTables.Values) + this.framework.RunOnFrameworkThread(() => { - virtualTable.Dispose(); - } + foreach (var virtualTable in this.modifiedTables.Values) + { + virtualTable.Dispose(); + } + }); } /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 1d767aac4..9359870a5 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")] +[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 58e32a252..ca5d970ef 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -19,7 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableAdvancedLogging = true; + private const bool EnableAdvancedLogging = false; private const bool EnableSpammyLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); From ab0500ca6f9ff49cf0c48da7c585ae8e7429fbdf Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 20:45:54 -0800 Subject: [PATCH 03/11] Fix unreachable code complaint --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index ca5d970ef..54c91248e 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -19,8 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableAdvancedLogging = false; - private const bool EnableSpammyLogging = false; + private const bool EnableLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); @@ -125,7 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); @@ -140,7 +139,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonSetupArg.Clear(); this.addonSetupArg.Addon = addon; @@ -164,7 +163,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonFinalize(AtkUnitBase* thisPtr) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; @@ -182,7 +181,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonDraw(AtkUnitBase* addon) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonDrawArg.Clear(); this.addonDrawArg.Addon = addon; @@ -202,7 +201,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonUpdateArg.Clear(); this.addonUpdateArg.Addon = addon; @@ -223,7 +222,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -250,7 +249,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonRequestedUpdateArg.Clear(); this.addonRequestedUpdateArg.Addon = addon; @@ -274,7 +273,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonReceiveEventArg.Clear(); this.addonReceiveEventArg.Addon = (nint)addon; @@ -302,7 +301,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -326,7 +325,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -350,7 +349,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; @@ -370,7 +369,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; @@ -389,15 +388,13 @@ internal unsafe class AddonVirtualTable : IDisposable } [Conditional("DEBUG")] - private void LogEvent([CallerMemberName] string caller = "") + private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") { - if (EnableAdvancedLogging) + if (loggingEnabled) { - if (!EnableSpammyLogging) - { - if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") - return; - } + // Manually disable the really spammy log events, you can comment this out if you need to debug them. + if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") + return; Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}"); } From c525655be66778cd3d092d32f6ae5aba16c0fbea Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 14:24:35 -0800 Subject: [PATCH 04/11] Improve LifecycleInvoke efficiency with Dictionary --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 0c23f5661..cf1270803 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal List EventListeners { get; } = []; + internal Dictionary> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -61,10 +61,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnTick(() => - { - this.EventListeners.Add(listener); - }); + this.EventListeners.TryAdd(listener.EventType, [ listener ]); + this.EventListeners[listener.EventType].Add(listener); } /// @@ -73,13 +71,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. - listener.Removed = true; - - this.framework.RunOnTick(() => + if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList)) { - this.EventListeners.Remove(listener); - }); + listenerList.Remove(listener); + } } /// @@ -90,16 +85,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// What to blame on errors. internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { + // Early return if we don't have any listeners of this type + if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return; + // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in this.EventListeners) + foreach (var listener in listenerList) { - if (listener.EventType != eventType) - continue; - - // 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; From 166f249e13ed310db4bc44a658d8d473b92ae6a2 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 14:30:40 -0800 Subject: [PATCH 05/11] Use hashset to prevent duplicate entries --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index cf1270803..403671920 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal Dictionary> EventListeners { get; } = []; + internal Dictionary> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() From 29c154f9b5a2d7ab1cabd41cbaac432d06c2b27d Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 08:35:54 -0800 Subject: [PATCH 06/11] Fix accidentally breaking widget --- .../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index c336f895e..73c4e540a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; @@ -58,12 +57,11 @@ public class AddonLifecycleWidget : IDataWindowWidget { if (!this.Ready) return; - foreach (var eventType in Enum.GetValues()) + foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners) { - if (ImGui.CollapsingHeader(eventType.ToString())) + if (ImGui.CollapsingHeader(listenerType.ToString())) { ImGui.Indent(); - var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList(); if (listeners.Count == 0) { From 325d28ee3211d7743fc407f9f934e4bbc66ec48a Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:08:24 -0800 Subject: [PATCH 07/11] further improve performance --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 61 ++++++++++++++----- .../Data/Widgets/AddonLifecycleWidget.cs | 40 ++++++------ 2 files changed, 67 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 403671920..e38f56921 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal Dictionary> EventListeners { get; } = []; + /// Mapping is: EventType -> AddonName -> ListenerList + internal Dictionary>> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -61,8 +62,18 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.EventListeners.TryAdd(listener.EventType, [ listener ]); - this.EventListeners[listener.EventType].Add(listener); + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + this.EventListeners.TryAdd(listener.EventType, []); + } + + // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) + { + this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []); + } + + this.EventListeners[listener.EventType][listener.AddonName].Add(listener); } /// @@ -71,9 +82,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList)) + if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) { - listenerList.Remove(listener); + if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + { + addonListener.Remove(listener); + } } } @@ -86,22 +100,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { // Early return if we don't have any listeners of this type - if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return; + if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return; - // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in listenerList) + // Handle listeners for this event type that don't care which addon is triggering it + if (addonListeners.TryGetValue(string.Empty, out var globalListeners)) { - // Match on string.empty for listeners that want events for all addons. - if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) - continue; - - try + foreach (var listener in globalListeners) { - listener.FunctionDelegate.Invoke(eventType, args); + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener."); + } } - catch (Exception e) + } + + // Handle listeners that are listening for this addon and event type specifically + if (addonListeners.TryGetValue(args.AddonName, out var addonListener)) + { + foreach (var listener in addonListener) { - Log.Error(e, $"Exception in {blame} during {eventType} invoke."); + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}."); + } } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 73c4e540a..0f193556b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -2,7 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -57,35 +58,38 @@ public class AddonLifecycleWidget : IDataWindowWidget { if (!this.Ready) return; - foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners) + foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { - if (ImGui.CollapsingHeader(listenerType.ToString())) + using var eventId = ImRaii.PushId(eventType.ToString()); + + if (ImGui.CollapsingHeader(eventType.ToString())) { - ImGui.Indent(); + using var eventIndent = ImRaii.PushIndent(); - if (listeners.Count == 0) + if (addonListeners.Count == 0) { - ImGui.Text("No Listeners Registered for Event"u8); + ImGui.Text("No Addons Registered for Event"u8); } - if (ImGui.BeginTable("AddonLifecycleListenersTable"u8, 2)) + foreach (var (addonName, listeners) in addonListeners) { - ImGui.TableSetupColumn("##AddonName"u8, ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##MethodInvoke"u8, ImGuiTableColumnFlags.WidthStretch); + using var addonId = ImRaii.PushId(addonName); - foreach (var listener in listeners) + if (ImGui.CollapsingHeader(addonName.IsNullOrEmpty() ? "GLOBAL" : addonName)) { - ImGui.TableNextColumn(); - ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName); + using var addonIndent = ImRaii.PushIndent(); - ImGui.TableNextColumn(); - ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); + if (listeners.Count == 0) + { + ImGui.Text("No Listeners Registered for Event"u8); + } + + foreach (var listener in listeners) + { + ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); + } } - - ImGui.EndTable(); } - - ImGui.Unindent(); } } } From 170f6e08599d4853acc260aa3a442171d30a1731 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:11:13 -0800 Subject: [PATCH 08/11] Remove redundant header --- .../Windows/Data/Widgets/AddonLifecycleWidget.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 0f193556b..4fb13b81a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -46,18 +46,6 @@ public class AddonLifecycleWidget : IDataWindowWidget return; } - if (ImGui.CollapsingHeader("Listeners"u8)) - { - ImGui.Indent(); - this.DrawEventListeners(); - ImGui.Unindent(); - } - } - - private void DrawEventListeners() - { - if (!this.Ready) return; - foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { using var eventId = ImRaii.PushId(eventType.ToString()); From b8724f7a59b5cb3dd0b454dff384b7c0c7b0d355 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:44:35 -0800 Subject: [PATCH 09/11] Fix copy paste error --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index e38f56921..d3d0fcebe 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -37,7 +37,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. - /// + ///
/// Mapping is: EventType -> AddonName -> ListenerList internal Dictionary>> EventListeners { get; } = []; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 54c91248e..db698e626 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -167,7 +167,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); try { From c51e65e0bd07bc58bfab65128cb7719ce56c996b Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 10:08:40 -0800 Subject: [PATCH 10/11] Better unload --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 33 +++++-------------- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 15 +++------ 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index d3d0fcebe..5d121bea4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -19,13 +19,13 @@ namespace Dalamud.Game.Addon.Lifecycle; [ServiceManager.EarlyLoadedService] internal unsafe class AddonLifecycle : IInternalDisposableService { + /// + /// Gets a list of all allocated addon virtual tables. + /// + public static readonly List AllocatedTables = []; + private static readonly ModuleLog Log = new("AddonLifecycle"); - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - - private readonly Dictionary modifiedTables = []; - private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] @@ -47,13 +47,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.onInitializeAddonHook?.Dispose(); this.onInitializeAddonHook = null; - this.framework.RunOnFrameworkThread(() => - { - foreach (var virtualTable in this.modifiedTables.Values) - { - virtualTable.Dispose(); - } - }); + AllocatedTables.ForEach(entry => entry.Dispose()); + AllocatedTables.Clear(); } /// @@ -141,18 +136,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { this.LogInitialize(addon->NameString); - if (!this.modifiedTables.ContainsKey(addon->NameString)) - { - // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions - var managedVirtualTableEntry = new AddonVirtualTable(addon, this) - { - // This event is invoked when the game itself has disposed of an addon - // We can use this to know when to remove our virtual table entry - OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString), - }; - - this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry); - } + // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + AllocatedTables.Add(new AddonVirtualTable(addon, this)); } catch (Exception e) { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index db698e626..d91cd648f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Logging.Internal; @@ -108,17 +109,11 @@ internal unsafe class AddonVirtualTable : IDisposable 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; + // Ensure restoration is done atomically. + Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable); IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); } @@ -131,7 +126,7 @@ internal unsafe class AddonVirtualTable : IDisposable if ((freeFlags & 1) == 1) { IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); - this.OnAddonFinalized(); + AddonLifecycle.AllocatedTables.Remove(this); } return result; From 26f119096bad6fd3111a6c5f0ad977f53e396384 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 10:39:35 -0800 Subject: [PATCH 11/11] Bunch of stuff... --- Dalamud/Configuration/PluginConfigurations.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 31 ++----------------- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 9 ++++-- .../AddonArgTypes/AddonFinalizeArgs.cs | 7 +++-- .../AddonArgTypes/AddonGenericArgs.cs | 3 +- .../AddonArgTypes/AddonReceiveEventArgs.cs | 18 +++-------- .../AddonArgTypes/AddonRefreshArgs.cs | 15 +++------ .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 11 +------ .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 15 +++------ .../AddonArgTypes/AddonUpdateArgs.cs | 26 +++++++--------- .../AddonLifecycleAddressResolver.cs | 2 +- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 14 --------- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 2 +- Dalamud/Interface/Animation/Easing.cs | 2 +- ...ToDoAttribute.cs => Api14ToDoAttribute.cs} | 6 ++-- Dalamud/Utility/Api15ToDoAttribute.cs | 25 +++++++++++++++ Dalamud/Utility/Util.cs | 2 +- 17 files changed, 74 insertions(+), 116 deletions(-) rename Dalamud/Utility/{Api13ToDoAttribute.cs => Api14ToDoAttribute.cs} (75%) create mode 100644 Dalamud/Utility/Api15ToDoAttribute.cs diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index fa2969d31..c01ab2af0 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -11,7 +11,7 @@ namespace Dalamud.Configuration; /// /// Configuration to store settings for a dalamud plugin. /// -[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] +[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 0b2ae1178..62ca47238 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -33,41 +33,14 @@ public abstract class AddonArgs /// public abstract AddonArgsType Type { get; } - /// - /// Checks if addon name matches the given span of char. - /// - /// The name to check. - /// Whether it is the case. - internal bool IsAddon(string name) - { - if (this.Addon.IsNull) - return false; - - if (name.Length is 0 or > 32) - return false; - - if (string.IsNullOrEmpty(this.Addon.Name)) - return false; - - return name == this.Addon.Name; - } - - /// - /// Clears this AddonArgs values. - /// - internal virtual void Clear() - { - this.addonName = null; - this.Addon = 0; - } - /// /// Helper method for ensuring the name of the addon is valid. /// /// The name of the addon for this object. when invalid. private string GetAddonName() { - if (this.Addon.IsNull) return InvalidAddon; + if (this.Addon.IsNull) + return InvalidAddon; var name = this.Addon.Name; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 7254ba7b3..a834d2983 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -1,15 +1,18 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Utility; + +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Draw events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonDrawArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonDrawArgs() + internal AddonDrawArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index 12def3ad3..11d15a081 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -1,15 +1,18 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonFinalizeArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonFinalizeArgs() + internal AddonFinalizeArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs index f3078af69..a20e9d23b 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs @@ -8,8 +8,7 @@ public class AddonGenericArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonGenericArgs() + internal AddonGenericArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 05f51b118..bb8168075 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// @@ -8,8 +10,7 @@ public class AddonReceiveEventArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonReceiveEventArgs() + internal AddonReceiveEventArgs() { } @@ -32,17 +33,8 @@ public class AddonReceiveEventArgs : AddonArgs public nint AtkEvent { get; set; } /// - /// Gets or sets the pointer to a block of data for this event message. + /// Gets or sets the pointer to an AtkEventData for this event message. /// + [Api14ToDo("Rename to AtkEventData")] public nint Data { get; set; } - - /// - internal override void Clear() - { - base.Clear(); - 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 c01c065c1..8af017318 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -10,8 +12,7 @@ public class AddonRefreshArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRefreshArgs() + internal AddonRefreshArgs() { } @@ -31,13 +32,7 @@ public class AddonRefreshArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// + [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] + [Api15ToDo("Remove this")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - - /// - internal override void Clear() - { - base.Clear(); - 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 bf00c5d6e..7005b77c2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -8,8 +8,7 @@ public class AddonRequestedUpdateArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRequestedUpdateArgs() + internal AddonRequestedUpdateArgs() { } @@ -25,12 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs /// Gets or sets the StringArrayData** for this event. /// public nint StringArrayData { get; set; } - - /// - internal override void Clear() - { - base.Clear(); - 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 9b7e86a61..9fd7b6dd0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -10,8 +12,7 @@ public class AddonSetupArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonSetupArgs() + internal AddonSetupArgs() { } @@ -31,13 +32,7 @@ public class AddonSetupArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// + [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] + [Api15ToDo("Remove this")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - - /// - internal override void Clear() - { - base.Clear(); - 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 bab62fc89..e6147d0eb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -1,39 +1,35 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonUpdateArgs() + internal AddonUpdateArgs() { } /// public override AddonArgsType Type => AddonArgsType.Update; - /// - /// Gets the time since the last update. - /// - public float TimeDelta - { - get => this.TimeDeltaInternal; - init => this.TimeDeltaInternal = value; - } - /// /// Gets or sets the time since the last update. /// internal float TimeDeltaInternal { get; set; } - /// - internal override void Clear() + /// + /// Gets the time since the last update. + /// + private float TimeDelta { - base.Clear(); - this.TimeDeltaInternal = 0; + get => this.TimeDeltaInternal; + init => this.TimeDeltaInternal = value; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 9359870a5..2fa3c5b91 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] +[Api14ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index d91cd648f..49ffdc7fb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -26,8 +26,6 @@ internal unsafe class AddonVirtualTable : IDisposable 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(); @@ -36,7 +34,6 @@ internal unsafe class AddonVirtualTable : IDisposable 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; @@ -136,7 +133,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonSetupArg.Clear(); this.addonSetupArg.Addon = addon; this.addonSetupArg.AtkValueCount = valueCount; this.addonSetupArg.AtkValues = (nint)values; @@ -160,7 +156,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); @@ -178,7 +173,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonDrawArg.Clear(); this.addonDrawArg.Addon = addon; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); @@ -198,7 +192,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonUpdateArg.Clear(); this.addonUpdateArg.Addon = addon; this.addonUpdateArg.TimeDeltaInternal = delta; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); @@ -221,7 +214,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonRefreshArg.Clear(); this.addonRefreshArg.Addon = addon; this.addonRefreshArg.AtkValueCount = valueCount; this.addonRefreshArg.AtkValues = (nint)values; @@ -246,7 +238,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonRequestedUpdateArg.Clear(); this.addonRequestedUpdateArg.Addon = addon; this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; @@ -270,7 +261,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonReceiveEventArg.Clear(); this.addonReceiveEventArg.Addon = (nint)addon; this.addonReceiveEventArg.AtkEventType = (byte)eventType; this.addonReceiveEventArg.EventParam = eventParam; @@ -300,7 +290,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); @@ -324,7 +313,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); @@ -346,7 +334,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); @@ -366,7 +353,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index f5b7011fe..af85f9228 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry } /// - [Api13ToDo("Maybe make this config scoped to internal name?")] + [Api14ToDo("Maybe make this config scoped to internal name?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index 0d2057b3b..cc1f48ce7 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -48,7 +48,7 @@ public abstract class Easing /// Gets the current value of the animation, following unclamped logic. /// [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] - [Api13ToDo("Map this field to ValueClamped, probably.")] + [Api14ToDo("Map this field to ValueClamped, probably.")] public double Value => this.ValueUnclamped; /// diff --git a/Dalamud/Utility/Api13ToDoAttribute.cs b/Dalamud/Utility/Api14ToDoAttribute.cs similarity index 75% rename from Dalamud/Utility/Api13ToDoAttribute.cs rename to Dalamud/Utility/Api14ToDoAttribute.cs index 576401cda..945b6e4db 100644 --- a/Dalamud/Utility/Api13ToDoAttribute.cs +++ b/Dalamud/Utility/Api14ToDoAttribute.cs @@ -4,7 +4,7 @@ namespace Dalamud.Utility; /// Utility class for marking something to be changed for API 13, for ease of lookup. /// [AttributeUsage(AttributeTargets.All, Inherited = false)] -internal sealed class Api13ToDoAttribute : Attribute +internal sealed class Api14ToDoAttribute : Attribute { /// /// Marks that this should be made internal. @@ -12,11 +12,11 @@ internal sealed class Api13ToDoAttribute : Attribute public const string MakeInternal = "Make internal."; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The explanation. /// The explanation 2. - public Api13ToDoAttribute(string what, string what2 = "") + public Api14ToDoAttribute(string what, string what2 = "") { _ = what; _ = what2; diff --git a/Dalamud/Utility/Api15ToDoAttribute.cs b/Dalamud/Utility/Api15ToDoAttribute.cs new file mode 100644 index 000000000..646c260e8 --- /dev/null +++ b/Dalamud/Utility/Api15ToDoAttribute.cs @@ -0,0 +1,25 @@ +namespace Dalamud.Utility; + +/// +/// Utility class for marking something to be changed for API 13, for ease of lookup. +/// Intended to represent not the upcoming API, but the one after it for more major changes. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +internal sealed class Api15ToDoAttribute : Attribute +{ + /// + /// Marks that this should be made internal. + /// + public const string MakeInternal = "Make internal."; + + /// + /// Initializes a new instance of the class. + /// + /// The explanation. + /// The explanation 2. + public Api15ToDoAttribute(string what, string what2 = "") + { + _ = what; + _ = what2; + } +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 2a3733303..ba31f47e5 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -79,7 +79,7 @@ public static partial class Util /// /// Gets the Dalamud version. /// - [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] + [Api14ToDo("Remove. Make both versions here internal. Add an API somewhere.")] public static string AssemblyVersion { get; } = Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString();