- gate keybind pill-sync on IsChannelOrExistingLinkshell so an empty linkshell slot no longer desyncs the pill from the real send channel - close manually-popped pop-out windows on logout via an IsOpen filter instead of the PopOut flag (which manual pops never set) - read the router's tell-tab lookup through a lock-wrapped accessor so the framework thread cannot enumerate Config.Tabs mid worker-thread mutation - add a "switch on every tell" toggle (default on) and make the auto-open mode pick the matching layout, so Sidebar vs Top-tab are distinct - comment corrections (stale/contradictory text, TEST-MIRROR path depth)
98 lines
4.3 KiB
C#
98 lines
4.3 KiB
C#
using HellionChat.Code;
|
|
using HellionChat.GameFunctions.Types;
|
|
|
|
namespace HellionChat.Util;
|
|
|
|
// Pure predicates for the TempTab pin lifecycle. Extracted from the strip
|
|
// sites in Plugin.cs and Configuration.cs so they stay in lockstep — a
|
|
// load-time strip that disagrees with the save-time strip is exactly how
|
|
// pinned tabs would silently fall out of the JSON.
|
|
internal static class TabLifecycleHelpers
|
|
{
|
|
public static bool IsInUnpinnedPool(Tab t) => t.IsTempTab && !t.IsPinned;
|
|
|
|
public static bool IsInPinnedPool(Tab t) => t.IsTempTab && t.IsPinned;
|
|
|
|
public static bool ShouldStripOnLoad(Tab t) => IsInUnpinnedPool(t);
|
|
|
|
public static bool ShouldStripOnSave(Tab t) => IsInUnpinnedPool(t);
|
|
|
|
// Stale-tell strip + channel derive, run at every tab activation. When a
|
|
// DIFFERENT tab becomes the input surface, drop any runtime tell state the
|
|
// game-side detour left on it (the CurrentChannel tell target plus the
|
|
// partner-name label) so a normal typed line cannot route as a silent /tell
|
|
// to the old partner — the same privacy guard StripTellBindingOnPromote
|
|
// applies on promote. Re-activating the already-active tab must NOT strip
|
|
// (a live game-tell would lose its context, TR-4); a tab carrying its own
|
|
// Tab.TellTarget is a real tell binding (leg1) and is left intact.
|
|
internal static void OnTabActivated(Tab tab, Tab? previous)
|
|
{
|
|
if (
|
|
!ReferenceEquals(tab, previous)
|
|
&& tab.CurrentChannel.Channel == InputChannel.Tell
|
|
&& tab.TellTarget?.IsSet() != true
|
|
)
|
|
{
|
|
tab.CurrentChannel.SetChannel(InputChannel.Invalid);
|
|
tab.CurrentChannel.TellTarget = null;
|
|
tab.CurrentChannel.ResetTempChannel();
|
|
// Label chunks carry the partner name after a game-side tell.
|
|
tab.CurrentChannel.Name = [];
|
|
}
|
|
|
|
EnsureCurrentChannel(tab);
|
|
}
|
|
|
|
// Pure derive-helper: resolves a tab's input channel from its
|
|
// SelectedChannels when none is set yet. Reached only via OnTabActivated
|
|
// now, so the strip and the derive stay in lockstep at every entry.
|
|
internal static void EnsureCurrentChannel(Tab tab)
|
|
{
|
|
if (tab.CurrentChannel.Channel != InputChannel.Invalid)
|
|
return;
|
|
|
|
foreach (var chatType in tab.SelectedChannels.Keys)
|
|
{
|
|
if (chatType.ToInputChannel() is { } input)
|
|
{
|
|
tab.CurrentChannel.SetChannel(input);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drops a temp/pinned tell tab's binding when it is promoted to a permanent
|
|
// tab. Beyond the obvious IsTempTab/IsPinned/Tab.TellTarget reset, this also
|
|
// clears the RUNTIME channel's tell state — that part is the CORR-1 guard:
|
|
// a spawned tell tab carries CurrentChannel.Channel == Tell plus a resolvable
|
|
// CurrentChannel.TellTarget, and neither is touched by clearing Tab.TellTarget
|
|
// alone. Without this clear the input bar would route a normal typed line on
|
|
// the promoted tab silently as /tell to the OLD partner (a privacy misfire the
|
|
// current==Tell routing gate cannot catch, because current here really IS
|
|
// Tell). Channel -> Invalid so the next sidebar/top-bar click re-derives the
|
|
// channel from SelectedChannels via EnsureCurrentChannel like any normal tab;
|
|
// the worst residual is a "/t" with no target, which the game rejects without
|
|
// sending (same safe class as the COMP-1 fall-through, no silent send).
|
|
internal static void StripTellBindingOnPromote(Tab tab)
|
|
{
|
|
tab.IsTempTab = false;
|
|
tab.IsPinned = false;
|
|
tab.TellTarget = TellTarget.Empty();
|
|
tab.Channel = null;
|
|
tab.CurrentChannel.SetChannel(InputChannel.Invalid);
|
|
tab.CurrentChannel.TellTarget = null;
|
|
tab.CurrentChannel.ResetTempChannel();
|
|
}
|
|
|
|
// Wrap-around tab index for keybind cycling. Pure so the Build-Suite can test the
|
|
// wrap math without a live window. count == 0 returns 0 (the caller dead-zones
|
|
// before activating); negative deltas wrap correctly via the double-mod.
|
|
// TEST-MIRROR: ../../../Hellion Build test/_Helpers/TabLifecycleHelpersTests.cs
|
|
internal static int WrapTabIndex(int current, int delta, int count)
|
|
{
|
|
if (count <= 0)
|
|
return 0;
|
|
return ((current + delta) % count + count) % count;
|
|
}
|
|
}
|