This commit is contained in:
MidoriKami 2025-12-10 14:16:59 -08:00 committed by GitHub
commit e8c6e02693
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1019 additions and 1105 deletions

View file

@ -11,7 +11,7 @@ namespace Dalamud.Configuration;
/// <summary> /// <summary>
/// Configuration to store settings for a dalamud plugin. /// Configuration to store settings for a dalamud plugin.
/// </summary> /// </summary>
[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] [Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
public sealed class PluginConfigurations public sealed class PluginConfigurations
{ {
private readonly DirectoryInfo configDirectory; private readonly DirectoryInfo configDirectory;

View file

@ -1,107 +0,0 @@
using System.Runtime.CompilerServices;
using System.Threading;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
namespace Dalamud.Game.Addon;
/// <summary>Argument pool for Addon Lifecycle services.</summary>
[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()
{
}
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonSetupArgs> Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonFinalizeArgs> Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonDrawArgs> Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonUpdateArgs> Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonRefreshArgs> Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonRequestedUpdateArgs> Rent(out AddonRequestedUpdateArgs arg) =>
new(out arg, this.addonRequestedUpdateArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonReceiveEventArgs> Rent(out AddonReceiveEventArgs arg) =>
new(out arg, this.addonReceiveEventArgPool);
/// <summary>Returns the object to the pool on dispose.</summary>
/// <typeparam name="T">The type.</typeparam>
public readonly ref struct PooledEntry<T>
where T : AddonArgs, new()
{
private readonly Span<T> pool;
private readonly T obj;
/// <summary>Initializes a new instance of the <see cref="PooledEntry{T}"/> struct.</summary>
/// <param name="arg">An instance of the argument.</param>
/// <param name="pool">The pool to rent from and return to.</param>
public PooledEntry(out T arg, Span<T> 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();
}
/// <summary>Returns the item to the pool.</summary>
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;
}
}
}
}

View file

@ -5,19 +5,24 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Base class for AddonLifecycle AddonArgTypes. /// Base class for AddonLifecycle AddonArgTypes.
/// </summary> /// </summary>
public abstract unsafe class AddonArgs public class AddonArgs
{ {
/// <summary> /// <summary>
/// Constant string representing the name of an addon that is invalid. /// Constant string representing the name of an addon that is invalid.
/// </summary> /// </summary>
public const string InvalidAddon = "NullAddon"; public const string InvalidAddon = "NullAddon";
private string? addonName; /// <summary>
/// Initializes a new instance of the <see cref="AddonArgs"/> class.
/// </summary>
internal AddonArgs()
{
}
/// <summary> /// <summary>
/// Gets the name of the addon this args referrers to. /// Gets the name of the addon this args referrers to.
/// </summary> /// </summary>
public string AddonName => this.GetAddonName(); public string AddonName { get; private set; } = InvalidAddon;
/// <summary> /// <summary>
/// Gets the pointer to the addons AtkUnitBase. /// Gets the pointer to the addons AtkUnitBase.
@ -25,55 +30,17 @@ public abstract unsafe class AddonArgs
public AtkUnitBasePtr Addon public AtkUnitBasePtr Addon
{ {
get; get;
internal set; internal set
{
field = value;
if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name))
this.AddonName = value.Name;
}
} }
/// <summary> /// <summary>
/// Gets the type of these args. /// Gets the type of these args.
/// </summary> /// </summary>
public abstract AddonArgsType Type { get; } public virtual AddonArgsType Type => AddonArgsType.Generic;
/// <summary>
/// Checks if addon name matches the given span of char.
/// </summary>
/// <param name="name">The name to check.</param>
/// <returns>Whether it is the case.</returns>
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;
}
/// <summary>
/// Clears this AddonArgs values.
/// </summary>
internal virtual void Clear()
{
this.addonName = null;
this.Addon = 0;
}
/// <summary>
/// Helper method for ensuring the name of the addon is valid.
/// </summary>
/// <returns>The name of the addon for this object. <see cref="InvalidAddon"/> when invalid.</returns>
private string GetAddonName()
{
if (this.Addon.IsNull) return InvalidAddon;
var name = this.Addon.Name;
if (string.IsNullOrEmpty(name))
return InvalidAddon;
return this.addonName ??= name;
}
} }

View file

@ -0,0 +1,22 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Close events.
/// </summary>
public class AddonCloseArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonCloseArgs"/> class.
/// </summary>
internal AddonCloseArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Close;
/// <summary>
/// Gets or sets a value indicating whether the window should fire the callback method on close.
/// </summary>
public bool FireCallback { get; set; }
}

View file

@ -1,24 +0,0 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Draw events.
/// </summary>
public class AddonDrawArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonDrawArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonDrawArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Draw;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -1,24 +0,0 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonFinalizeArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonFinalizeArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonFinalizeArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Finalize;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -0,0 +1,32 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Hide events.
/// </summary>
public class AddonHideArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonHideArgs"/> class.
/// </summary>
internal AddonHideArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Hide;
/// <summary>
/// Gets or sets a value indicating whether to call the hide callback handler when this hides.
/// </summary>
public bool CallHideCallback { get; set; }
/// <summary>
/// Gets or sets the flags that the window will set when it Shows/Hides.
/// </summary>
public uint SetShowHideFlags { get; set; }
/// <summary>
/// Gets or sets a value indicating whether something for this event message.
/// </summary>
internal bool UnknownBool { get; set; }
}

View file

@ -3,13 +3,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Addon argument data for ReceiveEvent events. /// Addon argument data for ReceiveEvent events.
/// </summary> /// </summary>
public class AddonReceiveEventArgs : AddonArgs, ICloneable public class AddonReceiveEventArgs : AddonArgs
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class. /// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
/// </summary> /// </summary>
[Obsolete("Not intended for public construction.", false)] internal AddonReceiveEventArgs()
public AddonReceiveEventArgs()
{ {
} }
@ -32,23 +31,7 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable
public nint AtkEvent { get; set; } public nint AtkEvent { get; set; }
/// <summary> /// <summary>
/// Gets or sets the pointer to a block of data for this event message. /// Gets or sets the pointer to an AtkEventData for this event message.
/// </summary> /// </summary>
public nint Data { get; set; } public nint AtkEventData { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkEventType = default;
this.EventParam = default;
this.AtkEvent = default;
this.Data = default;
}
} }

