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/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..c4a7e8f53 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -5,19 +5,24 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Base class for AddonLifecycle AddonArgTypes. /// -public abstract unsafe class AddonArgs +public class AddonArgs { /// /// Constant string representing the name of an addon that is invalid. /// public const string InvalidAddon = "NullAddon"; - private string? addonName; + /// + /// Initializes a new instance of the class. + /// + internal AddonArgs() + { + } /// /// Gets the name of the addon this args referrers to. /// - public string AddonName => this.GetAddonName(); + public string AddonName { get; private set; } = InvalidAddon; /// /// Gets the pointer to the addons AtkUnitBase. @@ -25,55 +30,17 @@ public abstract unsafe class AddonArgs public AtkUnitBasePtr Addon { get; - internal set; + internal set + { + field = value; + + if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name)) + this.AddonName = value.Name; + } } /// /// Gets the type of these args. /// - 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; - - var name = this.Addon.Name; - - if (string.IsNullOrEmpty(name)) - return InvalidAddon; - - return this.addonName ??= name; - } + public virtual AddonArgsType Type => AddonArgsType.Generic; } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs new file mode 100644 index 000000000..db3e442f8 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Close events. +/// +public class AddonCloseArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonCloseArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Close; + + /// + /// Gets or sets a value indicating whether the window should fire the callback method on close. + /// + public bool FireCallback { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs deleted file mode 100644 index 989e11912..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for Draw events. -/// -public class AddonDrawArgs : AddonArgs, ICloneable -{ - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Not intended for public construction.", false)] - public AddonDrawArgs() - { - } - - /// - 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 deleted file mode 100644 index d9401b414..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for ReceiveEvent events. -/// -public class AddonFinalizeArgs : AddonArgs, ICloneable -{ - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Not intended for public construction.", false)] - public AddonFinalizeArgs() - { - } - - /// - 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/AddonHideArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs new file mode 100644 index 000000000..3e3521bd0 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs @@ -0,0 +1,32 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Hide events. +/// +public class AddonHideArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonHideArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Hide; + + /// + /// Gets or sets a value indicating whether to call the hide callback handler when this hides. + /// + public bool CallHideCallback { get; set; } + + /// + /// Gets or sets the flags that the window will set when it Shows/Hides. + /// + public uint SetShowHideFlags { get; set; } + + /// + /// Gets or sets a value indicating whether something for this event message. + /// + internal bool UnknownBool { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 980fe4f2f..785cd199f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -3,13 +3,12 @@ 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. /// - [Obsolete("Not intended for public construction.", false)] - public AddonReceiveEventArgs() + internal AddonReceiveEventArgs() { } @@ -32,23 +31,7 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable 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. /// - 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; - } + public nint AtkEventData { get; set; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index d28631c3c..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; @@ -5,13 +7,12 @@ 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. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRefreshArgs() + internal AddonRefreshArgs() { } @@ -31,19 +32,7 @@ public class AddonRefreshArgs : AddonArgs, ICloneable /// /// 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); - - /// - public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - - /// - internal override void Clear() - { - base.Clear(); - this.AtkValueCount = default; - this.AtkValues = default; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index e87a980fd..7005b77c2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -3,13 +3,12 @@ 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. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRequestedUpdateArgs() + internal AddonRequestedUpdateArgs() { } @@ -25,18 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable /// Gets or sets the StringArrayData** for this event. /// 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; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 0dd9ecee2..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; @@ -5,13 +7,12 @@ 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. /// - [Obsolete("Not intended for public construction.", false)] - public AddonSetupArgs() + internal AddonSetupArgs() { } @@ -31,19 +32,7 @@ public class AddonSetupArgs : AddonArgs, ICloneable /// /// 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); - - /// - public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - - /// - internal override void Clear() - { - base.Clear(); - this.AtkValueCount = default; - this.AtkValues = default; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs new file mode 100644 index 000000000..3153d1208 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Show events. +/// +public class AddonShowArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonShowArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Show; + + /// + /// Gets or sets a value indicating whether the window should play open sound effects. + /// + public bool SilenceOpenSoundEffect { get; set; } + + /// + /// Gets or sets the flags that the window will unset when it Shows/Hides. + /// + public uint UnsetShowHideFlags { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs deleted file mode 100644 index a263f6ae4..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for Update events. -/// -public class AddonUpdateArgs : AddonArgs, ICloneable -{ - /// - /// Initializes a new instance of the class. - /// - [Obsolete("Not intended for public construction.", false)] - public 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; } - - /// - public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - - /// - internal override void Clear() - { - base.Clear(); - this.TimeDeltaInternal = default; - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index b58b5f4c7..46ee479ac 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -5,38 +5,43 @@ /// public enum AddonArgsType { + /// + /// Generic arg type that contains no meaningful data. + /// + Generic, + /// /// 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, + + /// + /// Contains argument data for Show. + /// + Show, + + /// + /// Contains argument data for Hide. + /// + Hide, + + /// + /// Contains argument data for Close. + /// + Close, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 5fd0ac964..3b9c6e867 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 @@ -29,7 +29,6 @@ public enum AddonEvent /// An event that is fired before an addon begins its update cycle via . This event /// is fired every frame that an addon is loaded, regardless of visibility. /// - /// PreUpdate, /// @@ -42,7 +41,6 @@ public enum AddonEvent /// An event that is fired before an addon begins drawing to screen via . Unlike /// , this event is only fired if an addon is visible or otherwise drawing to screen. /// - /// PreDraw, /// @@ -62,9 +60,8 @@ public enum AddonEvent ///
/// As this is part of the destruction process for an addon, this event does not have an associated Post event. /// - /// PreFinalize, - + /// /// An event that is fired before a call to is made in response to a /// change in the subscribed or @@ -81,13 +78,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 +93,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 +109,98 @@ 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, + + /// + /// An event that is fired before an addon processes its OnMove method. + /// OnMove is triggered only when a move is completed. + /// + PreMove, + + /// + /// An event that is fired after an addon has processed its OnMove method. + /// OnMove is triggered only when a move is completed. + /// + PostMove, + + /// + /// An event that is fired before an addon processes its MouseOver method. + /// + PreMouseOver, + + /// + /// An event that is fired after an addon has processed its MouseOver method. + /// + PostMouseOver, + + /// + /// An event that is fired before an addon processes its MouseOut method. + /// + PreMouseOut, + + /// + /// An event that is fired after an addon has processed its MouseOut method. + /// + PostMouseOut, + + /// + /// An event that is fired before an addon processes its Focus method. + /// + /// + /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows. + /// + PreFocus, + + /// + /// An event that is fired after an addon has processed its Focus method. + /// + /// + /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows. + /// + PostFocus, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index b44ab8764..716ce1bfb 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; @@ -21,75 +19,36 @@ 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(); - - [ServiceManager.ServiceDependency] - private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); - - 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) + private AddonLifecycle() { - this.address = new AddonLifecycleAddressResolver(); - this.address.Setup(sigScanner); - - 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(); + this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); + this.onInitializeAddonHook.Enable(); } - 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(); + ///
+ /// Mapping is: EventType -> AddonName -> ListenerList + internal Dictionary>> 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) - { - receiveEventListener.Dispose(); - } + AllocatedTables.ForEach(entry => entry.Dispose()); + AllocatedTables.Clear(); } /// @@ -98,20 +57,20 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnTick(() => + if (!this.EventListeners.ContainsKey(listener.EventType)) { - 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(); - } - } - }); + if (!this.EventListeners.TryAdd(listener.EventType, [])) + return; + } + + // 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)) + { + if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, [])) + return; + } + + this.EventListeners[listener.EventType][listener.AddonName].Add(listener); } /// @@ -120,27 +79,13 @@ 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 addonListeners)) { - 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 }) + if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) { - // 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(); - } - } + addonListener.Remove(listener); } - }); + } } /// @@ -151,226 +96,63 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// What to blame on errors. internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { - // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in this.EventListeners) + // Early return if we don't have any listeners of this type + if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return; + + // Handle listeners for this event type that don't care which addon is triggering it + if (addonListeners.TryGetValue(string.Empty, out var globalListeners)) { - 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; - - try + foreach (var listener in globalListeners) { - listener.FunctionDelegate.Invoke(eventType, args); - } - catch (Exception e) - { - Log.Error(e, $"Exception in {blame} during {eventType} invoke."); - } - } - } - - private void RegisterReceiveEventHook(AtkUnitBase* addon) - { - // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener. - // Disallows hooking the core internal event handler. - var addonName = addon->NameString; - var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent; - if (receiveEventAddress != this.disallowedReceiveEventAddress) - { - // If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler. - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener) - { - if (!existingListener.AddonNames.Contains(addonName)) + try { - existingListener.AddonNames.Add(addonName); + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener."); } } + } - // Else, we have an addon that we don't have the ReceiveEvent for yet, make it. - else + // 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) { - 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) + try { - receiveEventListener.TryEnable(); + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}."); } } } } - private void UnregisterReceiveEventHook(string addonName) + private void OnAddonInitialize(AtkUnitBase* addon) { - // Remove this addons ReceiveEvent Registration - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener) + try { - eventListener.AddonNames.Remove(addonName); + this.LogInitialize(addon->NameString); - // If there are no more listeners let's remove and dispose. - if (eventListener.AddonNames.Count is 0) - { - this.ReceiveEventListeners.Remove(eventListener); - eventListener.Dispose(); - } + // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + AllocatedTables.Add(new AddonVirtualTable(addon, this)); } + catch (Exception e) + { + Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize."); + } + + this.onInitializeAddonHook!.Original(addon); } - private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) + [Conditional("DEBUG")] + private void LogInitialize(string addonName) { - 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 +169,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 +240,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 deleted file mode 100644 index bc9e4b639..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Dalamud.Plugin.Services; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// AddonLifecycleService memory address resolver. -/// -internal unsafe 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/AddonLifecycleEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs index 9d411cdbc..fc82e0582 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs @@ -25,17 +25,12 @@ internal class AddonLifecycleEventListener /// string.Empty if it wants to be called for any addon. /// public string AddonName { get; init; } - - /// - /// Gets or sets a value indicating whether this event has been unregistered. - /// - public bool Removed { get; set; } - + /// /// Gets the event type this listener is looking for. /// public AddonEvent EventType { get; init; } - + /// /// Gets the delegate this listener invokes. /// 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..47ff92c3d --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -0,0 +1,638 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +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 EnableLogging = false; + + private static readonly ModuleLog Log = new("LifecycleVT"); + + private readonly AddonLifecycle lifecycleService; + + // Each addon gets its own set of args that are used to mutate the original call when used in pre-calls + private readonly AddonSetupArgs setupArgs = new(); + private readonly AddonArgs finalizeArgs = new(); + private readonly AddonArgs drawArgs = new(); + private readonly AddonArgs updateArgs = new(); + private readonly AddonRefreshArgs refreshArgs = new(); + private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new(); + private readonly AddonReceiveEventArgs receiveEventArgs = new(); + private readonly AddonArgs openArgs = new(); + private readonly AddonCloseArgs closeArgs = new(); + private readonly AddonShowArgs showArgs = new(); + private readonly AddonHideArgs hideArgs = new(); + private readonly AddonArgs onMoveArgs = new(); + private readonly AddonArgs onMouseOverArgs = new(); + private readonly AddonArgs onMouseOutArgs = new(); + private readonly AddonArgs focusArgs = new(); + + 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; + private readonly AtkUnitBase.Delegates.OnMove onMoveFunction; + private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction; + private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction; + private readonly AtkUnitBase.Delegates.Focus focusFunction; + + /// + /// 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; + this.onMoveFunction = this.OnAddonMove; + this.onMouseOverFunction = this.OnAddonMouseOver; + this.onMouseOutFunction = this.OnAddonMouseOut; + this.focusFunction = this.OnAddonFocus; + + // 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); + this.modifiedVirtualTable->OnMove = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction); + this.modifiedVirtualTable->OnMouseOver = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction); + this.modifiedVirtualTable->OnMouseOut = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); + this.modifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); + } + + /// + public void Dispose() + { + // Ensure restoration is done atomically. + Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable); + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + } + + private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) + { + AtkEventListener* result = null; + + try + { + this.LogEvent(EnableLogging); + + try + { + result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Dtor. This may be a bug in the game or another plugin hooking this method."); + } + + if ((freeFlags & 1) == 1) + { + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + AddonLifecycle.AllocatedTables.Remove(this); + } + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDestructor."); + } + + return result; + } + + private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + try + { + this.LogEvent(EnableLogging); + + this.setupArgs.Addon = addon; + this.setupArgs.AtkValueCount = valueCount; + this.setupArgs.AtkValues = (nint)values; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs); + + valueCount = this.setupArgs.AtkValueCount; + values = (AtkValue*)this.setupArgs.AtkValues; + + try + { + this.originalVirtualTable->OnSetup(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnSetup. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonSetup."); + } + } + + private void OnAddonFinalize(AtkUnitBase* thisPtr) + { + try + { + this.LogEvent(EnableLogging); + + this.finalizeArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs); + + try + { + this.originalVirtualTable->Finalizer(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Finalizer. This may be a bug in the game or another plugin hooking this method."); + } + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFinalize."); + } + } + + private void OnAddonDraw(AtkUnitBase* addon) + { + try + { + this.LogEvent(EnableLogging); + + this.drawArgs.Addon = addon; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs); + + try + { + this.originalVirtualTable->Draw(addon); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Draw. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDraw."); + } + } + + private void OnAddonUpdate(AtkUnitBase* addon, float delta) + { + try + { + this.LogEvent(EnableLogging); + + this.updateArgs.Addon = addon; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs); + + // Note: Do not pass or allow manipulation of delta. + // It's realistically not something that should be needed. + // And even if someone does, they are encouraged to hook Update themselves. + + try + { + this.originalVirtualTable->Update(addon, delta); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Update. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.updateArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonUpdate."); + } + } + + private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + var result = false; + + try + { + this.LogEvent(EnableLogging); + + this.refreshArgs.Addon = addon; + this.refreshArgs.AtkValueCount = valueCount; + this.refreshArgs.AtkValues = (nint)values; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs); + + valueCount = this.refreshArgs.AtkValueCount; + values = (AtkValue*)this.refreshArgs.AtkValues; + + try + { + result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnRefresh. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonRefresh."); + } + + return result; + } + + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + try + { + this.LogEvent(EnableLogging); + + this.requestedUpdateArgs.Addon = addon; + this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData; + this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs); + + numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData; + stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData; + + try + { + this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnRequestedUpdate."); + } + } + + private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) + { + try + { + this.LogEvent(EnableLogging); + + this.receiveEventArgs.Addon = (nint)addon; + this.receiveEventArgs.AtkEventType = (byte)eventType; + this.receiveEventArgs.EventParam = eventParam; + this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent; + this.receiveEventArgs.AtkEventData = (nint)atkEventData; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs); + + eventType = (AtkEventType)this.receiveEventArgs.AtkEventType; + eventParam = this.receiveEventArgs.EventParam; + atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent; + atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData; + + try + { + this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon ReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonReceiveEvent."); + } + } + + private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) + { + var result = false; + + try + { + this.LogEvent(EnableLogging); + + this.openArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs); + + try + { + result = this.originalVirtualTable->Open(thisPtr, depthLayer); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Open. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonOpen."); + } + + return result; + } + + private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) + { + var result = false; + + try + { + this.LogEvent(EnableLogging); + + this.closeArgs.Addon = thisPtr; + this.closeArgs.FireCallback = fireCallback; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs); + + fireCallback = this.closeArgs.FireCallback; + + try + { + result = this.originalVirtualTable->Close(thisPtr, fireCallback); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Close. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonClose."); + } + + return result; + } + + private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) + { + try + { + this.LogEvent(EnableLogging); + + this.showArgs.Addon = thisPtr; + this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect; + this.showArgs.UnsetShowHideFlags = unsetShowHideFlags; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs); + + silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect; + unsetShowHideFlags = this.showArgs.UnsetShowHideFlags; + + try + { + this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Show. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.showArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonShow."); + } + } + + private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) + { + try + { + this.LogEvent(EnableLogging); + + this.hideArgs.Addon = thisPtr; + this.hideArgs.UnknownBool = unkBool; + this.hideArgs.CallHideCallback = callHideCallback; + this.hideArgs.SetShowHideFlags = setShowHideFlags; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs); + + unkBool = this.hideArgs.UnknownBool; + callHideCallback = this.hideArgs.CallHideCallback; + setShowHideFlags = this.hideArgs.SetShowHideFlags; + + try + { + this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Hide. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonHide."); + } + } + + private void OnAddonMove(AtkUnitBase* thisPtr) + { + try + { + this.LogEvent(EnableLogging); + + this.onMoveArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs); + + try + { + this.originalVirtualTable->OnMove(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnMove. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMove."); + } + } + + private void OnAddonMouseOver(AtkUnitBase* thisPtr) + { + try + { + this.LogEvent(EnableLogging); + + this.onMouseOverArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs); + + try + { + this.originalVirtualTable->OnMouseOver(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnMouseOver. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOver."); + } + } + + private void OnAddonMouseOut(AtkUnitBase* thisPtr) + { + try + { + this.LogEvent(EnableLogging); + + this.onMouseOutArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs); + + try + { + this.originalVirtualTable->OnMouseOut(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnMouseOut. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOut."); + } + } + + private void OnAddonFocus(AtkUnitBase* thisPtr) + { + try + { + this.LogEvent(EnableLogging); + + this.focusArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs); + + try + { + this.originalVirtualTable->Focus(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Focus. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs); + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocus."); + } + } + + [Conditional("DEBUG")] + private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") + { + if (loggingEnabled) + { + // 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}"); + } + } +} 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/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/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/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index b58166e89..4fb13b81a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,11 +1,9 @@ -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; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -48,97 +46,38 @@ public class AddonLifecycleWidget : IDataWindowWidget return; } - if (ImGui.CollapsingHeader("Listeners"u8)) + foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { - ImGui.Indent(); - this.DrawEventListeners(); - ImGui.Unindent(); - } + using var eventId = ImRaii.PushId(eventType.ToString()); - if (ImGui.CollapsingHeader("ReceiveEvent Hooks"u8)) - { - ImGui.Indent(); - this.DrawReceiveEventHooks(); - ImGui.Unindent(); - } - } - - private void DrawEventListeners() - { - if (!this.Ready) return; - - foreach (var eventType in Enum.GetValues()) - { if (ImGui.CollapsingHeader(eventType.ToString())) { - ImGui.Indent(); - var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList(); + 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(); - } - } - } - - 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); } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index 3ad8f86c2..f3e25caf8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Game; -using Dalamud.Game.Addon.Lifecycle; using Dalamud.Hooking; using FFXIVClientStructs.FFXIV.Component.GUI; using Serilog; @@ -34,7 +33,7 @@ internal unsafe class HookWidget : IDataWindowWidget private MessageBoxWDelegate? messageBoxWOriginal; private AddonFinalizeDelegate? addonFinalizeOriginal; - private AddonLifecycleAddressResolver? address; + private nint address; private delegate int MessageBoxWDelegate( IntPtr hWnd, @@ -55,7 +54,7 @@ internal unsafe class HookWidget : IDataWindowWidget public string DisplayName { get; init; } = "Hook"; /// - public string[]? CommandShortcuts { get; init; } = { "hook" }; + public string[]? CommandShortcuts { get; init; } = ["hook"]; /// public bool Ready { get; set; } @@ -65,8 +64,8 @@ internal unsafe class HookWidget : IDataWindowWidget { this.Ready = true; - this.address = new AddonLifecycleAddressResolver(); - this.address.Setup(Service.Get()); + var sigScanner = Service.Get(); + this.address = sigScanner.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); } /// @@ -224,7 +223,7 @@ internal unsafe class HookWidget : IDataWindowWidget private IDalamudHook HookAddonFinalize() { - var hook = Hook.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize); + var hook = Hook.FromAddress(this.address, this.OnAddonFinalize); this.addonFinalizeOriginal = hook.Original; hook.Enable(); diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 69cdc4d28..482a00153 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -472,9 +472,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable private unsafe void OnVersionStringDraw(AddonEvent ev, AddonArgs args) { - if (args is not AddonDrawArgs drawArgs) return; + if (ev is not (AddonEvent.PostDraw or AddonEvent.PreDraw)) return; - var addon = drawArgs.Addon.Struct; + var addon = args.Addon.Struct; var textNode = addon->GetTextNodeById(3); // look and feel init. should be harmless to set. 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 bde113904..e273decac 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(); diff --git a/Directory.Build.props b/Directory.Build.props index eabb727e8..3897256bf 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ net10.0-windows x64 x64 - 13.0 + 14.0 diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs index 665fa434f..3cf20bb30 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs @@ -238,7 +238,7 @@ public static unsafe partial class ImGui ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vSpeed, vMin, vMax, @@ -251,7 +251,7 @@ public static unsafe partial class ImGui ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vSpeed, vMin, vMax, @@ -264,7 +264,7 @@ public static unsafe partial class ImGui ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vSpeed, vMin, vMax, diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs index fb86096ff..5881ac462 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs @@ -205,7 +205,7 @@ public static unsafe partial class ImGui InputScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref data)), + MemoryMarshal.Cast(new Span(ref data)), step, stepFast, format.MoveOrDefault("%.3f"u8), @@ -219,7 +219,7 @@ public static unsafe partial class ImGui InputScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref data)), + MemoryMarshal.Cast(new Span(ref data)), step, stepFast, format.MoveOrDefault("%.3f"u8), @@ -233,7 +233,7 @@ public static unsafe partial class ImGui InputScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref data)), + MemoryMarshal.Cast(new Span(ref data)), step, stepFast, format.MoveOrDefault("%.3f"u8), diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs index 20ee78ab6..b0c4b7c79 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs @@ -210,7 +210,7 @@ public static unsafe partial class ImGui ImU8String format = default, ImGuiSliderFlags flags = ImGuiSliderFlags.None) => SliderScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vMin, vMax, format.MoveOrDefault("%.3f"u8), @@ -222,7 +222,7 @@ public static unsafe partial class ImGui SliderScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vMin, vMax, format.MoveOrDefault("%.3f"u8), @@ -236,7 +236,7 @@ public static unsafe partial class ImGui SliderScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vMin, vMax, format.MoveOrDefault("%.3f"u8), diff --git a/imgui/Dalamud.Bindings.ImGui/ImU8String.cs b/imgui/Dalamud.Bindings.ImGui/ImU8String.cs index a62152c39..f2b635764 100644 --- a/imgui/Dalamud.Bindings.ImGui/ImU8String.cs +++ b/imgui/Dalamud.Bindings.ImGui/ImU8String.cs @@ -156,7 +156,7 @@ public ref struct ImU8String return this.rentedBuffer is { } buf ? buf.AsSpan() - : MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer))); + : MemoryMarshal.Cast(new Span(ref Unsafe.AsRef(ref this.fixedBuffer))); } } @@ -165,7 +165,7 @@ public ref struct ImU8String private ref byte FixedBufferByteRef => ref this.FixedBufferSpan[0]; private Span FixedBufferSpan => - MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer))); + MemoryMarshal.Cast(new Span(ref Unsafe.AsRef(ref this.fixedBuffer))); public static implicit operator ImU8String(ReadOnlySpan text) => new(text); public static implicit operator ImU8String(ReadOnlyMemory text) => new(text);