diff --git a/HellionChat/AutoTellTabsService.cs b/HellionChat/AutoTellTabsService.cs index 8bc3436..64eb8f5 100644 --- a/HellionChat/AutoTellTabsService.cs +++ b/HellionChat/AutoTellTabsService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using HellionChat.Code; @@ -19,6 +20,14 @@ internal sealed class AutoTellTabsService : IDisposable private readonly MessageStore _store; private readonly object _tempTabsLock = new(); + // F2.1: lock-free counter mirrors Config.Tabs.Count(IsTempTab) so the + // hot-path getter doesn't contend with HandleTell on every render frame. + // Bumped from inside the existing mutation paths so it stays consistent + // with the underlying list — see SpawnTempTab, DropOldestTempTab, OnLogout + // and ResyncTempTabCounter (used by Plugin.cs snapshot-restore). + // TEST-MIRROR: ../../Hellion Build test/_Helpers/TempTabCounterTests.cs + private int _activeTempTabCount; + private bool _initialized; internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store) @@ -28,16 +37,7 @@ internal sealed class AutoTellTabsService : IDisposable _store = store; } - internal int ActiveTempTabCount - { - get - { - lock (_tempTabsLock) - { - return Plugin.Config.Tabs.Count(t => t.IsTempTab); - } - } - } + internal int ActiveTempTabCount => Volatile.Read(ref _activeTempTabCount); internal void Initialize() { @@ -46,11 +46,31 @@ internal sealed class AutoTellTabsService : IDisposable return; } + // Seed the counter from the persisted Tabs list so a config that already + // contains TempTabs from a prior session starts in sync. Plugin.cs:168 + // crash-recovery has already dropped TempTabs by the time we get here, + // so the snapshot reflects post-recovery reality. + Interlocked.Exchange( + ref _activeTempTabCount, + Plugin.Config.Tabs.Count(t => t.IsTempTab) + ); + _messageManager.MessageProcessed += HandleTell; Plugin.ClientState.Logout += OnLogout; _initialized = true; } + // F2.1: callable from outside paths that mutate Config.Tabs directly + // (Plugin.cs snapshot-restore). Atomically re-pegs the counter to the + // live IsTempTab count. + internal void ResyncTempTabCounter() + { + Interlocked.Exchange( + ref _activeTempTabCount, + Plugin.Config.Tabs.Count(t => t.IsTempTab) + ); + } + public void Dispose() { if (!_initialized) @@ -184,6 +204,7 @@ internal sealed class AutoTellTabsService : IDisposable } Plugin.Config.Tabs.RemoveAt(victim.Index); + Interlocked.Decrement(ref _activeTempTabCount); // Re-anchor active tab to avoid silent switch when tab is dropped if (victim.Index <= _plugin.LastTab) @@ -208,6 +229,7 @@ internal sealed class AutoTellTabsService : IDisposable } Plugin.Config.Tabs.Add(tab); + Interlocked.Increment(ref _activeTempTabCount); } private static Tab BuildTempTab(string playerName, uint worldRowId) @@ -361,7 +383,8 @@ internal sealed class AutoTellTabsService : IDisposable } } - Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab); + var removed = Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab); + Interlocked.Add(ref _activeTempTabCount, -removed); // Force switch to tab 0 if active tab was temp or index is now out of range var stillValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count; diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index a8411f5..0a33a1c 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -641,6 +641,11 @@ public sealed class Plugin : IAsyncDalamudPlugin Config.Tabs.Clear(); Config.Tabs.AddRange(snapshot); + + // F2.1: snapshot-restore preserves IsTempTab tabs but the mid-step + // RemoveAll bypasses AutoTellTabsService, so re-peg the counter. + // Null-conditional because SaveConfig can fire before Phase-2 init. + AutoTellTabsService?.ResyncTempTabCounter(); } internal void LanguageChanged(string langCode)