View file

@ -1,3 +1,5 @@
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@ -5,13 +7,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Addon argument data for Refresh events. /// Addon argument data for Refresh events.
/// </summary> /// </summary>
public class AddonRefreshArgs : AddonArgs, ICloneable public class AddonRefreshArgs : AddonArgs
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class. /// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class.
/// </summary> /// </summary>
[Obsolete("Not intended for public construction.", false)] internal AddonRefreshArgs()
public AddonRefreshArgs()
{ {
} }
@ -31,19 +32,7 @@ public class AddonRefreshArgs : AddonArgs, ICloneable
/// <summary> /// <summary>
/// Gets the AtkValues in the form of a span. /// Gets the AtkValues in the form of a span.
/// </summary> /// </summary>
[Obsolete("Pending removal, unsafe to use when using custom ClientStructs")]
[Api15ToDo("Remove this")]
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
}
} }

View file

@ -3,13 +3,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Addon argument data for OnRequestedUpdate events. /// Addon argument data for OnRequestedUpdate events.
/// </summary> /// </summary>
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable public class AddonRequestedUpdateArgs : AddonArgs
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class. /// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
/// </summary> /// </summary>
[Obsolete("Not intended for public construction.", false)] internal AddonRequestedUpdateArgs()
public AddonRequestedUpdateArgs()
{ {
} }
@ -25,18 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
/// Gets or sets the StringArrayData** for this event. /// Gets or sets the StringArrayData** for this event.
/// </summary> /// </summary>
public nint StringArrayData { get; set; } public nint StringArrayData { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.NumberArrayData = default;
this.StringArrayData = default;
}
} }

View file

@ -1,3 +1,5 @@
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
@ -5,13 +7,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Addon argument data for Setup events. /// Addon argument data for Setup events.
/// </summary> /// </summary>
public class AddonSetupArgs : AddonArgs, ICloneable public class AddonSetupArgs : AddonArgs
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class. /// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
/// </summary> /// </summary>
[Obsolete("Not intended for public construction.", false)] internal AddonSetupArgs()
public AddonSetupArgs()
{ {
} }
@ -31,19 +32,7 @@ public class AddonSetupArgs : AddonArgs, ICloneable
/// <summary> /// <summary>
/// Gets the AtkValues in the form of a span. /// Gets the AtkValues in the form of a span.
/// </summary> /// </summary>
[Obsolete("Pending removal, unsafe to use when using custom ClientStructs")]
[Api15ToDo("Remove this")]
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
}
} }

View file

@ -0,0 +1,27 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Show events.
/// </summary>
public class AddonShowArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonShowArgs"/> class.
/// </summary>
internal AddonShowArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Show;
/// <summary>
/// Gets or sets a value indicating whether the window should play open sound effects.
/// </summary>
public bool SilenceOpenSoundEffect { get; set; }
/// <summary>
/// Gets or sets the flags that the window will unset when it Shows/Hides.
/// </summary>
public uint UnsetShowHideFlags { get; set; }
}

View file

@ -1,45 +0,0 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Update events.
/// </summary>
public class AddonUpdateArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonUpdateArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonUpdateArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Update;
/// <summary>
/// Gets the time since the last update.
/// </summary>
public float TimeDelta
{
get => this.TimeDeltaInternal;
init => this.TimeDeltaInternal = value;
}
/// <summary>
/// Gets or sets the time since the last update.
/// </summary>
internal float TimeDeltaInternal { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.TimeDeltaInternal = default;
}
}

View file

@ -5,38 +5,43 @@
/// </summary> /// </summary>
public enum AddonArgsType public enum AddonArgsType
{ {
/// <summary>
/// Generic arg type that contains no meaningful data.
/// </summary>
Generic,
/// <summary> /// <summary>
/// Contains argument data for Setup. /// Contains argument data for Setup.
/// </summary> /// </summary>
Setup, Setup,
/// <summary>
/// Contains argument data for Update.
/// </summary>
Update,
/// <summary>
/// Contains argument data for Draw.
/// </summary>
Draw,
/// <summary>
/// Contains argument data for Finalize.
/// </summary>
Finalize,
/// <summary> /// <summary>
/// Contains argument data for RequestedUpdate. /// Contains argument data for RequestedUpdate.
/// </summary> /// </summary>
RequestedUpdate, RequestedUpdate,
/// <summary> /// <summary>
/// Contains argument data for Refresh. /// Contains argument data for Refresh.
/// </summary> /// </summary>
Refresh, Refresh,
/// <summary> /// <summary>
/// Contains argument data for ReceiveEvent. /// Contains argument data for ReceiveEvent.
/// </summary> /// </summary>
ReceiveEvent, ReceiveEvent,
/// <summary>
/// Contains argument data for Show.
/// </summary>
Show,
/// <summary>
/// Contains argument data for Hide.
/// </summary>
Hide,
/// <summary>
/// Contains argument data for Close.
/// </summary>
Close,
} }

View file

