From 13980542168073087f38cf6da09516d8aa0d8ced Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 4 Jan 2026 14:03:15 -0800 Subject: [PATCH 1/2] Push AddonLifecycle event register/unregister to main thread --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 78cea1a0f..4da8e429c 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -27,6 +27,9 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private static readonly ModuleLog Log = new("AddonLifecycle"); + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] @@ -58,20 +61,23 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - if (!this.EventListeners.ContainsKey(listener.EventType)) + this.framework.RunOnFrameworkThread(() => { - if (!this.EventListeners.TryAdd(listener.EventType, [])) - return; - } + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + if (!this.EventListeners.TryAdd(listener.EventType, [])) + return; + } - // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type - if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) - { - if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, [])) - return; - } + // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) + { + if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, [])) + return; + } - this.EventListeners[listener.EventType][listener.AddonName].Add(listener); + this.EventListeners[listener.EventType][listener.AddonName].Add(listener); + }); } /// @@ -80,13 +86,16 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) + this.framework.RunOnFrameworkThread(() => { - if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) { - addonListener.Remove(listener); + if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + { + addonListener.Remove(listener); + } } - } + }); } /// From 36c3429566478cffa01a2aa4677299d528c4b48b Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 4 Jan 2026 14:41:30 -0800 Subject: [PATCH 2/2] Force to next tick instead of running immediately --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 4da8e429c..d07821149 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -31,6 +31,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private readonly Framework framework = Service.Get(); private Hook? onInitializeAddonHook; + private bool isInvokingListeners = false; [ServiceManager.ServiceConstructor] private AddonLifecycle() @@ -61,7 +62,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnFrameworkThread(() => + this.framework.RunOnTick(() => { if (!this.EventListeners.ContainsKey(listener.EventType)) { @@ -77,7 +78,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } this.EventListeners[listener.EventType][listener.AddonName].Add(listener); - }); + }, delayTicks: this.isInvokingListeners ? 1 : 0); } /// @@ -86,7 +87,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnFrameworkThread(() => + this.framework.RunOnTick(() => { if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) { @@ -95,7 +96,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService addonListener.Remove(listener); } } - }); + }, delayTicks: this.isInvokingListeners ? 1 : 0); } /// @@ -106,6 +107,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// What to blame on errors. internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { + this.isInvokingListeners = true; + // Early return if we don't have any listeners of this type if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return; @@ -140,6 +143,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } } } + + this.isInvokingListeners = false; } ///