fix(tabs): rehydrate pinned TempTab tell-targets after reload
Smoke-test (Jin's scenario) surfaced two coupled bugs in v1.4.7 pin persistence: 1. The chat input couldn't send to the pinned partner after a reload. Tab.CurrentChannel is NonSerialized, so it came back as a fresh UsedChannel with TellTarget=null even though tab.TellTarget (the persisted twin) was intact. The game-side channel hook only repaints CurrentChannel on a /tell or channel switch, so the pinned tab sat there mute until the user manually re-bounced the channel. 2. An incoming tell from the pinned partner spawned a *second* TempTab instead of routing into the existing pinned one. The Name+World lookup in FindTempTab was vulnerable to any round-trip nuance on tab.TellTarget — the fallback path now matches by tab name, which FormatTabName pins at spawn time. Fix: - AutoTellTabsService.Initialize now calls RehydratePinnedTabs() after the Phase-2 wiring lands, seeding tab.CurrentChannel.TellTarget + Channel from the persisted tab.TellTarget. Channel is also defaulted to InputChannel.Tell on the tab record so the chat-input bar paints Tell mode immediately on first selection. - FindTempTab gained a Name-based fallback for the case where the primary TellTarget lookup misses (e.g. a pinned tab whose TellTarget didn't round-trip cleanly through an old save). - HandleTell self-heals: when the fallback matches a pinned tab with a missing TellTarget, the tab is repaired from the live partner data and persisted, so subsequent messages take the fast path. Build-suite coverage was attempted (PinnedTabJsonRoundtripTests) but Tab + TellTarget are both Dalamud-coupled — Newtonsoft's reflection walk loads Dalamud.dll which isn't available in the xUnit AppDomain (documented in feedback_dalamud_test_isolation). Verification stays on the ingame smoke path.
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user