@ -16,7 +16,7 @@ public enum AddonEvent
/// </summary> /// </summary>
/// <seealso cref="AddonSetupArgs"/> /// <seealso cref="AddonSetupArgs"/>
PreSetup, PreSetup,
/// <summary> /// <summary>
/// An event that is fired after an addon has finished its initial setup. This event is particularly useful for /// 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 /// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data
@ -29,7 +29,6 @@ public enum AddonEvent
/// An event that is fired before an addon begins its update cycle via <see cref="AtkUnitBase.Update"/>. This event /// An event that is fired before an addon begins its update cycle via <see cref="AtkUnitBase.Update"/>. This event
/// is fired every frame that an addon is loaded, regardless of visibility. /// is fired every frame that an addon is loaded, regardless of visibility.
/// </summary> /// </summary>
/// <seealso cref="AddonUpdateArgs"/>
PreUpdate, PreUpdate,
/// <summary> /// <summary>
@ -42,7 +41,6 @@ public enum AddonEvent
/// An event that is fired before an addon begins drawing to screen via <see cref="AtkUnitBase.Draw"/>. Unlike /// An event that is fired before an addon begins drawing to screen via <see cref="AtkUnitBase.Draw"/>. Unlike
/// <see cref="PreUpdate"/>, this event is only fired if an addon is visible or otherwise drawing to screen. /// <see cref="PreUpdate"/>, this event is only fired if an addon is visible or otherwise drawing to screen.
/// </summary> /// </summary>
/// <seealso cref="AddonDrawArgs"/>
PreDraw, PreDraw,
/// <summary> /// <summary>
@ -62,9 +60,8 @@ public enum AddonEvent
/// <br /> /// <br />
/// As this is part of the destruction process for an addon, this event does not have an associated Post event. /// As this is part of the destruction process for an addon, this event does not have an associated Post event.
/// </remarks> /// </remarks>
/// <seealso cref="AddonFinalizeArgs"/>
PreFinalize, PreFinalize,
/// <summary> /// <summary>
/// An event that is fired before a call to <see cref="AtkUnitBase.OnRequestedUpdate"/> is made in response to a /// An event that is fired before a call to <see cref="AtkUnitBase.OnRequestedUpdate"/> is made in response to a
/// change in the subscribed <see cref="AddonRequestedUpdateArgs.NumberArrayData"/> or /// change in the subscribed <see cref="AddonRequestedUpdateArgs.NumberArrayData"/> or
@ -81,13 +78,13 @@ public enum AddonEvent
/// to the Free Company's overview. /// to the Free Company's overview.
/// </example> /// </example>
PreRequestedUpdate, PreRequestedUpdate,
/// <summary> /// <summary>
/// An event that is fired after an addon has finished processing an <c>ArrayData</c> update. /// An event that is fired after an addon has finished processing an <c>ArrayData</c> update.
/// See <see cref="PreRequestedUpdate"/> for more information. /// See <see cref="PreRequestedUpdate"/> for more information.
/// </summary> /// </summary>
PostRequestedUpdate, PostRequestedUpdate,
/// <summary> /// <summary>
/// An event that is fired before an addon calls its <see cref="AtkUnitManager.RefreshAddon"/> method. Refreshes are /// An event that is fired before an addon calls its <see cref="AtkUnitManager.RefreshAddon"/> method. Refreshes are
/// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to /// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to
@ -96,13 +93,13 @@ public enum AddonEvent
/// <seealso cref="AddonRefreshArgs"/> /// <seealso cref="AddonRefreshArgs"/>
/// <seealso cref="PostRefresh"/> /// <seealso cref="PostRefresh"/>
PreRefresh, PreRefresh,
/// <summary> /// <summary>
/// An event that is fired after an addon has finished its refresh. /// An event that is fired after an addon has finished its refresh.
/// See <see cref="PreRefresh"/> for more information. /// See <see cref="PreRefresh"/> for more information.
/// </summary> /// </summary>
PostRefresh, PostRefresh,
/// <summary> /// <summary>
/// An event that is fired before an addon begins processing a user-driven event via /// An event that is fired before an addon begins processing a user-driven event via
/// <see cref="AtkEventListener.ReceiveEvent"/>, such as mousing over an element or clicking a button. This event /// <see cref="AtkEventListener.ReceiveEvent"/>, such as mousing over an element or clicking a button. This event
@ -112,10 +109,98 @@ public enum AddonEvent
/// <seealso cref="AddonReceiveEventArgs"/> /// <seealso cref="AddonReceiveEventArgs"/>
/// <seealso cref="PostReceiveEvent"/> /// <seealso cref="PostReceiveEvent"/>
PreReceiveEvent, PreReceiveEvent,
/// <summary> /// <summary>
/// An event that is fired after an addon finishes calling its <see cref="AtkEventListener.ReceiveEvent"/> method. /// An event that is fired after an addon finishes calling its <see cref="AtkEventListener.ReceiveEvent"/> method.
/// See <see cref="PreReceiveEvent"/> for more information. /// See <see cref="PreReceiveEvent"/> for more information.
/// </summary> /// </summary>
PostReceiveEvent, PostReceiveEvent,
/// <summary>
/// An event that is fired before an addon processes its open method.
/// </summary>
PreOpen,
/// <summary>
/// An event that is fired after an addon has processed its open method.
/// </summary>
PostOpen,
/// <summary>
/// An even that is fired before an addon processes its Close method.
/// </summary>
PreClose,
/// <summary>
/// An event that is fired after an addon has processed its Close method.
/// </summary>
PostClose,
/// <summary>
/// An event that is fired before an addon processes its Show method.
/// </summary>
PreShow,
/// <summary>
/// An event that is fired after an addon has processed its Show method.
/// </summary>
PostShow,
/// <summary>
/// An event that is fired before an addon processes its Hide method.
/// </summary>
PreHide,
/// <summary>
/// An event that is fired after an addon has processed its Hide method.
/// </summary>
PostHide,
/// <summary>
/// An event that is fired before an addon processes its OnMove method.
/// OnMove is triggered only when a move is completed.
/// </summary>
PreMove,
/// <summary>
/// An event that is fired after an addon has processed its OnMove method.
/// OnMove is triggered only when a move is completed.
/// </summary>
PostMove,
/// <summary>
/// An event that is fired before an addon processes its MouseOver method.
/// </summary>
PreMouseOver,
/// <summary>
/// An event that is fired after an addon has processed its MouseOver method.
/// </summary>
PostMouseOver,
/// <summary>
/// An event that is fired before an addon processes its MouseOut method.
/// </summary>
PreMouseOut,
/// <summary>
/// An event that is fired after an addon has processed its MouseOut method.
/// </summary>
PostMouseOut,
/// <summary>
/// An event that is fired before an addon processes its Focus method.
/// </summary>
/// <remarks>
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
/// </remarks>
PreFocus,
/// <summary>
/// An event that is fired after an addon has processed its Focus method.
/// </summary>
/// <remarks>
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
/// </remarks>
PostFocus,
} }

View file

