fix(chat): #363 — retail 0x1A typing for command refusals via the interface-text seam

ChatVM gains an OnInterfaceText hook + ShowInterfaceText(text), the
App-layer composition wires it to RuntimeCommunicationState.AddText,
and ChatCommandRouter routes every retail-0x1A command refusal through
it instead of the chat log's 0x00 sink. UI.Abstractions still never
references Runtime directly; unwired hosts (headless, tests) fall back
to the chat log tagged ClientLocal so no text is ever silently lost.

Reclassified per register row AP-183 (DoChannelList/On/Off, DoAllegiance,
DoHouseAvailableList — the last also corrected to retail's own bad-house-
type string instead of a synthesized "Usage:" line) and newly wired two
sites that previously showed nothing at all (DoStupidChannelHack's bare
legacy-channel-verb refusal, DoReply's message-but-no-last-teller
refusal). The generic bad-args fallback now resolves WeenieErrorMessages
0x026 ("That is not a valid command.", retail's HandleFailureEvent(0x26))
instead of synthesizing "Usage: {Usage}". DoSpeaker/DoEndurance/DoTitle
are untouched — already correct at 0x00.

Also closes #367 (DoHelp's "Unknown command" fallback and the degenerate-
prefix refusal now reach the SpewBox too) and retires register row
AP-186, whose own filing proposed exactly this seam shape.

Full Release suite: 12,542 passed / 4 skipped / 0 failed (baseline
12,466/4/0 at ff2784ea).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 15:20:57 +02:00
parent ff2784eaa4
commit 09453ecae8
14 changed files with 702 additions and 106 deletions

View file

@ -73,6 +73,21 @@ public sealed class ChatVM : IDisposable
/// </summary>
public Func<Vector3>? PositionProvider { get; init; }
/// <summary>
/// Optional hook routing retail-<c>0x1A</c> (<see
/// cref="RetailLogTextType.ClientLocal"/>) interface text — command
/// refusals and bad-argument usage lines — to the SpewBox instead of
/// the chat transcript. <c>AcDream.UI.Abstractions</c> must stay
/// Runtime-independent (Code Structure Rules), so it cannot call
/// <c>RuntimeCommunicationState.AddText</c> directly; the App-layer
/// composition host wires this the same way it wires
/// <see cref="FpsProvider"/>/<see cref="PositionProvider"/>. Closes
/// ISSUES.md #367 / register row AP-186 — <see cref="ChatCommandRouter"/>
/// no longer has to render every <c>0x1A</c> refusal through the chat
/// scroll.
/// </summary>
public Action<string>? OnInterfaceText { get; init; }
/// <summary>Monotonic revision of the underlying transcript content.</summary>
public long Revision => _log.Revision;
@ -139,6 +154,33 @@ public sealed class ChatVM : IDisposable
/// </remarks>
public void ShowSystemMessage(string text) => _log.OnSystemMessage(text, chatType: 0x00u);
/// <summary>
/// Route a retail-<c>0x1A</c> (<see cref="RetailLogTextType.ClientLocal"/>)
/// command refusal / usage line to the SpewBox — retail's
/// <c>ClientSystem::AddTextToScroll(text, 0x1A, 1, windowId) @0x00563C50</c>
/// destination for this text type is the SpewBox exclusively, never a
/// chat window (<c>docs/research/2026-08-09-chat-retail-interface-text.md</c>
/// §2.1/§2.2).
/// </summary>
/// <remarks>
/// Prefers <see cref="OnInterfaceText"/> when the App-layer host wired
/// it (the production graphical client). When unwired — headless, the
/// automation probe runner, or a test fixture that only exercises the
/// pure UI.Abstractions layer — the text still needs to reach the
/// player somewhere, so it falls back to the ordinary chat transcript
/// tagged with the real <see cref="RetailLogTextType.ClientLocal"/>
/// color rather than being silently dropped. That fallback lands in
/// the wrong PANEL (chat instead of SpewBox) but keeps the right TYPE
/// and never loses the line — the safe default issue #363 requires.
/// </remarks>
public void ShowInterfaceText(string text)
{
if (OnInterfaceText is { } hook)
hook(text);
else
_log.OnSystemMessage(text, chatType: (uint)RetailLogTextType.ClientLocal);
}
/// <summary>
/// Drain the chat log. Used by the /clear client-side command.
/// </summary>