diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index c01ab2af0..fa2969d31 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. /// -[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.")] +[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.")] public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index d9f6ef172..54e25b6f2 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -192,8 +192,8 @@ public sealed class EntryPoint var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", - Versioning.GetScmVersion(), - Versioning.GetGitHashClientStructs(), + Util.GetScmVersion(), + Util.GetGitHashClientStructs(), FFXIVClientStructs.ThisAssembly.Git.Commits); dalamud.WaitForUnload(); diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs new file mode 100644 index 000000000..14def2036 --- /dev/null +++ b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs @@ -0,0 +1,107 @@ +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 c4a7e8f53..c008db08f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -5,24 +5,19 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Base class for AddonLifecycle AddonArgTypes. /// -public class AddonArgs +public abstract unsafe class AddonArgs { /// /// Constant string representing the name of an addon that is invalid. /// public const string InvalidAddon = "NullAddon"; - /// - /// Initializes a new instance of the class. - /// - internal AddonArgs() - { - } + private string? addonName; /// /// Gets the name of the addon this args referrers to. /// - public string AddonName { get; private set; } = InvalidAddon; + public string AddonName => this.GetAddonName(); /// /// Gets the pointer to the addons AtkUnitBase. @@ -30,17 +25,55 @@ public class AddonArgs public AtkUnitBasePtr Addon { get; - internal set - { - field = value; - - if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name)) - this.AddonName = value.Name; - } + internal set; } /// /// Gets the type of these args. /// - public virtual AddonArgsType Type => AddonArgsType.Generic; + 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; + } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs deleted file mode 100644 index db3e442f8..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 new file mode 100644 index 000000000..989e11912 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -0,0 +1,24 @@ +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 new file mode 100644 index 000000000..d9401b414 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -0,0 +1,24 @@ +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 deleted file mode 100644 index 3e3521bd0..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 785cd199f..980fe4f2f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -3,12 +3,13 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonReceiveEventArgs : AddonArgs +public class AddonReceiveEventArgs : AddonArgs, ICloneable { /// /// Initializes a new instance of the class. /// - internal AddonReceiveEventArgs() + [Obsolete("Not intended for public construction.", false)] + public AddonReceiveEventArgs() { } @@ -31,7 +32,23 @@ public class AddonReceiveEventArgs : AddonArgs public nint AtkEvent { get; set; } /// - /// Gets or sets the pointer to an AtkEventData for this event message. + /// Gets or sets the pointer to a block of data for this event message. /// - public nint AtkEventData { get; set; } + 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; + } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index 4fc81632a..d28631c3c 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,22 +1,17 @@ -using System.Collections.Generic; - -using Dalamud.Game.NativeWrapper; -using Dalamud.Utility; - using FFXIVClientStructs.FFXIV.Component.GUI; -using FFXIVClientStructs.Interop; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Refresh events. /// -public class AddonRefreshArgs : AddonArgs +public class AddonRefreshArgs : AddonArgs, ICloneable { /// /// Initializes a new instance of the class. /// - internal AddonRefreshArgs() + [Obsolete("Not intended for public construction.", false)] + public AddonRefreshArgs() { } @@ -36,30 +31,19 @@ public class AddonRefreshArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// - [Obsolete("Pending removal, Use AtkValueEnumerable instead.")] - [Api15ToDo("Make this internal, remove obsolete")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - /// - /// Gets an enumerable collection of of the event's AtkValues. - /// - /// - /// An of corresponding to the event's AtkValues. - /// - public IEnumerable AtkValueEnumerable - { - get - { - for (var i = 0; i < this.AtkValueCount; i++) - { - AtkValuePtr ptr; - unsafe - { - ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i)); - } + /// + public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); - yield return ptr; - } - } + /// + 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 7005b77c2..e87a980fd 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -3,12 +3,13 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for OnRequestedUpdate events. /// -public class AddonRequestedUpdateArgs : AddonArgs +public class AddonRequestedUpdateArgs : AddonArgs, ICloneable { /// /// Initializes a new instance of the class. /// - internal AddonRequestedUpdateArgs() + [Obsolete("Not intended for public construction.", false)] + public AddonRequestedUpdateArgs() { } @@ -24,4 +25,18 @@ public class AddonRequestedUpdateArgs : AddonArgs /// 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 e0b2defbf..0dd9ecee2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,22 +1,17 @@ -using System.Collections.Generic; - -using Dalamud.Game.NativeWrapper; -using Dalamud.Utility; - using FFXIVClientStructs.FFXIV.Component.GUI; -using FFXIVClientStructs.Interop; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Setup events. /// -public class AddonSetupArgs : AddonArgs +public class AddonSetupArgs : AddonArgs, ICloneable { /// /// Initializes a new instance of the class. /// - internal AddonSetupArgs() + [Obsolete("Not intended for public construction.", false)] + public AddonSetupArgs() { } @@ -36,30 +31,19 @@ public class AddonSetupArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// - [Obsolete("Pending removal, Use AtkValueEnumerable instead.")] - [Api15ToDo("Make this internal, remove obsolete")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - /// - /// Gets an enumerable collection of of the event's AtkValues. - /// - /// - /// An of corresponding to the event's AtkValues. - /// - public IEnumerable AtkValueEnumerable - { - get - { - for (var i = 0; i < this.AtkValueCount; i++) - { - AtkValuePtr ptr; - unsafe - { - ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i)); - } + /// + public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); - yield return ptr; - } - } + /// + 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 deleted file mode 100644 index 3153d1208..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 new file mode 100644 index 000000000..a263f6ae4 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -0,0 +1,45 @@ +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 46ee479ac..b58b5f4c7 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -5,43 +5,38 @@ /// 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 3b9c6e867..5fd0ac964 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,6 +29,7 @@ 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, /// @@ -41,6 +42,7 @@ 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, /// @@ -60,8 +62,9 @@ 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 @@ -78,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 @@ -93,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 @@ -109,98 +112,10 @@ 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 716ce1bfb..b44ab8764 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,14 +1,16 @@ using System.Collections.Generic; -using System.Diagnostics; +using System.Linq; 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; @@ -19,36 +21,75 @@ 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"); - private Hook? onInitializeAddonHook; + [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; [ServiceManager.ServiceConstructor] - private AddonLifecycle() + private AddonLifecycle(TargetSigScanner sigScanner) { - this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); - this.onInitializeAddonHook.Enable(); + 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(); } + 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. - ///
- /// Mapping is: EventType -> AddonName -> ListenerList - internal Dictionary>> EventListeners { get; } = []; + ///
+ internal List EventListeners { get; } = new(); /// void IInternalDisposableService.DisposeService() { - this.onInitializeAddonHook?.Dispose(); - this.onInitializeAddonHook = null; + this.onAddonSetupHook.Dispose(); + this.onAddonFinalizeHook.Dispose(); + this.onAddonDrawHook.Dispose(); + this.onAddonUpdateHook.Dispose(); + this.onAddonRefreshHook.Dispose(); + this.onAddonRequestedUpdateHook.Dispose(); - AllocatedTables.ForEach(entry => entry.Dispose()); - AllocatedTables.Clear(); + foreach (var receiveEventListener in this.ReceiveEventListeners) + { + receiveEventListener.Dispose(); + } } /// @@ -57,20 +98,20 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - if (!this.EventListeners.ContainsKey(listener.EventType)) + this.framework.RunOnTick(() => { - 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); + 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(); + } + } + }); } /// @@ -79,13 +120,27 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) + // 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 (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + 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 }) { - addonListener.Remove(listener); + // 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(); + } + } } - } + }); } /// @@ -96,63 +151,226 @@ 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 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)) + // 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 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 { - try - { - listener.FunctionDelegate.Invoke(eventType, args); - } - catch (Exception e) - { - Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener."); - } + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke."); } } + } - // Handle listeners that are listening for this addon and event type specifically - if (addonListeners.TryGetValue(args.AddonName, out var addonListener)) + 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) { - foreach (var listener in addonListener) + // 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) { - try + if (!existingListener.AddonNames.Contains(addonName)) { - listener.FunctionDelegate.Invoke(eventType, args); + existingListener.AddonNames.Add(addonName); } - catch (Exception e) + } + + // 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) { - Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}."); + receiveEventListener.TryEnable(); } } } } - private void OnAddonInitialize(AtkUnitBase* addon) + private void UnregisterReceiveEventHook(string addonName) + { + // Remove this addons ReceiveEvent Registration + if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener) + { + eventListener.AddonNames.Remove(addonName); + + // If there are no more listeners let's remove and dispose. + if (eventListener.AddonNames.Count is 0) + { + this.ReceiveEventListeners.Remove(eventListener); + eventListener.Dispose(); + } + } + } + + private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { try { - this.LogInitialize(addon->NameString); - - // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions - AllocatedTables.Add(new AddonVirtualTable(addon, this)); + this.RegisterReceiveEventHook(addon); } catch (Exception e) { - Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize."); + Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); } - this.onInitializeAddonHook!.Original(addon); + 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); } - [Conditional("DEBUG")] - private void LogInitialize(string addonName) + private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { - Log.Debug($"Initializing {addonName}"); + try + { + var addonName = atkUnitBase[0]->NameString; + this.UnregisterReceiveEventHook(addonName); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); + } + + using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg); + arg.Clear(); + arg.Addon = (nint)atkUnitBase[0]; + this.InvokeListenersSafely(AddonEvent.PreFinalize, arg); + + try + { + this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); + } + } + + private void OnAddonDraw(AtkUnitBase* addon) + { + using var returner = this.argsPool.Rent(out AddonDrawArgs arg); + arg.Clear(); + arg.Addon = (nint)addon; + this.InvokeListenersSafely(AddonEvent.PreDraw, arg); + + try + { + addon->Draw(); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); + } + + this.InvokeListenersSafely(AddonEvent.PostDraw, arg); + } + + private void OnAddonUpdate(AtkUnitBase* addon, float delta) + { + using var returner = this.argsPool.Rent(out AddonUpdateArgs arg); + arg.Clear(); + arg.Addon = (nint)addon; + arg.TimeDeltaInternal = delta; + this.InvokeListenersSafely(AddonEvent.PreUpdate, arg); + + try + { + addon->Update(delta); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.InvokeListenersSafely(AddonEvent.PostUpdate, arg); + } + + private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + var result = false; + + using var returner = this.argsPool.Rent(out AddonRefreshArgs arg); + arg.Clear(); + arg.Addon = (nint)addon; + arg.AtkValueCount = valueCount; + arg.AtkValues = (nint)values; + this.InvokeListenersSafely(AddonEvent.PreRefresh, arg); + valueCount = arg.AtkValueCount; + values = (AtkValue*)arg.AtkValues; + + try + { + result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); + } + + this.InvokeListenersSafely(AddonEvent.PostRefresh, arg); + return result; + } + + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg); + arg.Clear(); + arg.Addon = (nint)addon; + arg.NumberArrayData = (nint)numberArrayData; + arg.StringArrayData = (nint)stringArrayData; + this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg); + numberArrayData = (NumberArrayData**)arg.NumberArrayData; + stringArrayData = (StringArrayData**)arg.StringArrayData; + + try + { + addon->OnRequestedUpdate(numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg); } } @@ -169,7 +387,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); - private readonly List eventListeners = []; + private readonly List eventListeners = new(); /// void IInternalDisposableService.DisposeService() @@ -240,7 +458,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 new file mode 100644 index 000000000..bc9e4b639 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -0,0 +1,56 @@ +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 fc82e0582..9d411cdbc 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs @@ -25,12 +25,17 @@ 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 new file mode 100644 index 000000000..0d2bcc7f2 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs @@ -0,0 +1,112 @@ +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 new file mode 100644 index 000000000..297323b8f --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs @@ -0,0 +1,80 @@ +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 deleted file mode 100644 index 47ff92c3d..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ /dev/null @@ -1,638 +0,0 @@ -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/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 279bf46e5..b1b798a8a 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -104,7 +104,7 @@ internal partial class ChatHandlers : IServiceType if (this.configuration.PrintDalamudWelcomeMsg) { - chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Versioning.GetScmVersion()) + chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion()) + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded))); } @@ -116,7 +116,7 @@ internal partial class ChatHandlers : IServiceType } } - if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Versioning.GetAssemblyVersion().StartsWith(this.configuration.LastVersion)) + if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion)) { var linkPayload = chatGui.AddChatLinkHandler( (_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs)); @@ -137,7 +137,7 @@ internal partial class ChatHandlers : IServiceType Type = XivChatType.Notice, }); - this.configuration.LastVersion = Versioning.GetAssemblyVersion(); + this.configuration.LastVersion = Util.AssemblyVersion; this.configuration.QueueSave(); } diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index af85f9228..f5b7011fe 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 } /// - [Api14ToDo("Maybe make this config scoped to internal name?")] + [Api13ToDo("Maybe make this config scoped to internal name?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// diff --git a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs index da448c683..2b8325927 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs @@ -92,16 +92,34 @@ public enum FlyTextKind : int /// IslandExp = 15, + /// + /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. + /// + [Obsolete("Use Dataset instead", true)] + Unknown16 = 16, + /// /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. /// Dataset = 16, + /// + /// Val1 in serif font, Text2 in sans-serif as subtitle. + /// + [Obsolete("Use Knowledge instead", true)] + Unknown17 = 17, + /// /// Val1 in serif font, Text2 in sans-serif as subtitle. /// Knowledge = 17, + /// + /// Val1 in serif font, Text2 in sans-serif as subtitle. + /// + [Obsolete("Use PhantomExp instead", true)] + Unknown18 = 18, + /// /// Val1 in serif font, Text2 in sans-serif as subtitle. /// diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs new file mode 100644 index 000000000..92bc6e31a --- /dev/null +++ b/Dalamud/Hooking/Internal/CallHook.cs @@ -0,0 +1,100 @@ +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 cc1f48ce7..0d2057b3b 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)] - [Api14ToDo("Map this field to ValueClamped, probably.")] + [Api13ToDo("Map this field to ValueClamped, probably.")] public double Value => this.ValueUnclamped; /// diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index f161c1868..4a8e6517e 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -162,7 +162,8 @@ internal class SeStringRenderer : IServiceType if (drawParams.Font.HasValue) font = drawParams.Font.Value; - if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null) + // API14: Remove commented out code + if (ThreadSafety.IsMainThread /* && drawParams.TargetDrawList is null */ && font is null) font = ImGui.GetFont(); if (font is null) throw new ArgumentException("Specified font is empty."); diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index dcbe123e7..5edf60e9d 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -66,10 +66,17 @@ public unsafe ref struct SeStringDrawState : IDisposable this.drawList = ssdp.TargetDrawList.Value; this.ScreenOffset = ssdp.ScreenOffset ?? Vector2.Zero; - this.ScreenOffset = ssdp.ScreenOffset ?? throw new ArgumentException( - $"{nameof(ssdp.ScreenOffset)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state. (GetCursorScreenPos?)"); - this.FontSize = ssdp.FontSize ?? throw new ArgumentException( - $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); + // API14: Remove, always throw + if (ThreadSafety.IsMainThread) + { + this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos(); + this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize(); + } + else + { + throw new ArgumentException( + $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); + } // this.FontSize = ssdp.FontSize ?? throw new ArgumentException( // $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index 3e4a5cec6..b1fdb5232 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -305,12 +305,12 @@ internal class DalamudCommands : IServiceType chatGui.Print(new SeStringBuilder() .AddItalics("Dalamud:") - .AddText($" {Versioning.GetScmVersion()}") + .AddText($" {Util.GetScmVersion()}") .Build()); chatGui.Print(new SeStringBuilder() .AddItalics("FFXIVCS:") - .AddText($" {Versioning.GetGitHashClientStructs()}") + .AddText($" {Util.GetGitHashClientStructs()}") .Build()); } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index be4228a81..b0fbeb6c5 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -182,7 +182,7 @@ internal class DalamudInterface : IInternalDisposableService () => Service.GetNullable()?.ToggleDevMenu(), VirtualKey.SHIFT); - if (Versioning.GetActiveTrack() != "release") + if (Util.GetActiveTrack() != "release") { titleScreenMenu.AddEntryCore( Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), @@ -865,7 +865,7 @@ internal class DalamudInterface : IInternalDisposableService } ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false, false); - ImGui.MenuItem($"D: {Versioning.GetScmVersion()} CS: {Versioning.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false, false); + ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false, false); ImGui.MenuItem($"CLR: {Environment.Version}", false, false); ImGui.EndMenu(); @@ -1076,8 +1076,8 @@ internal class DalamudInterface : IInternalDisposableService { ImGui.PushFont(InterfaceManager.MonoFont); - ImGui.BeginMenu($"{Versioning.GetActiveTrack() ?? "???"} on {Versioning.GetGitBranch() ?? "???"}", false); - ImGui.BeginMenu($"{Versioning.GetScmVersion()}", false); + ImGui.BeginMenu($"{Util.GetActiveTrack() ?? "???"} on {Util.GetGitBranch() ?? "???"}", false); + ImGui.BeginMenu($"{Util.GetScmVersion()}", false); ImGui.BeginMenu(this.FrameCount.ToString("000000"), false); ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false); diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index 51a9c48a6..9cc14ea14 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -47,7 +47,7 @@ public class BranchSwitcherWindow : Window this.branches = await client.GetFromJsonAsync>(BranchInfoUrl); Debug.Assert(this.branches != null, "this.branches != null"); - var trackName = Versioning.GetActiveTrack(); + var trackName = Util.GetActiveTrack(); this.selectedBranchIndex = this.branches.IndexOf(x => x.Value.Track == trackName); if (this.selectedBranchIndex == -1) { diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 44626ba31..b0a910ead 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -147,7 +147,7 @@ internal sealed class ChangelogWindow : Window, IDisposable var pmWantsChangelog = pm?.InstalledPlugins.Any() ?? true; return (string.IsNullOrEmpty(configuration.LastChangelogMajorMinor) || (!WarrantsChangelogForMajorMinor.StartsWith(configuration.LastChangelogMajorMinor) && - Versioning.GetAssemblyVersion().StartsWith(WarrantsChangelogForMajorMinor))) && pmWantsChangelog; + Util.AssemblyVersion.StartsWith(WarrantsChangelogForMajorMinor))) && pmWantsChangelog; } /// @@ -357,7 +357,7 @@ internal sealed class ChangelogWindow : Window, IDisposable { case State.WindowFadeIn: case State.ExplainerIntro: - ImGui.TextWrapped($"Welcome to Dalamud v{Versioning.GetScmVersion()}!"); + ImGui.TextWrapped($"Welcome to Dalamud v{Util.GetScmVersion()}!"); ImGuiHelpers.ScaledDummy(5); ImGui.TextWrapped(ChangeLog); ImGuiHelpers.ScaledDummy(5); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 4fb13b81a..b58166e89 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,9 +1,11 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Utility; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -46,38 +48,97 @@ public class AddonLifecycleWidget : IDataWindowWidget return; } - foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) + if (ImGui.CollapsingHeader("Listeners"u8)) { - using var eventId = ImRaii.PushId(eventType.ToString()); + ImGui.Indent(); + this.DrawEventListeners(); + ImGui.Unindent(); + } + 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())) { - using var eventIndent = ImRaii.PushIndent(); + ImGui.Indent(); + var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList(); - if (addonListeners.Count == 0) + if (listeners.Count == 0) { - ImGui.Text("No Addons Registered for Event"u8); + ImGui.Text("No Listeners Registered for Event"u8); } - foreach (var (addonName, listeners) in addonListeners) + if (ImGui.BeginTable("AddonLifecycleListenersTable"u8, 2)) { - using var addonId = ImRaii.PushId(addonName); + ImGui.TableSetupColumn("##AddonName"u8, ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##MethodInvoke"u8, ImGuiTableColumnFlags.WidthStretch); - if (ImGui.CollapsingHeader(addonName.IsNullOrEmpty() ? "GLOBAL" : addonName)) + foreach (var listener in listeners) { - using var addonIndent = ImRaii.PushIndent(); + ImGui.TableNextColumn(); + ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName); - 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.TableNextColumn(); + 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 f3e25caf8..3ad8f86c2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -5,6 +5,7 @@ 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; @@ -33,7 +34,7 @@ internal unsafe class HookWidget : IDataWindowWidget private MessageBoxWDelegate? messageBoxWOriginal; private AddonFinalizeDelegate? addonFinalizeOriginal; - private nint address; + private AddonLifecycleAddressResolver? address; private delegate int MessageBoxWDelegate( IntPtr hWnd, @@ -54,7 +55,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; } @@ -64,8 +65,8 @@ internal unsafe class HookWidget : IDataWindowWidget { this.Ready = true; - var sigScanner = Service.Get(); - this.address = sigScanner.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); + this.address = new AddonLifecycleAddressResolver(); + this.address.Setup(Service.Get()); } /// @@ -223,7 +224,7 @@ internal unsafe class HookWidget : IDataWindowWidget private IDalamudHook HookAddonFinalize() { - var hook = Hook.FromAddress(this.address, this.OnAddonFinalize); + var hook = Hook.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize); this.addonFinalizeOriginal = hook.Original; hook.Enable(); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index e9b4022e4..a88f576f9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -144,7 +144,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget new TextEntry(TextEntryType.Macro, " "), ]; - private SeStringParameter[]? localParameters = [Versioning.GetScmVersion()]; + private SeStringParameter[]? localParameters = [Util.GetScmVersion()]; private ReadOnlySeString input; private ClientLanguage? language; private Task? validImportSheetNamesTask; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 3241015fc..ac092bd25 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -302,7 +302,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.profileManagerWidget.Reset(); - if (this.staleDalamudNewVersion == null && !Versioning.GetActiveTrack().IsNullOrEmpty()) + if (this.staleDalamudNewVersion == null && !Util.GetActiveTrack().IsNullOrEmpty()) { Service.Get().GetVersionForCurrentTrack().ContinueWith(t => { @@ -310,7 +310,7 @@ internal class PluginInstallerWindow : Window, IDisposable return; var versionInfo = t.Result; - if (versionInfo.AssemblyVersion != Versioning.GetScmVersion()) + if (versionInfo.AssemblyVersion != Util.GetScmVersion()) { this.staleDalamudNewVersion = versionInfo.AssemblyVersion; } @@ -1670,7 +1670,7 @@ internal class PluginInstallerWindow : Window, IDisposable DrawWarningIcon(); DrawLinesCentered("A new version of Dalamud is available.\n" + "Please restart the game to ensure compatibility with updated plugins.\n" + - $"old: {Versioning.GetScmVersion()} new: {this.staleDalamudNewVersion}"); + $"old: {Util.GetScmVersion()} new: {this.staleDalamudNewVersion}"); ImGuiHelpers.ScaledDummy(10); } @@ -2461,7 +2461,7 @@ internal class PluginInstallerWindow : Window, IDisposable var isOutdated = effectiveApiLevel < PluginManager.DalamudApiLevel; var isIncompatible = manifest.MinimumDalamudVersion != null && - manifest.MinimumDalamudVersion > Versioning.GetAssemblyVersionParsed(); + manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed; var enableInstallButton = this.updateStatus != OperationStatus.InProgress && this.installStatus != OperationStatus.InProgress && diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 4785ceb3c..74b9b0fd7 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -223,7 +223,7 @@ Contribute at: https://github.com/goatcorp/Dalamud .Select(plugin => $"{plugin.Manifest.Name} by {plugin.Manifest.Author}\n") .Aggregate(string.Empty, (current, next) => $"{current}{next}"); - this.creditsText = string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits, Versioning.GetGitHashClientStructs()); + this.creditsText = string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits, Util.GetGitHashClientStructs()); var gameGui = Service.Get(); var playerState = PlayerState.Instance(); diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index f826da622..69cdc4d28 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 (ev is not (AddonEvent.PostDraw or AddonEvent.PreDraw)) return; + if (args is not AddonDrawArgs drawArgs) return; - var addon = args.Addon.Struct; + var addon = drawArgs.Addon.Struct; var textNode = addon->GetTextNodeById(3); // look and feel init. should be harmless to set. @@ -503,7 +503,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable lssb.PushEdgeColorType(701).PushColorType(539) .Append(SeIconChar.BoxedLetterD.ToIconChar()) .PopColorType().PopEdgeColorType(); - lssb.Append($" Dalamud: {Versioning.GetScmVersion()}"); + lssb.Append($" Dalamud: {Util.GetScmVersion()}"); lssb.Append($" - {count} {(count != 1 ? "plugins" : "plugin")} loaded"); diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs index c6a476fff..aeed98695 100644 --- a/Dalamud/Networking/Http/HappyHttpClient.cs +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -36,7 +36,7 @@ internal class HappyHttpClient : IInternalDisposableService { UserAgent = { - new ProductInfoHeaderValue("Dalamud", Versioning.GetAssemblyVersion()), + new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), }, }, }; diff --git a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs index ae8319f21..c5a4c851a 100644 --- a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs +++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs @@ -38,7 +38,7 @@ internal sealed class ClientHelloService : IInternalDisposableService return new ClientHelloResponse { ApiVersion = "1.0", - DalamudVersion = Versioning.GetScmVersion(), + DalamudVersion = Util.GetScmVersion(), GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", ProcessId = Environment.ProcessId, ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(), diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 1051f908c..6fd9064b6 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -16,15 +16,18 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.Sanitizer; using Dalamud.Interface; using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Windows.PluginInstaller; +using Dalamud.Interface.Internal.Windows.SelfTest; +using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; -using Dalamud.Plugin.VersionInfo; -using Dalamud.Utility; +using Dalamud.Plugin.Services; using Serilog; @@ -201,7 +204,11 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa return true; } - /// + /// + /// Gets the plugin the given assembly is part of. + /// + /// The assembly to check. + /// The plugin the given assembly is part of, or null if this is a shared assembly or if this information cannot be determined. public IExposedPlugin? GetPlugin(Assembly assembly) => AssemblyLoadContext.GetLoadContext(assembly) switch { @@ -209,7 +216,11 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa var context => this.GetPlugin(context), }; - /// + /// + /// Gets the plugin that loads in the given context. + /// + /// The context to check. + /// The plugin that loads in the given context, or null if this isn't a plugin's context or if this information cannot be determined. public IExposedPlugin? GetPlugin(AssemblyLoadContext context) => Service.Get().InstalledPlugins.FirstOrDefault(p => p.LoadsIn(context)) switch { @@ -217,12 +228,6 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa var p => new ExposedPlugin(p), }; - /// - public IDalamudVersionInfo GetDalamudVersion() - { - return new DalamudVersionInfo(Versioning.GetAssemblyVersionParsed(), Versioning.GetActiveTrack()); - } - #region IPC /// diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index 92ecab006..d1b6977d4 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -15,7 +15,7 @@ using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; -using Dalamud.Plugin.VersionInfo; +using Dalamud.Plugin.Services; namespace Dalamud.Plugin; @@ -194,12 +194,6 @@ public interface IDalamudPluginInterface : IServiceProvider /// The plugin that loads in the given context, or null if this isn't a plugin's context or if this information cannot be determined. IExposedPlugin? GetPlugin(AssemblyLoadContext context); - /// - /// Gets information about the version of Dalamud this plugin is loaded into. - /// - /// Class containing version information. - IDalamudVersionInfo GetDalamudVersion(); - /// T GetOrCreateData(string tag, Func dataGenerator) where T : class; diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 193a2d45f..e2eded57c 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1790,7 +1790,7 @@ internal class PluginManager : IInternalDisposableService var updates = this.AvailablePlugins .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName) .Where(remoteManifest => plugin.Manifest.InstalledFromUrl == remoteManifest.SourceRepo.PluginMasterUrl || !remoteManifest.SourceRepo.IsThirdParty) - .Where(remoteManifest => remoteManifest.MinimumDalamudVersion == null || Versioning.GetAssemblyVersionParsed() >= remoteManifest.MinimumDalamudVersion) + .Where(remoteManifest => remoteManifest.MinimumDalamudVersion == null || Util.AssemblyVersionParsed >= remoteManifest.MinimumDalamudVersion) .Where(remoteManifest => { var useTesting = this.UseTesting(remoteManifest); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 1fe18b95b..0197683ef 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -315,7 +315,7 @@ internal class LocalPlugin : IAsyncDisposable if (!this.CheckPolicy()) throw new PluginPreconditionFailedException($"Unable to load {this.Name} as a load policy forbids it"); - if (this.Manifest.MinimumDalamudVersion != null && this.Manifest.MinimumDalamudVersion > Versioning.GetAssemblyVersionParsed()) + if (this.Manifest.MinimumDalamudVersion != null && this.Manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed) throw new PluginPreconditionFailedException($"Unable to load {this.Name}, Dalamud version is lower than minimum required version {this.Manifest.MinimumDalamudVersion}"); this.State = PluginState.Loading; diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index d5c1131af..c5e703e4b 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -59,7 +59,7 @@ internal class PluginRepository }, UserAgent = { - new ProductInfoHeaderValue("Dalamud", Versioning.GetAssemblyVersion()), + new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), }, }, }; @@ -164,7 +164,7 @@ internal class PluginRepository } this.PluginMaster = pluginMaster.Where(this.IsValidManifest).ToList().AsReadOnly(); - + // API9 HACK: Force IsHide to false, we should remove that if (!this.IsThirdParty) { @@ -197,7 +197,7 @@ internal class PluginRepository Log.Error("Plugin {PluginName} in {RepoLink} has an invalid Name.", manifest.InternalName, this.PluginMasterUrl); return false; } - + // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (manifest.AssemblyVersion == null) { @@ -224,7 +224,7 @@ internal class PluginRepository request.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true }; using var requestCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); - + return await httpClient.SendAsync(request, requestCts.Token); } } diff --git a/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs b/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs deleted file mode 100644 index c87c012af..000000000 --- a/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Dalamud.Plugin.VersionInfo; - -/// -internal class DalamudVersionInfo(Version version, string? track) : IDalamudVersionInfo -{ - /// - public Version Version { get; } = version; - - /// - public string? BetaTrack { get; } = track; -} diff --git a/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs b/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs deleted file mode 100644 index e6b6a9601..000000000 --- a/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Dalamud.Plugin.VersionInfo; - -/// -/// Interface exposing various information related to Dalamud versioning. -/// -public interface IDalamudVersionInfo -{ - /// - /// Gets the Dalamud version. - /// - Version Version { get; } - - /// - /// Gets the currently used beta track. - /// Please don't tell users to switch branches. They have it bad enough, fix your things instead. - /// Null if this build wasn't launched from XIVLauncher. - /// - string? BetaTrack { get; } -} diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index f0a98ca98..7ce96208c 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -37,7 +37,7 @@ internal static class BugBait Name = plugin.InternalName, Version = isTesting ? plugin.TestingAssemblyVersion?.ToString() : plugin.AssemblyVersion.ToString(), Platform = Util.GetHostPlatform().ToString(), - DalamudHash = Versioning.GetScmVersion(), + DalamudHash = Util.GetScmVersion(), }; if (includeException) diff --git a/Dalamud/Support/DalamudReleases.cs b/Dalamud/Support/DalamudReleases.cs index 949ebf94a..603c77487 100644 --- a/Dalamud/Support/DalamudReleases.cs +++ b/Dalamud/Support/DalamudReleases.cs @@ -38,7 +38,7 @@ internal class DalamudReleases : IServiceType /// The version info for the current track. public async Task GetVersionForCurrentTrack() { - var currentTrack = Versioning.GetActiveTrack(); + var currentTrack = Util.GetActiveTrack(); if (currentTrack.IsNullOrEmpty()) return null; diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index de529a29b..88048c462 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -69,11 +69,11 @@ public static class Troubleshooting LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(), PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()), EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(), - DalamudVersion = Versioning.GetScmVersion(), - DalamudGitHash = Versioning.GetGitHash() ?? "Unknown", + DalamudVersion = Util.GetScmVersion(), + DalamudGitHash = Util.GetGitHash() ?? "Unknown", GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown", Language = startInfo.Language.ToString(), - BetaKey = Versioning.GetActiveTrack(), + BetaKey = Util.GetActiveTrack(), DoPluginTest = configuration.DoPluginTest, LoadAllApiLevels = pluginManager?.LoadAllApiLevels == true, InterfaceLoaded = interfaceManager?.IsReady ?? false, diff --git a/Dalamud/Utility/Api14ToDoAttribute.cs b/Dalamud/Utility/Api13ToDoAttribute.cs similarity index 75% rename from Dalamud/Utility/Api14ToDoAttribute.cs rename to Dalamud/Utility/Api13ToDoAttribute.cs index 945b6e4db..576401cda 100644 --- a/Dalamud/Utility/Api14ToDoAttribute.cs +++ b/Dalamud/Utility/Api13ToDoAttribute.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 Api14ToDoAttribute : Attribute +internal sealed class Api13ToDoAttribute : Attribute { /// /// Marks that this should be made internal. @@ -12,11 +12,11 @@ internal sealed class Api14ToDoAttribute : 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 Api14ToDoAttribute(string what, string what2 = "") + public Api13ToDoAttribute(string what, string what2 = "") { _ = what; _ = what2; diff --git a/Dalamud/Utility/Api15ToDoAttribute.cs b/Dalamud/Utility/Api15ToDoAttribute.cs deleted file mode 100644 index 646c260e8..000000000 --- a/Dalamud/Utility/Api15ToDoAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -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 0ea5bbcbf..bde113904 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -68,10 +68,96 @@ public static partial class Util ]; private static readonly Type GenericSpanType = typeof(Span<>); + private static string? scmVersionInternal; + private static string? gitHashInternal; + private static string? gitHashClientStructsInternal; + private static string? branchInternal; private static ulong moduleStartAddr; private static ulong moduleEndAddr; + /// + /// Gets the Dalamud version. + /// + [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] + public static string AssemblyVersion { get; } = + Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString(); + + /// + /// Gets the Dalamud version. + /// + internal static Version AssemblyVersionParsed { get; } = + Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!; + + /// + /// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return + /// the git describe output for this build, which will be a raw version if this is a stable build or an + /// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string. + /// + /// The SCM version of the assembly. + public static string GetScmVersion() + { + if (scmVersionInternal != null) return scmVersionInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value + ?? asm.GetName().Version!.ToString(); + } + + /// + /// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds, + /// and will be suffixed with `-dirty` if in release with pending changes. + /// + /// The git hash of the assembly. + public static string? GetGitHash() + { + if (gitHashInternal != null) + return gitHashInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + return gitHashInternal = attrs.FirstOrDefault(a => a.Key == "GitHash")?.Value ?? "N/A"; + } + + /// + /// Gets the git hash value from the assembly or null if it cannot be found. + /// + /// The git hash of the assembly. + public static string? GetGitHashClientStructs() + { + if (gitHashClientStructsInternal != null) + return gitHashClientStructsInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + gitHashClientStructsInternal = attrs.First(a => a.Key == "GitHashClientStructs").Value; + + return gitHashClientStructsInternal; + } + + /// + /// Gets the Git branch name this version of Dalamud was built from, or null, if this is a Debug build. + /// + /// The branch name. + public static string? GetGitBranch() + { + if (branchInternal != null) + return branchInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + var gitBranch = attrs.FirstOrDefault(a => a.Key == "GitBranch")?.Value; + if (gitBranch == null) + return null; + + return branchInternal = gitBranch; + } + /// public static unsafe string DescribeAddress(void* p) => DescribeAddress((nint)p); @@ -607,6 +693,16 @@ public static partial class Util } } + /// + /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version + /// downloaded from webservices. + /// + /// The name of the track, or null. + internal static string? GetActiveTrack() + { + return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); + } + /// /// Gets a random, inoffensive, human-friendly string. /// diff --git a/Dalamud/Utility/Versioning.cs b/Dalamud/Utility/Versioning.cs deleted file mode 100644 index d3b30b834..000000000 --- a/Dalamud/Utility/Versioning.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Linq; -using System.Reflection; - -namespace Dalamud.Utility; - -/// -/// Helpers to access Dalamud versioning information. -/// -internal static class Versioning -{ - private static string? scmVersionInternal; - private static string? gitHashInternal; - private static string? gitHashClientStructsInternal; - private static string? branchInternal; - - /// - /// Gets the Dalamud version. - /// - /// The raw Dalamud assembly version. - internal static string GetAssemblyVersion() => - Assembly.GetAssembly(typeof(Versioning))!.GetName().Version!.ToString(); - - /// - /// Gets the Dalamud version. - /// - /// The parsed Dalamud assembly version. - internal static Version GetAssemblyVersionParsed() => - Assembly.GetAssembly(typeof(Versioning))!.GetName().Version!; - - /// - /// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return - /// the git describe output for this build, which will be a raw version if this is a stable build or an - /// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string. - /// - /// The SCM version of the assembly. - internal static string GetScmVersion() - { - if (scmVersionInternal != null) return scmVersionInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value - ?? asm.GetName().Version!.ToString(); - } - - /// - /// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds, - /// and will be suffixed with `-dirty` if in release with pending changes. - /// - /// The git hash of the assembly. - internal static string? GetGitHash() - { - if (gitHashInternal != null) - return gitHashInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - return gitHashInternal = attrs.FirstOrDefault(a => a.Key == "GitHash")?.Value ?? "N/A"; - } - - /// - /// Gets the git hash value from the assembly or null if it cannot be found. - /// - /// The git hash of the assembly. - internal static string? GetGitHashClientStructs() - { - if (gitHashClientStructsInternal != null) - return gitHashClientStructsInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - gitHashClientStructsInternal = attrs.First(a => a.Key == "GitHashClientStructs").Value; - - return gitHashClientStructsInternal; - } - - /// - /// Gets the Git branch name this version of Dalamud was built from, or null, if this is a Debug build. - /// - /// The branch name. - internal static string? GetGitBranch() - { - if (branchInternal != null) - return branchInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - var gitBranch = attrs.FirstOrDefault(a => a.Key == "GitBranch")?.Value; - if (gitBranch == null) - return null; - - return branchInternal = gitBranch; - } - - /// - /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version - /// downloaded from webservices. - /// - /// The name of the track, or null. - internal static string? GetActiveTrack() - { - return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); - } -} diff --git a/Directory.Build.props b/Directory.Build.props index 3897256bf..eabb727e8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ net10.0-windows x64 x64 - 14.0 + 13.0 diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs index 3cf20bb30..665fa434f 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 Span(ref v)), + MemoryMarshal.Cast(new(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 Span(ref v)), + MemoryMarshal.Cast(new(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 Span(ref v)), + MemoryMarshal.Cast(new(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 5881ac462..fb86096ff 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 Span(ref data)), + MemoryMarshal.Cast(new(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 Span(ref data)), + MemoryMarshal.Cast(new(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 Span(ref data)), + MemoryMarshal.Cast(new(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 b0c4b7c79..20ee78ab6 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 Span(ref v)), + MemoryMarshal.Cast(new(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 Span(ref v)), + MemoryMarshal.Cast(new(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 Span(ref v)), + MemoryMarshal.Cast(new(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 f2b635764..a62152c39 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 Span(ref Unsafe.AsRef(ref this.fixedBuffer))); + : MemoryMarshal.Cast(new(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 Span(ref Unsafe.AsRef(ref this.fixedBuffer))); + MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer))); public static implicit operator ImU8String(ReadOnlySpan text) => new(text); public static implicit operator ImU8String(ReadOnlyMemory text) => new(text);