@ -1,16 +1,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Hooking.Internal;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle; namespace Dalamud.Game.Addon.Lifecycle;
@ -21,75 +19,36 @@ namespace Dalamud.Game.Addon.Lifecycle;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal unsafe class AddonLifecycle : IInternalDisposableService internal unsafe class AddonLifecycle : IInternalDisposableService
{ {
/// <summary>
/// Gets a list of all allocated addon virtual tables.
/// </summary>
public static readonly List<AddonVirtualTable> AllocatedTables = [];
private static readonly ModuleLog Log = new("AddonLifecycle"); private static readonly ModuleLog Log = new("AddonLifecycle");
[ServiceManager.ServiceDependency] private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
private readonly nint disallowedReceiveEventAddress;
private readonly AddonLifecycleAddressResolver address;
private readonly AddonSetupHook<AtkUnitBase.Delegates.OnSetup> onAddonSetupHook;
private readonly Hook<AddonFinalizeDelegate> onAddonFinalizeHook;
private readonly CallHook<AtkUnitBase.Delegates.Draw> onAddonDrawHook;
private readonly CallHook<AtkUnitBase.Delegates.Update> onAddonUpdateHook;
private readonly Hook<AtkUnitManager.Delegates.RefreshAddon> onAddonRefreshHook;
private readonly CallHook<AtkUnitBase.Delegates.OnRequestedUpdate> onAddonRequestedUpdateHook;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner) private AddonLifecycle()
{ {
this.address = new AddonLifecycleAddressResolver(); this.onInitializeAddonHook = Hook<AtkUnitBase.Delegates.Initialize>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
this.address.Setup(sigScanner); this.onInitializeAddonHook.Enable();
this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent;
var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon;
this.onAddonSetupHook = new AddonSetupHook<AtkUnitBase.Delegates.OnSetup>(this.address.AddonSetup, this.OnAddonSetup);
this.onAddonFinalizeHook = Hook<AddonFinalizeDelegate>.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
this.onAddonDrawHook = new CallHook<AtkUnitBase.Delegates.Draw>(this.address.AddonDraw, this.OnAddonDraw);
this.onAddonUpdateHook = new CallHook<AtkUnitBase.Delegates.Update>(this.address.AddonUpdate, this.OnAddonUpdate);
this.onAddonRefreshHook = Hook<AtkUnitManager.Delegates.RefreshAddon>.FromAddress(refreshAddonAddress, this.OnAddonRefresh);
this.onAddonRequestedUpdateHook = new CallHook<AtkUnitBase.Delegates.OnRequestedUpdate>(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);
/// <summary>
/// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
/// </summary>
internal List<AddonLifecycleReceiveEventListener> ReceiveEventListeners { get; } = new();
/// <summary> /// <summary>
/// Gets a list of all AddonLifecycle Event Listeners. /// Gets a list of all AddonLifecycle Event Listeners.
/// </summary> /// </summary> <br/>
internal List<AddonLifecycleEventListener> EventListeners { get; } = new(); /// Mapping is: EventType -> AddonName -> ListenerList
internal Dictionary<AddonEvent, Dictionary<string, HashSet<AddonLifecycleEventListener>>> EventListeners { get; } = [];
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.onAddonSetupHook.Dispose(); this.onInitializeAddonHook?.Dispose();
this.onAddonFinalizeHook.Dispose(); this.onInitializeAddonHook = null;
this.onAddonDrawHook.Dispose();
this.onAddonUpdateHook.Dispose();
this.onAddonRefreshHook.Dispose();
this.onAddonRequestedUpdateHook.Dispose();
foreach (var receiveEventListener in this.ReceiveEventListeners) AllocatedTables.ForEach(entry => entry.Dispose());
{ AllocatedTables.Clear();
receiveEventListener.Dispose();
}
} }
/// <summary> /// <summary>
@ -98,20 +57,20 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to register.</param> /// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener) internal void RegisterListener(AddonLifecycleEventListener listener)
{ {
this.framework.RunOnTick(() => if (!this.EventListeners.ContainsKey(listener.EventType))
{ {
this.EventListeners.Add(listener); if (!this.EventListeners.TryAdd(listener.EventType, []))
return;
// 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 }) // 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.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) {
{ if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
receiveEventListener.TryEnable(); return;
} }
}
}); this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
} }
/// <summary> /// <summary>
@ -120,27 +79,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param> /// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener) internal void UnregisterListener(AddonLifecycleEventListener listener)
{ {
// Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
listener.Removed = true;
this.framework.RunOnTick(() =>
{ {
this.EventListeners.Remove(listener); if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
// If we are disabling an ReceiveEvent listener, check if we should disable the hook.
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
{ {
// Get the ReceiveEvent Listener for this addon addonListener.Remove(listener);
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();
}
}
} }
}); }
} }
/// <summary> /// <summary>
@ -151,226 +96,63 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="blame">What to blame on errors.</param> /// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{ {
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. // Early return if we don't have any listeners of this type
foreach (var listener in this.EventListeners) if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
// Handle listeners for this event type that don't care which addon is triggering it
if (addonListeners.TryGetValue(string.Empty, out var globalListeners))
{ {
if (listener.EventType != eventType) foreach (var listener in globalListeners)
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
{ {
listener.FunctionDelegate.Invoke(eventType, args); try
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
}
}
}
private void RegisterReceiveEventHook(AtkUnitBase* addon)
{
// Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
// Disallows hooking the core internal event handler.
var addonName = addon->NameString;
var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent;
if (receiveEventAddress != this.disallowedReceiveEventAddress)
{
// If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener)
{
if (!existingListener.AddonNames.Contains(addonName))
{ {
existingListener.AddonNames.Add(addonName); listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener.");
} }
} }
}
// Else, we have an addon that we don't have the ReceiveEvent for yet, make it. // Handle listeners that are listening for this addon and event type specifically
else if (addonListeners.TryGetValue(args.AddonName, out var addonListener))
{
foreach (var listener in addonListener)
{ {
this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress)); try
}
// If we have an active listener for this addon already, we need to activate this hook.
if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
{
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
{ {
receiveEventListener.TryEnable(); listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}.");
} }
} }
} }
} }
private void UnregisterReceiveEventHook(string addonName) private void OnAddonInitialize(AtkUnitBase* addon)
{ {
// Remove this addons ReceiveEvent Registration try
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
{ {
eventListener.AddonNames.Remove(addonName); this.LogInitialize(addon->NameString);
// If there are no more listeners let's remove and dispose. // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
if (eventListener.AddonNames.Count is 0) AllocatedTables.Add(new AddonVirtualTable(addon, this));
{
this.ReceiveEventListeners.Remove(eventListener);
eventListener.Dispose();
}
} }
catch (Exception e)
{
Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize.");
}
this.onInitializeAddonHook!.Original(addon);
} }
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) [Conditional("DEBUG")]
private void LogInitialize(string addonName)
{ {
try Log.Debug($"Initializing {addonName}");
{
this.RegisterReceiveEventHook(addon);
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
}
using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
valueCount = arg.AtkValueCount;
values = (AtkValue*)arg.AtkValues;
try
{
addon->OnSetup(valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
}
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
{
try
{
var addonName = atkUnitBase[0]->NameString;
this.UnregisterReceiveEventHook(addonName);
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
}
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
arg.Clear();
arg.Addon = (nint)atkUnitBase[0];
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
try
{
this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
}
}
private void OnAddonDraw(AtkUnitBase* addon)
{
using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
try
{
addon->Draw();
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.TimeDeltaInternal = delta;
this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
try
{
addon->Update(delta);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
}
private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
var result = false;
using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
valueCount = arg.AtkValueCount;
values = (AtkValue*)arg.AtkValues;
try
{
result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.NumberArrayData = (nint)numberArrayData;
arg.StringArrayData = (nint)stringArrayData;
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
numberArrayData = (NumberArrayData**)arg.NumberArrayData;
stringArrayData = (StringArrayData**)arg.StringArrayData;
try
{
addon->OnRequestedUpdate(numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
} }
} }
@ -387,7 +169,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get(); private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
private readonly List<AddonLifecycleEventListener> eventListeners = new(); private readonly List<AddonLifecycleEventListener> eventListeners = [];
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
@ -458,7 +240,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
this.eventListeners.RemoveAll(entry => this.eventListeners.RemoveAll(entry =>
{ {
if (entry.FunctionDelegate != handler) return false; if (entry.FunctionDelegate != handler) return false;
this.addonLifecycleService.UnregisterListener(entry); this.addonLifecycleService.UnregisterListener(entry);
return true; return true;
}); });

View file

@ -1,56 +0,0 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// AddonLifecycleService memory address resolver.
/// </summary>
internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
{
/// <summary>
/// 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.
/// </summary>
public nint AddonSetup { get; private set; }
/// <summary>
/// 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.
/// </summary>
public nint AddonSetup2 { get; private set; }
/// <summary>
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
/// </summary>
public nint AddonFinalize { get; private set; }
/// <summary>
/// Gets the address of the addon draw hook invoked by virtual function call.
/// </summary>
public nint AddonDraw { get; private set; }
/// <summary>
/// Gets the address of the addon update hook invoked by virtual function call.
/// </summary>
public nint AddonUpdate { get; private set; }
/// <summary>
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
/// </summary>
public nint AddonOnRequestedUpdate { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="sig">The signature scanner to facilitate setup.</param>
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");
}
}

View file

@ -25,17 +25,12 @@ internal class AddonLifecycleEventListener
/// string.Empty if it wants to be called for any addon. /// string.Empty if it wants to be called for any addon.
/// </summary> /// </summary>
public string AddonName { get; init; } public string AddonName { get; init; }
/// <summary>
/// Gets or sets a value indicating whether this event has been unregistered.
/// </summary>
public bool Removed { get; set; }
/// <summary> /// <summary>
/// Gets the event type this listener is looking for. /// Gets the event type this listener is looking for.
/// </summary> /// </summary>
public AddonEvent EventType { get; init; } public AddonEvent EventType { get; init; }
/// <summary> /// <summary>
/// Gets the delegate this listener invokes. /// Gets the delegate this listener invokes.
/// </summary> /// </summary>

View file

@ -1,112 +0,0 @@
using System.Collections.Generic;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// 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.
/// </summary>
internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
{
private static readonly ModuleLog Log = new("AddonLifecycle");
[ServiceManager.ServiceDependency]
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
/// </summary>
/// <param name="service">AddonLifecycle service instance.</param>
/// <param name="addonName">Initial Addon Requesting this listener.</param>
/// <param name="receiveEventAddress">Address of Addon's ReceiveEvent function.</param>
internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
{
this.AddonLifecycle = service;
this.AddonNames = [addonName];
this.FunctionAddress = receiveEventAddress;
}
/// <summary>
/// Gets the list of addons that use this receive event hook.
/// </summary>
public List<string> AddonNames { get; init; }
/// <summary>
/// Gets the address of the ReceiveEvent function as provided by the vtable on setup.
/// </summary>
public nint FunctionAddress { get; init; }
/// <summary>
/// Gets the contained hook for these addons.
/// </summary>
public Hook<AtkUnitBase.Delegates.ReceiveEvent>? Hook { get; private set; }
/// <summary>
/// Gets or sets the Reference to AddonLifecycle service instance.
/// </summary>
private AddonLifecycle AddonLifecycle { get; set; }
/// <summary>
/// Try to hook and enable this receive event handler.
/// </summary>
public void TryEnable()
{
this.Hook ??= Hook<AtkUnitBase.Delegates.ReceiveEvent>.FromAddress(this.FunctionAddress, this.OnReceiveEvent);
this.Hook?.Enable();
}
/// <summary>
/// Disable the hook for this receive event handler.
/// </summary>
public void Disable()
{
this.Hook?.Disable();
}
/// <inheritdoc/>
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);
}
}

View file

@ -1,80 +0,0 @@
using System.Runtime.InteropServices;
using Reloaded.Hooks.Definitions;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// This class represents a callsite hook used to replace the address of the OnSetup function in r9.
/// </summary>
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
internal class AddonSetupHook<T> : IDisposable where T : Delegate
{
private readonly Reloaded.Hooks.AsmHook asmHook;
private T? detour;
private bool activated;
/// <summary>
/// Initializes a new instance of the <see cref="AddonSetupHook{T}"/> class.
/// </summary>
/// <param name="address">Address of the instruction to replace.</param>
/// <param name="detour">Delegate to invoke.</param>
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);
}
/// <summary>
/// Gets a value indicating whether the hook is enabled.
/// </summary>
public bool IsEnabled => this.asmHook.IsEnabled;
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
public void Enable()
{
if (!this.activated)
{
this.activated = true;
this.asmHook.Activate();
return;
}
this.asmHook.Enable();
}
/// <summary>
/// Stops intercepting a call to the function.
/// </summary>
public void Disable()
{
this.asmHook.Disable();
}
/// <summary>
/// Remove a hook from the current process.
/// </summary>
public void Dispose()
{
this.asmHook.Disable();
this.detour = null;
}
}

