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