From 0bfcc557749df00f92b824c3d062ee10cfbf5a13 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 8 Dec 2023 08:49:09 +0900 Subject: [PATCH] Reduce heap allocation every frame in AddonLifecycle (#1555) --- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 35 +++- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 8 +- .../AddonArgTypes/AddonFinalizeArgs.cs | 8 +- .../AddonArgTypes/AddonReceiveEventArgs.cs | 24 ++- .../AddonArgTypes/AddonRefreshArgs.cs | 16 +- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 16 +- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 16 +- .../AddonArgTypes/AddonUpdateArgs.cs | 10 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 188 ++++++------------ .../AddonLifecycleReceiveEventListener.cs | 47 ++--- Dalamud/Memory/MemoryHelper.cs | 32 +++ 11 files changed, 212 insertions(+), 188 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 334542c71..077ca7c93 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -14,22 +14,51 @@ public abstract unsafe class AddonArgs public const string InvalidAddon = "NullAddon"; private string? addonName; + private IntPtr addon; /// /// Gets the name of the addon this args referrers to. /// public string AddonName => this.GetAddonName(); - + /// /// Gets the pointer to the addons AtkUnitBase. /// - public nint Addon { get; init; } - + public nint Addon + { + get => this.addon; + internal set + { + if (this.addon == value) + return; + + this.addon = value; + this.addonName = null; + } + } + /// /// Gets the type of these args. /// public abstract AddonArgsType Type { get; } + /// + /// Checks if addon name matches the given span of char. + /// + /// The name to check. + /// Whether it is the case. + internal bool IsAddon(ReadOnlySpan name) + { + if (this.Addon == nint.Zero) return false; + if (name.Length is 0 or > 0x20) + return false; + + var addonPointer = (AtkUnitBase*)this.Addon; + if (addonPointer->Name is null) return false; + + return MemoryHelper.EqualsZeroTerminatedString(name, (nint)addonPointer->Name, null, 0x20); + } + /// /// Helper method for ensuring the name of the addon is valid. /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 10d46a573..1e1013dd5 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -3,8 +3,14 @@ /// /// Addon argument data for Draw events. /// -public class AddonDrawArgs : AddonArgs +public class AddonDrawArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Draw; + + /// + public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index caf422927..fc26a6c33 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -3,8 +3,14 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonFinalizeArgs : AddonArgs +public class AddonFinalizeArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Finalize; + + /// + public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index df75307f1..8f9003b4c 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -3,28 +3,34 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonReceiveEventArgs : AddonArgs +public class AddonReceiveEventArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.ReceiveEvent; /// - /// Gets the AtkEventType for this event message. + /// Gets or sets the AtkEventType for this event message. /// - public byte AtkEventType { get; init; } + public byte AtkEventType { get; set; } /// - /// Gets the event id for this event message. + /// Gets or sets the event id for this event message. /// - public int EventParam { get; init; } + public int EventParam { get; set; } /// - /// Gets the pointer to an AtkEvent for this event message. + /// Gets or sets the pointer to an AtkEvent for this event message. /// - public nint AtkEvent { get; init; } + public nint AtkEvent { get; set; } /// - /// Gets the pointer to a block of data for this event message. + /// Gets or sets the pointer to a block of data for this event message. /// - public nint Data { get; init; } + public nint Data { get; set; } + + /// + public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index b6ac6d8b6..bfcf02544 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -5,23 +5,29 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Refresh events. /// -public class AddonRefreshArgs : AddonArgs +public class AddonRefreshArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Refresh; /// - /// Gets the number of AtkValues. + /// Gets or sets the number of AtkValues. /// - public uint AtkValueCount { get; init; } + public uint AtkValueCount { get; set; } /// - /// Gets the address of the AtkValue array. + /// Gets or sets the address of the AtkValue array. /// - public nint AtkValues { get; init; } + public nint AtkValues { get; set; } /// /// Gets the AtkValues in the form of a span. /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); + + /// + public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index 1b743b31a..219288ccf 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -3,18 +3,24 @@ /// /// Addon argument data for OnRequestedUpdate events. /// -public class AddonRequestedUpdateArgs : AddonArgs +public class AddonRequestedUpdateArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.RequestedUpdate; /// - /// Gets the NumberArrayData** for this event. + /// Gets or sets the NumberArrayData** for this event. /// - public nint NumberArrayData { get; init; } + public nint NumberArrayData { get; set; } /// - /// Gets the StringArrayData** for this event. + /// Gets or sets the StringArrayData** for this event. /// - public nint StringArrayData { get; init; } + public nint StringArrayData { get; set; } + + /// + public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index df2ec26be..bd60879b8 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -5,23 +5,29 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Setup events. /// -public class AddonSetupArgs : AddonArgs +public class AddonSetupArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Setup; /// - /// Gets the number of AtkValues. + /// Gets or sets the number of AtkValues. /// - public uint AtkValueCount { get; init; } + public uint AtkValueCount { get; set; } /// - /// Gets the address of the AtkValue array. + /// Gets or sets the address of the AtkValue array. /// - public nint AtkValues { get; init; } + public nint AtkValues { get; set; } /// /// Gets the AtkValues in the form of a span. /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); + + /// + public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index 651fbcafb..b087ac15a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// -public class AddonUpdateArgs : AddonArgs +public class AddonUpdateArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Update; @@ -11,5 +11,11 @@ public class AddonUpdateArgs : AddonArgs /// /// Gets the time since the last update. /// - public float TimeDelta { get; init; } + public float TimeDelta { get; internal set; } + + /// + public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 3528de562..decb7a9f4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Hooking; @@ -40,6 +41,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); + // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet + // package, and these events are always called from the main thread, this is fine. + private readonly AddonSetupArgs recyclingSetupArgs = new(); + private readonly AddonFinalizeArgs recyclingFinalizeArgs = new(); + private readonly AddonDrawArgs recyclingDrawArgs = new(); + private readonly AddonUpdateArgs recyclingUpdateArgs = new(); + private readonly AddonRefreshArgs recyclingRefreshArgs = new(); + private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new(); + [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) { @@ -132,12 +142,27 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType /// /// Event Type. /// AddonArgs. - internal void InvokeListeners(AddonEvent eventType, AddonArgs args) + /// What to blame on errors. + internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { - // Match on string.empty for listeners that want events for all addons. - foreach (var listener in this.EventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) + // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. + foreach (var listener in this.EventListeners) { - listener.FunctionDelegate.Invoke(eventType, args); + if (listener.EventType != eventType) + 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); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke."); + } } } @@ -249,20 +274,13 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); } - - try - { - this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonSetup pre-setup invoke."); - } + + this.recyclingSetupArgs.Addon = (nint)addon; + this.recyclingSetupArgs.AtkValueCount = valueCount; + this.recyclingSetupArgs.AtkValues = (nint)values; + this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs); + valueCount = this.recyclingSetupArgs.AtkValueCount; + values = (AtkValue*)this.recyclingSetupArgs.AtkValues; try { @@ -273,19 +291,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonSetup post-setup invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostSetup, this.recyclingSetupArgs); } private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) @@ -299,15 +305,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); } - - try - { - this.InvokeListeners(AddonEvent.PreFinalize, new AddonFinalizeArgs { Addon = (nint)atkUnitBase[0] }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonFinalize pre-finalize invoke."); - } + + this.recyclingFinalizeArgs.Addon = (nint)atkUnitBase[0]; + this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs); try { @@ -321,14 +321,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonDraw(AtkUnitBase* addon) { - try - { - this.InvokeListeners(AddonEvent.PreDraw, new AddonDrawArgs { Addon = (nint)addon }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); - } + this.recyclingDrawArgs.Addon = (nint)addon; + this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs); try { @@ -339,26 +333,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostDraw, new AddonDrawArgs { Addon = (nint)addon }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonDraw post-draw invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostDraw, this.recyclingDrawArgs); } private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - try - { - this.InvokeListeners(AddonEvent.PreUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonUpdate pre-update invoke."); - } + this.recyclingUpdateArgs.Addon = (nint)addon; + this.recyclingUpdateArgs.TimeDelta = delta; + this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs); try { @@ -369,33 +351,19 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonUpdate post-update invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostUpdate, this.recyclingUpdateArgs); } private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) { byte result = 0; - - try - { - this.InvokeListeners(AddonEvent.PreRefresh, new AddonRefreshArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke."); - } + + this.recyclingRefreshArgs.Addon = (nint)addon; + this.recyclingRefreshArgs.AtkValueCount = valueCount; + this.recyclingRefreshArgs.AtkValues = (nint)values; + this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs); + valueCount = this.recyclingRefreshArgs.AtkValueCount; + values = (AtkValue*)this.recyclingRefreshArgs.AtkValues; try { @@ -406,38 +374,18 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostRefresh, new AddonRefreshArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke."); - } - + this.InvokeListenersSafely(AddonEvent.PostRefresh, this.recyclingRefreshArgs); return result; } private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - try - { - this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonRequestedUpdateArgs - { - Addon = (nint)addon, - NumberArrayData = (nint)numberArrayData, - StringArrayData = (nint)stringArrayData, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke."); - } + this.recyclingRequestedUpdateArgs.Addon = (nint)addon; + this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData; + this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData; + this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs); + numberArrayData = (NumberArrayData**)this.recyclingRequestedUpdateArgs.NumberArrayData; + stringArrayData = (StringArrayData**)this.recyclingRequestedUpdateArgs.StringArrayData; try { @@ -448,19 +396,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonRequestedUpdateArgs - { - Addon = (nint)addon, - NumberArrayData = (nint)numberArrayData, - StringArrayData = (nint)stringArrayData, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.recyclingRequestedUpdateArgs); } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs index 10171eb16..1c138e447 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs @@ -16,6 +16,10 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable { private static readonly ModuleLog Log = new("AddonLifecycle"); + // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet + // package, and these events are always called from the main thread, this is fine. + private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new(); + /// /// Initializes a new instance of the class. /// @@ -74,22 +78,17 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable this.Hook!.Original(addon, eventType, eventParam, atkEvent, data); return; } - - try - { - this.AddonLifecycle.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs - { - Addon = (nint)addon, - AtkEventType = (byte)eventType, - EventParam = eventParam, - AtkEvent = (nint)atkEvent, - Data = data, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke."); - } + + this.recyclingReceiveEventArgs.Addon = (nint)addon; + this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType; + this.recyclingReceiveEventArgs.EventParam = eventParam; + this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent; + this.recyclingReceiveEventArgs.Data = data; + this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.recyclingReceiveEventArgs); + eventType = (AtkEventType)this.recyclingReceiveEventArgs.AtkEventType; + eventParam = this.recyclingReceiveEventArgs.EventParam; + atkEvent = (AtkEvent*)this.recyclingReceiveEventArgs.AtkEvent; + data = this.recyclingReceiveEventArgs.Data; try { @@ -100,20 +99,6 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.AddonLifecycle.InvokeListeners(AddonEvent.PostReceiveEvent, new AddonReceiveEventArgs - { - Addon = (nint)addon, - AtkEventType = (byte)eventType, - EventParam = eventParam, - AtkEvent = (nint)atkEvent, - Data = data, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRefresh post-receiveEvent invoke."); - } + this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.recyclingReceiveEventArgs); } } diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs index 3ceecf6a6..552817646 100644 --- a/Dalamud/Memory/MemoryHelper.cs +++ b/Dalamud/Memory/MemoryHelper.cs @@ -163,6 +163,38 @@ public static unsafe class MemoryHelper #region ReadString + /// + /// Compares if the given char span equals to the null-terminated string at . + /// + /// The character span. + /// The address of null-terminated string. + /// The encoding of the null-terminated string. + /// The maximum length of the null-terminated string. + /// Whether they are equal. + public static bool EqualsZeroTerminatedString( + ReadOnlySpan charSpan, + nint memoryAddress, + Encoding? encoding = null, + int maxLength = int.MaxValue) + { + encoding ??= Encoding.UTF8; + maxLength = Math.Min(maxLength, charSpan.Length + 4); + + var pmem = ((byte*)memoryAddress)!; + var length = 0; + while (length < maxLength && pmem[length] != 0) + length++; + + var mem = new Span(pmem, length); + var memCharCount = encoding.GetCharCount(mem); + if (memCharCount != charSpan.Length) + return false; + + Span chars = stackalloc char[memCharCount]; + encoding.GetChars(mem, chars); + return charSpan.SequenceEqual(chars); + } + /// /// Read a UTF-8 encoded string from a specified memory address. ///