View file

@ -0,0 +1,638 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// Represents a class that holds references to an addons original and modified virtual table entries.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="AddonVirtualTable"/> class.
/// </summary>
/// <param name="addon">AtkUnitBase* for the addon to replace the table of.</param>
/// <param name="lifecycleService">Reference to AddonLifecycle service to callback and invoke listeners.</param>
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<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
this.modifiedVirtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
this.modifiedVirtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
this.modifiedVirtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
this.modifiedVirtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AtkUnitBase*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
this.modifiedVirtualTable->Open = (delegate* unmanaged<AtkUnitBase*, uint, bool>)Marshal.GetFunctionPointerForDelegate(this.openFunction);
this.modifiedVirtualTable->Close = (delegate* unmanaged<AtkUnitBase*, bool, bool>)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
this.modifiedVirtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
this.modifiedVirtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
this.modifiedVirtualTable->OnMove = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction);
this.modifiedVirtualTable->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
this.modifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
this.modifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
}
/// <inheritdoc/>
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}");
}
}
}

View file

@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
} }
/// <inheritdoc/> /// <inheritdoc/>
[Api13ToDo("Maybe make this config scoped to internal name?")] [Api14ToDo("Maybe make this config scoped to internal name?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -1,100 +0,0 @@
using System.Runtime.InteropServices;
using Reloaded.Hooks.Definitions;
namespace Dalamud.Hooking.Internal;
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
internal class CallHook<T> : IDalamudHook where T : Delegate
{
private readonly Reloaded.Hooks.AsmHook asmHook;
private T? detour;
private bool activated;
/// <summary>
/// Initializes a new instance of the <see cref="CallHook{T}"/> class.
/// </summary>
/// <param name="address">Address of the instruction to replace.</param>
/// <param name="detour">Delegate to invoke.</param>
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);
}
/// <summary>
/// Gets a value indicating whether the hook is enabled.
/// </summary>
public bool IsEnabled => this.asmHook.IsEnabled;
/// <inheritdoc/>
public IntPtr Address { get; }
/// <inheritdoc/>
public string BackendName => "Reloaded AsmHook";
/// <inheritdoc/>
public bool IsDisposed => this.detour == null;
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
public void Enable()
{
if (!this.activated)
{
this.activated = true;
this.asmHook.Activate();
return;
}
this.asmHook.Enable();
}
/// <summary>
/// Stops intercepting a call to the function.
/// </summary>
public void Disable()
{
this.asmHook.Disable();
}
/// <summary>
/// Remove a hook from the current process.
/// </summary>
public void Dispose()
{
this.asmHook.Disable();
this.detour = null;
}
}

