fix(tell): restore outgoing tell routing from the input bar
Input-bar tells went out as a bare "/t" without the target, so the game
rejected them with "you must add the World name". Rebuild the full
"/tell name@world" from the 1.5.6 target chain in a pure BuildOutgoing:
- leg2/leg3 gated on current == Tell so a stale tell target on a Say tab
can't send a say line silently as /tell (CORR-1)
- world-resolve gate: an unresolvable world falls back to the channel
prefix, never "/tell name@ text" (COMP-1)
- ResetTempChannel after the send, tell-only
Also clear the runtime tell state on PromoteToPermanent so a promoted tab
can't route a typed line to the old partner, and surface the tell partner
("-> name@world") in the channel pill so a misfire stays visible. Adds
tell-routing and pill-transparency self tests.
This commit is contained in:
@@ -8,6 +8,7 @@ using Dalamud.Interface.Utility.Raii;
|
||||
using HellionChat._Helpers;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Themes;
|
||||
using HellionChat.Ui;
|
||||
@@ -201,6 +202,34 @@ internal sealed class InputBar
|
||||
// saved default and is null for most non-FC tabs, which produced
|
||||
// the "—" placeholder users saw.
|
||||
var current = tab?.CurrentChannel?.Channel ?? InputChannel.Invalid;
|
||||
|
||||
// Privacy transparency: a game-side tell or reply writes {Channel=Tell,
|
||||
// TellTarget} onto the active tab's CurrentChannel even on a NORMAL tab
|
||||
// (Tab.TellTarget stays empty, so the isTell branch above is false). In
|
||||
// that state BuildOutgoing's leg2/leg3 would route the next typed line as
|
||||
// /tell to that partner — but the bare "Tell" label hid WHO. Mirror the
|
||||
// exact leg2/leg3 source (current==Tell, TempTellTarget ?? TellTarget) AND
|
||||
// the COMP-1 world-resolve gate, so the pill names the partner ONLY when a
|
||||
// /tell would actually be built; an unresolvable world sends no /tell and
|
||||
// falls through to the plain label below. Read-only — no routing effect.
|
||||
// 1.5.6 showed the partner name here; this restores that transparency.
|
||||
if (current == InputChannel.Tell)
|
||||
{
|
||||
// Mirror BuildOutgoing's exact target chain for the tell channel (leg1
|
||||
// Tab.TellTarget first, then leg2/leg3 CurrentChannel) so the pill names
|
||||
// precisely who the next line would reach — no drift between shown and sent.
|
||||
var ccTarget =
|
||||
tab is not null && tab.TellTarget.IsSet()
|
||||
? tab.TellTarget
|
||||
: tab?.CurrentChannel?.TempTellTarget ?? tab?.CurrentChannel?.TellTarget;
|
||||
if (ccTarget is not null && ccTarget.IsSet())
|
||||
{
|
||||
var world = ccTarget.ToWorldString();
|
||||
if (!string.IsNullOrEmpty(world))
|
||||
return $"→ {ccTarget.Name}@{world}";
|
||||
}
|
||||
}
|
||||
|
||||
if (current != InputChannel.Invalid)
|
||||
return current.ToChatType().Name();
|
||||
|
||||
@@ -382,20 +411,11 @@ internal sealed class InputBar
|
||||
}
|
||||
_disclosureArmedBuffer = null;
|
||||
|
||||
// Slash commands route through verbatim — the game's chat parser
|
||||
// handles /tell, /fc, /hellion etc. on its own. Other text gets
|
||||
// the active channel's prefix so the line lands on the channel
|
||||
// the user is reading instead of the game-side default.
|
||||
string toSend;
|
||||
if (text.StartsWith('/'))
|
||||
{
|
||||
toSend = text;
|
||||
}
|
||||
else
|
||||
{
|
||||
var current = activeTab?.CurrentChannel?.Channel ?? InputChannel.Invalid;
|
||||
toSend = current == InputChannel.Invalid ? text : $"{current.Prefix()} {text}";
|
||||
}
|
||||
// Route the trimmed buffer into the exact send string. BuildOutgoing is
|
||||
// pure (no send, no field write) so the SelfTest can exercise the tell
|
||||
// routing without firing a real chat line; the wasTell flag drives the
|
||||
// post-send ResetTempChannel below.
|
||||
var (toSend, wasTell) = BuildOutgoing(activeTab, text);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -415,6 +435,13 @@ internal sealed class InputBar
|
||||
}
|
||||
ChatBox.SendMessageUnsafe(bytes);
|
||||
_pendingMessage = string.Empty;
|
||||
|
||||
// 1.5.6 parity (1d3b429:ChatLogWindow.cs:1558): clear the temp channel
|
||||
// after a tell so a one-off /tell doesn't stick to the tab. Tell-only,
|
||||
// so Say/Party/FC stay untouched. A no-op in today's input-bar path
|
||||
// (TempTellTarget is inert), kept for an eventual temp-channel revival.
|
||||
if (wasTell)
|
||||
activeTab?.CurrentChannel?.ResetTempChannel();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -422,6 +449,52 @@ internal sealed class InputBar
|
||||
}
|
||||
}
|
||||
|
||||
// Pure routing: turns the trimmed buffer into the bytes-source string and
|
||||
// reports whether it became a tell. No send, no field mutation — the
|
||||
// ResetTempChannel side-effect lives in TrySend, gated by wasTell, so this
|
||||
// stays exercisable from the SelfTest. Slash input is verbatim (the game
|
||||
// parser owns /tell, /fc, …); everything else gets the channel prefix,
|
||||
// except a tell tab, which needs the full "/tell name@world" because
|
||||
// InputChannel.Tell.Prefix() is only "/t" and would drop the target.
|
||||
private (string toSend, bool wasTell) BuildOutgoing(Tab? activeTab, string text)
|
||||
{
|
||||
if (text.StartsWith('/'))
|
||||
return (text, false);
|
||||
|
||||
var current = activeTab?.CurrentChannel?.Channel ?? InputChannel.Invalid;
|
||||
|
||||
// 1.5.6 tell-target chain (1d3b429:ChatLogWindow.cs:1543-1546).
|
||||
TellTarget? target = null;
|
||||
if (activeTab is not null && activeTab.TellTarget.IsSet())
|
||||
{
|
||||
// leg1 — unconditional: a freshly spawned temp tab carries its target
|
||||
// only here, with CurrentChannel still Invalid until a sidebar/top-bar
|
||||
// click runs EnsureCurrentChannel. A current==Tell gate would miss it.
|
||||
target = activeTab.TellTarget;
|
||||
}
|
||||
else if (current == InputChannel.Tell)
|
||||
{
|
||||
// leg2/leg3 — gated on Tell (CORR-1): CurrentChannel.TellTarget is NOT
|
||||
// channel-bound. After a game-side tell, switching the pill to Say leaves
|
||||
// the tell target standing (SetChannel only sets Channel), so without this
|
||||
// gate a say line would silently go out as /tell — a privacy misfire.
|
||||
target =
|
||||
activeTab?.CurrentChannel?.TempTellTarget ?? activeTab?.CurrentChannel?.TellTarget;
|
||||
}
|
||||
|
||||
// One world lookup, reused by the gate and the string build (ToTargetString
|
||||
// would resolve the sheet twice). The !IsNullOrEmpty(world) check is the
|
||||
// COMP-1 guard: IsSet() only proves World > 0, not that the id resolves in
|
||||
// the Lumina sheet. A miss yields an empty world, and "/tell Name@ text" is
|
||||
// exactly what the game rejects with "you must add the World name". On a miss
|
||||
// we fall through to the channel-prefix path.
|
||||
var world = target?.ToWorldString();
|
||||
if (target != null && target.IsSet() && !string.IsNullOrEmpty(world))
|
||||
return ($"/tell {target.Name}@{world} {text}", true);
|
||||
|
||||
return (current == InputChannel.Invalid ? text : $"{current.Prefix()} {text}", false);
|
||||
}
|
||||
|
||||
private void DrawQuickButtons()
|
||||
{
|
||||
using (_fonts.FontAwesome.Push())
|
||||
@@ -480,6 +553,20 @@ internal sealed class InputBar
|
||||
// so a SelfTest leaves no residual arm state.
|
||||
internal void TestResetDisclosureForSelfTest() => _disclosureArmedBuffer = null;
|
||||
|
||||
// Test-only hook; do not call from production code. Exposes the pure routing
|
||||
// so the tell SelfTest can assert the string + wasTell flag without ever
|
||||
// reaching ChatBox.SendMessageUnsafe (no real chat line).
|
||||
internal (string toSend, bool wasTell) TestBuildOutgoingForSelfTest(
|
||||
Tab? activeTab,
|
||||
string text
|
||||
) => BuildOutgoing(activeTab, text);
|
||||
|
||||
// Test-only hook; do not call from production code. Exposes the pure pill-label
|
||||
// resolution so the tell-transparency SelfTest can assert the partner name is
|
||||
// shown in the stale-/reply-tell state. Static (ResolvePillLabel is static).
|
||||
internal static string TestResolvePillLabelForSelfTest(Tab? tab, bool isTell) =>
|
||||
ResolvePillLabel(tab, isTell);
|
||||
|
||||
private void DrawAutoCompletePopup()
|
||||
{
|
||||
if (_autoCompleteInfo == null)
|
||||
|
||||
Reference in New Issue
Block a user