diff --git a/HellionChat/AutoTellTabsService.cs b/HellionChat/AutoTellTabsService.cs index 59413d3..340e309 100644 --- a/HellionChat/AutoTellTabsService.cs +++ b/HellionChat/AutoTellTabsService.cs @@ -52,11 +52,33 @@ internal sealed class AutoTellTabsService : IDisposable return; } + // Pinned tabs come out of the JSON with TellTarget set but + // CurrentChannel reset (NonSerialized). Without re-seeding, the chat + // input has no tell-target on the active pinned tab, and the + // game-side channel hook only repaints CurrentChannel once the user + // triggers a /tell or channel switch. + RehydratePinnedTabs(); + _messageManager.MessageProcessed += HandleTell; Plugin.ClientState.Logout += OnLogout; _initialized = true; } + private static void RehydratePinnedTabs() + { + foreach (var tab in Plugin.Config.Tabs) + { + if (!TabLifecycleHelpers.IsInPinnedPool(tab)) + continue; + if (tab.TellTarget is null || !tab.TellTarget.IsSet()) + continue; + + tab.Channel ??= InputChannel.Tell; + tab.CurrentChannel.Channel = InputChannel.Tell; + tab.CurrentChannel.TellTarget = tab.TellTarget.Clone(); + } + } + public void Dispose() { if (!_initialized) @@ -102,7 +124,23 @@ internal sealed class AutoTellTabsService : IDisposable var existing = FindTempTab(partner.Value.Name, partner.Value.World); if (existing != null) { - // Already routed via MessageManager pipeline + // Already routed via MessageManager pipeline. Repair the + // tell-target if the fallback hit a pinned tab whose + // TellTarget didn't survive a previous round-trip — keeps + // FindTempTab fast on the next message. + if ( + existing.IsPinned + && (existing.TellTarget is null || !existing.TellTarget.IsSet()) + ) + { + existing.TellTarget = new TellTarget( + partner.Value.Name, + partner.Value.World, + 0, + TellReason.Direct + ); + _plugin.SaveConfig(); + } return; } @@ -152,14 +190,25 @@ internal sealed class AutoTellTabsService : IDisposable return null; } - private Tab? FindTempTab(string name, uint world) + private static Tab? FindTempTab(string name, uint world) { - return Plugin.Config.Tabs.FirstOrDefault(t => + var byTarget = Plugin.Config.Tabs.FirstOrDefault(t => t.IsTempTab && t.TellTarget != null && string.Equals(t.TellTarget.Name, name, StringComparison.OrdinalIgnoreCase) && t.TellTarget.World == world ); + if (byTarget != null) + return byTarget; + + // Fallback: match by tab name. Pinned tabs are named via + // FormatTabName(player, world) at spawn time, so the name is a + // stable secondary key when TellTarget didn't survive a save/load + // (older configs from a renamed pin, malformed migrations, etc.). + var expectedName = FormatTabName(name, world); + return Plugin.Config.Tabs.FirstOrDefault(t => + t.IsTempTab && string.Equals(t.Name, expectedName, StringComparison.OrdinalIgnoreCase) + ); } internal void DropOldestTempTab()