View file

@ -48,7 +48,7 @@ public abstract class Easing
/// Gets the current value of the animation, following unclamped logic. /// Gets the current value of the animation, following unclamped logic.
/// </summary> /// </summary>
[Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)]
[Api13ToDo("Map this field to ValueClamped, probably.")] [Api14ToDo("Map this field to ValueClamped, probably.")]
public double Value => this.ValueUnclamped; public double Value => this.ValueUnclamped;
/// <summary> /// <summary>

View file

@ -1,11 +1,9 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Utility; using Dalamud.Utility;
namespace Dalamud.Interface.Internal.Windows.Data.Widgets; namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
@ -48,97 +46,38 @@ public class AddonLifecycleWidget : IDataWindowWidget
return; return;
} }
if (ImGui.CollapsingHeader("Listeners"u8)) foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners)
{ {
ImGui.Indent(); using var eventId = ImRaii.PushId(eventType.ToString());
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<AddonEvent>())
{
if (ImGui.CollapsingHeader(eventType.ToString())) if (ImGui.CollapsingHeader(eventType.ToString()))
{ {
ImGui.Indent(); using var eventIndent = ImRaii.PushIndent();
var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList();
if (listeners.Count == 0) if (addonListeners.Count == 0)
{ {
ImGui.Text("No Listeners Registered for Event"u8); ImGui.Text("No Addons Registered for Event"u8);
} }
if (ImGui.BeginTable("AddonLifecycleListenersTable"u8, 2)) foreach (var (addonName, listeners) in addonListeners)
{ {
ImGui.TableSetupColumn("##AddonName"u8, ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale); using var addonId = ImRaii.PushId(addonName);
ImGui.TableSetupColumn("##MethodInvoke"u8, ImGuiTableColumnFlags.WidthStretch);
foreach (var listener in listeners) if (ImGui.CollapsingHeader(addonName.IsNullOrEmpty() ? "GLOBAL" : addonName))
{ {
ImGui.TableNextColumn(); using var addonIndent = ImRaii.PushIndent();
ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName);
ImGui.TableNextColumn(); if (listeners.Count == 0)
ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); {
ImGui.Text("No Listeners Registered for Event"u8);
}
foreach (var listener in listeners)
{
ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}");
}
} }
ImGui.EndTable();
} }
ImGui.Unindent();
}
}
}
private void DrawReceiveEventHooks()
{
if (!this.Ready) return;
var listeners = this.AddonLifecycle.ReceiveEventListeners;
if (listeners.Count == 0)
{
ImGui.Text("No ReceiveEvent Hooks are Registered"u8);
}
foreach (var receiveEventListener in this.AddonLifecycle.ReceiveEventListeners)
{
if (ImGui.CollapsingHeader(string.Join(", ", receiveEventListener.AddonNames)))
{
ImGui.Columns(2);
var functionAddress = receiveEventListener.FunctionAddress;
ImGui.Text("Hook Address"u8);
ImGui.NextColumn();
ImGui.Text($"0x{functionAddress:X} (ffxiv_dx11.exe+{functionAddress - Process.GetCurrentProcess().MainModule!.BaseAddress:X})");
ImGui.NextColumn();
ImGui.Text("Hook Status"u8);
ImGui.NextColumn();
if (receiveEventListener.Hook is null)
{
ImGui.Text("Hook is null"u8);
}
else
{
var color = receiveEventListener.Hook.IsEnabled ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed;
var text = receiveEventListener.Hook.IsEnabled ? "Enabled"u8 : "Disabled"u8;
ImGui.TextColored(color, text);
}
ImGui.Columns(1);
} }
} }
} }

View file

@ -5,7 +5,6 @@ using System.Threading.Tasks;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Hooking; using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Serilog; using Serilog;
@ -34,7 +33,7 @@ internal unsafe class HookWidget : IDataWindowWidget
private MessageBoxWDelegate? messageBoxWOriginal; private MessageBoxWDelegate? messageBoxWOriginal;
private AddonFinalizeDelegate? addonFinalizeOriginal; private AddonFinalizeDelegate? addonFinalizeOriginal;
private AddonLifecycleAddressResolver? address; private nint address;
private delegate int MessageBoxWDelegate( private delegate int MessageBoxWDelegate(
IntPtr hWnd, IntPtr hWnd,
@ -55,7 +54,7 @@ internal unsafe class HookWidget : IDataWindowWidget
public string DisplayName { get; init; } = "Hook"; public string DisplayName { get; init; } = "Hook";
/// <inheritdoc/> /// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = { "hook" }; public string[]? CommandShortcuts { get; init; } = ["hook"];
/// <inheritdoc/> /// <inheritdoc/>
public bool Ready { get; set; } public bool Ready { get; set; }
@ -65,8 +64,8 @@ internal unsafe class HookWidget : IDataWindowWidget
{ {
this.Ready = true; this.Ready = true;
this.address = new AddonLifecycleAddressResolver(); var sigScanner = Service<TargetSigScanner>.Get();
this.address.Setup(Service<TargetSigScanner>.Get()); this.address = sigScanner.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -224,7 +223,7 @@ internal unsafe class HookWidget : IDataWindowWidget
private IDalamudHook HookAddonFinalize() private IDalamudHook HookAddonFinalize()
{ {
var hook = Hook<AddonFinalizeDelegate>.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize); var hook = Hook<AddonFinalizeDelegate>.FromAddress(this.address, this.OnAddonFinalize);
this.addonFinalizeOriginal = hook.Original; this.addonFinalizeOriginal = hook.Original;
hook.Enable(); hook.Enable();

View file

@ -472,9 +472,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable
private unsafe void OnVersionStringDraw(AddonEvent ev, AddonArgs args) private unsafe void OnVersionStringDraw(AddonEvent ev, AddonArgs args)
{ {
if (args is not AddonDrawArgs drawArgs) return; if (ev is not (AddonEvent.PostDraw or AddonEvent.PreDraw)) return;
var addon = drawArgs.Addon.Struct; var addon = args.Addon.Struct;
var textNode = addon->GetTextNodeById(3); var textNode = addon->GetTextNodeById(3);
// look and feel init. should be harmless to set. // look and feel init. should be harmless to set.

View file

@ -4,7 +4,7 @@ namespace Dalamud.Utility;
/// Utility class for marking something to be changed for API 13, for ease of lookup. /// Utility class for marking something to be changed for API 13, for ease of lookup.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.All, Inherited = false)] [AttributeUsage(AttributeTargets.All, Inherited = false)]
internal sealed class Api13ToDoAttribute : Attribute internal sealed class Api14ToDoAttribute : Attribute
{ {
/// <summary> /// <summary>
/// Marks that this should be made internal. /// Marks that this should be made internal.
@ -12,11 +12,11 @@ internal sealed class Api13ToDoAttribute : Attribute
public const string MakeInternal = "Make internal."; public const string MakeInternal = "Make internal.";
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Api13ToDoAttribute"/> class. /// Initializes a new instance of the <see cref="Api14ToDoAttribute"/> class.
/// </summary> /// </summary>
/// <param name="what">The explanation.</param> /// <param name="what">The explanation.</param>
/// <param name="what2">The explanation 2.</param> /// <param name="what2">The explanation 2.</param>
public Api13ToDoAttribute(string what, string what2 = "") public Api14ToDoAttribute(string what, string what2 = "")
{ {
_ = what; _ = what;
_ = what2; _ = what2;

View file

@ -0,0 +1,25 @@
namespace Dalamud.Utility;
/// <summary>
/// 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.
/// </summary>
[AttributeUsage(AttributeTargets.All, Inherited = false)]
internal sealed class Api15ToDoAttribute : Attribute
{
/// <summary>
/// Marks that this should be made internal.
/// </summary>
public const string MakeInternal = "Make internal.";
/// <summary>
/// Initializes a new instance of the <see cref="Api15ToDoAttribute"/> class.
/// </summary>
/// <param name="what">The explanation.</param>
/// <param name="what2">The explanation 2.</param>
public Api15ToDoAttribute(string what, string what2 = "")
{
_ = what;
_ = what2;
}
}

View file

@ -79,7 +79,7 @@ public static partial class Util
/// <summary> /// <summary>
/// Gets the Dalamud version. /// Gets the Dalamud version.
/// </summary> /// </summary>
[Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] [Api14ToDo("Remove. Make both versions here internal. Add an API somewhere.")]
public static string AssemblyVersion { get; } = public static string AssemblyVersion { get; } =
Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString(); Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString();

View file

@ -5,7 +5,7 @@
<TargetFramework>net10.0-windows</TargetFramework> <TargetFramework>net10.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget> <PlatformTarget>x64</PlatformTarget>
<Platforms>x64</Platforms> <Platforms>x64</Platforms>
<LangVersion>13.0</LangVersion> <LangVersion>14.0</LangVersion>
<!-- Disable Intel CET. Causes crashes on unpatched Windows 10 systems. --> <!-- Disable Intel CET. Causes crashes on unpatched Windows 10 systems. -->
<!-- https://github.com/dotnet/runtime/issues/108589 --> <!-- https://github.com/dotnet/runtime/issues/108589 -->

View file

@ -238,7 +238,7 @@ public static unsafe partial class ImGui
ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar(
label, label,
ImGuiDataType.Float, ImGuiDataType.Float,
MemoryMarshal.Cast<Vector2, float>(new(ref v)), MemoryMarshal.Cast<Vector2, float>(new Span<Vector2>(ref v)),
vSpeed, vSpeed,
vMin, vMin,
vMax, vMax,
@ -251,7 +251,7 @@ public static unsafe partial class ImGui
ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar(
label, label,
ImGuiDataType.Float, ImGuiDataType.Float,
MemoryMarshal.Cast<Vector3, float>(new(ref v)), MemoryMarshal.Cast<Vector3, float>(new Span<Vector3>(ref v)),
vSpeed, vSpeed,
vMin, vMin,
vMax, vMax,
@ -264,7 +264,7 @@ public static unsafe partial class ImGui
ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar(
label, label,
ImGuiDataType.Float, ImGuiDataType.Float,
MemoryMarshal.Cast<Vector4, float>(new(ref v)), MemoryMarshal.Cast<Vector4, float>(new Span<Vector4>(ref v)),
vSpeed, vSpeed,
vMin, vMin,
vMax, vMax,

View file

@ -205,7 +205,7 @@ public static unsafe partial class ImGui
InputScalar( InputScalar(
label, label,
ImGuiDataType.Float, ImGuiDataType.Float,
MemoryMarshal.Cast<Vector2, float>(new(ref data)), MemoryMarshal.Cast<Vector2, float>(new Span<Vector2>(ref data)),
step, step,
stepFast, stepFast,
format.MoveOrDefault("%.3f"u8), format.MoveOrDefault("%.3f"u8),
@ -219,7 +219,7 @@ public static unsafe partial class ImGui
InputScalar( InputScalar(
label, label,
ImGuiDataType.Float, ImGuiDataType.Float,
MemoryMarshal.Cast<Vector3, float>(new(ref data)), MemoryMarshal.Cast<Vector3, float>(new Span<Vector3>(ref data)),
step, step,
stepFast, stepFast,
format.MoveOrDefault("%.3f"u8), format.MoveOrDefault("%.3f"u8),
@ -233,7 +233,7 @@ public static unsafe partial class ImGui
InputScalar( InputScalar(
label, label,
ImGuiDataType.Float, ImGuiDataType.Float,
MemoryMarshal.Cast<Vector4, float>(new(ref data)), MemoryMarshal.Cast<Vector4, float>(new Span<Vector4>(ref data)),
step, step,
stepFast, stepFast,
format.MoveOrDefault("%.3f"u8), format.MoveOrDefault("%.3f"u8),

View file

@ -210,7 +210,7 @@ public static unsafe partial class ImGui
ImU8String format = default, ImGuiSliderFlags flags = ImGuiSliderFlags.None) => SliderScalar( ImU8String format = default, ImGuiSliderFlags flags = ImGuiSliderFlags.None) => SliderScalar(
label, label,
ImGuiDataType.Float, ImGuiDataType.Float,
MemoryMarshal.Cast<Vector2, float>(new(ref v)), MemoryMarshal.Cast<Vector2, float>(new Span<Vector2>(ref v)),
vMin, vMin,
vMax, vMax,
format.MoveOrDefault("%.3f"u8), format.MoveOrDefault("%.3f"u8),
@ -222,7 +222,7 @@ public static unsafe partial class ImGui
SliderScalar( SliderScalar(
label, label,
ImGuiDataType.Float, ImGuiDataType.Float,
MemoryMarshal.Cast<Vector3, float>(new(ref v)), MemoryMarshal.Cast<Vector3, float>(new Span<Vector3>(ref v)),
vMin, vMin,
vMax, vMax,
format.MoveOrDefault("%.3f"u8), format.MoveOrDefault("%.3f"u8),
@ -236,7 +236,7 @@ public static unsafe partial class ImGui
SliderScalar( SliderScalar(
label, label,
ImGuiDataType.Float, ImGuiDataType.Float,
MemoryMarshal.Cast<Vector4, float>(new(ref v)), MemoryMarshal.Cast<Vector4, float>(new Span<Vector4>(ref v)),
vMin, vMin,
vMax, vMax,
format.MoveOrDefault("%.3f"u8), format.MoveOrDefault("%.3f"u8),

View file

@ -156,7 +156,7 @@ public ref struct ImU8String
return this.rentedBuffer is { } buf return this.rentedBuffer is { } buf
? buf.AsSpan() ? buf.AsSpan()
: MemoryMarshal.Cast<FixedBufferContainer, byte>(new(ref Unsafe.AsRef(ref this.fixedBuffer))); : MemoryMarshal.Cast<FixedBufferContainer, byte>(new Span<FixedBufferContainer>(ref Unsafe.AsRef(ref this.fixedBuffer)));
} }
} }
@ -165,7 +165,7 @@ public ref struct ImU8String
private ref byte FixedBufferByteRef => ref this.FixedBufferSpan[0]; private ref byte FixedBufferByteRef => ref this.FixedBufferSpan[0];
private Span<byte> FixedBufferSpan => private Span<byte> FixedBufferSpan =>
MemoryMarshal.Cast<FixedBufferContainer, byte>(new(ref Unsafe.AsRef(ref this.fixedBuffer))); MemoryMarshal.Cast<FixedBufferContainer, byte>(new Span<FixedBufferContainer>(ref Unsafe.AsRef(ref this.fixedBuffer)));
public static implicit operator ImU8String(ReadOnlySpan<byte> text) => new(text); public static implicit operator ImU8String(ReadOnlySpan<byte> text) => new(text);
public static implicit operator ImU8String(ReadOnlyMemory<byte> text) => new(text); public static implicit operator ImU8String(ReadOnlyMemory<byte> text) => new(text);