merge: plugin text and unknown-command refusals go to the chat window (owner-directed, AD-124)
Owner 2026-09-07: plugin output and 'Unknown command' land in the chat scroll, not the SpewBox. Plugin text = Decal/VTank-faithful; the unknown-command re-route is recorded as AD-124 (retail types it 0x1A). Bad-argument refusals of real retail commands stay SpewBox-only (AP-183). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
66160741f0
11 changed files with 311 additions and 65 deletions
|
|
@ -74,30 +74,30 @@ after each deliberate `Top` write for the imported-layout element. Precedent:
|
||||||
`MapPageController.cs:235-249` (the same fix already landed for other
|
`MapPageController.cs:235-249` (the same fix already landed for other
|
||||||
runtime-repositioned imported/programmatic elements).
|
runtime-repositioned imported/programmatic elements).
|
||||||
|
|
||||||
## #488 — MossTank `.utl` expression block: length prefix measured before newline normalization
|
## #488 — MossTank `.utl` expression block: length prefix measured before newline normalization
|
||||||
|
|
||||||
**Status:** OPEN — found 2026-09-07 by the final Opus re-check of Campaign VT
|
**Status:** OPEN — found 2026-09-07 by the final Opus re-check of Campaign VT
|
||||||
slice 1 Part A (`f58e997b1`), not reachable from the UI.
|
slice 1 Part A (`f58e997b1`), not reachable from the UI.
|
||||||
**Severity:** LOW (latent)
|
**Severity:** LOW (latent)
|
||||||
**Component:** `src/AcDream.Plugins.MossTank/MossTankLootProfileStore.cs` (`AttachMossTankExpressions` ~504-521, `ApplyMossTankExpressions` ~539-554) vs `VtankLootProfileSerializer.cs` (`WriteBlock` ~357-366, `NormalizePayload`)
|
**Component:** `src/AcDream.Plugins.MossTank/MossTankLootProfileStore.cs` (`AttachMossTankExpressions` ~504-521, `ApplyMossTankExpressions` ~539-554) vs `VtankLootProfileSerializer.cs` (`WriteBlock` ~357-366, `NormalizePayload`)
|
||||||
|
|
||||||
**Description.** The MossTank-owned unknown block that carries each loot rule's
|
**Description.** The MossTank-owned unknown block that carries each loot rule's
|
||||||
`Expression` text writes `expression.Length` as a length prefix and then the raw
|
`Expression` text writes `expression.Length` as a length prefix and then the raw
|
||||||
text; `WriteBlock` normalizes the whole payload afterwards, rewriting a lone
|
text; `WriteBlock` normalizes the whole payload afterwards, rewriting a lone
|
||||||
`
|
`
|
||||||
`/`
` to `
|
`/`
` to `
|
||||||
`. An expression containing a bare newline therefore grows
|
`. An expression containing a bare newline therefore grows
|
||||||
after its prefix was measured, the reader truncates it, lands mid-text on the
|
after its prefix was measured, the reader truncates it, lands mid-text on the
|
||||||
next length line, fails `int.TryParse` and silently abandons every remaining
|
next length line, fails `int.TryParse` and silently abandons every remaining
|
||||||
rule's expression. The loot expression control is a single-line field so the UI
|
rule's expression. The loot expression control is a single-line field so the UI
|
||||||
cannot author one; the legacy-JSON sweep can (free-form JSON).
|
cannot author one; the legacy-JSON sweep can (free-form JSON).
|
||||||
|
|
||||||
**Fix shape.** Normalize the expression before measuring it (or escape/refuse
|
**Fix shape.** Normalize the expression before measuring it (or escape/refuse
|
||||||
newlines in the block), with a pin that writes a two-line expression and reads
|
newlines in the block), with a pin that writes a two-line expression and reads
|
||||||
it back through `VtankLootProfileSerializer.TryRead`. Companion cosmetics from
|
it back through `VtankLootProfileSerializer.TryRead`. Companion cosmetics from
|
||||||
the same re-check: the unreachable `remaining` roster branch in the route and
|
the same re-check: the unreachable `remaining` roster branch in the route and
|
||||||
loot sweeps, and the meta Delete notice printing the raw file name.
|
loot sweeps, and the meta Delete notice printing the raw file name.
|
||||||
|
|
||||||
## #487 — Radar compass tokens may be pinned by the anchor pass (candidate)
|
## #487 — Radar compass tokens may be pinned by the anchor pass (candidate)
|
||||||
|
|
||||||
**Status:** OPEN — CANDIDATE, found 2026-09-06 by the Opus review of
|
**Status:** OPEN — CANDIDATE, found 2026-09-06 by the Opus review of
|
||||||
|
|
@ -5922,6 +5922,13 @@ slice CH4).
|
||||||
|
|
||||||
## #363 — Chat refusal/usage call sites are typed ClientLocal 0x00 where retail types several 0x1A
|
## #363 — Chat refusal/usage call sites are typed ClientLocal 0x00 where retail types several 0x1A
|
||||||
|
|
||||||
|
**2026-09-07 owner-directed re-route:** the "Unknown command" refusals this
|
||||||
|
issue's closure routed to `ShowInterfaceText`/SpewBox now route to
|
||||||
|
`ShowSystemMessage`/the chat scroll instead, per explicit owner direction
|
||||||
|
that unknown commands must be visible in chat, not the SpewBox overlay.
|
||||||
|
Every OTHER site this issue named (bad-args refusals of real commands,
|
||||||
|
AP-183) is unaffected. See register row AD-124.
|
||||||
|
|
||||||
**Status:** CLOSED 2026-08-10. `ChatVM` gained a typed interface-text seam
|
**Status:** CLOSED 2026-08-10. `ChatVM` gained a typed interface-text seam
|
||||||
(`OnInterfaceText` init hook + `ShowInterfaceText(text)`) that the App-layer
|
(`OnInterfaceText` init hook + `ShowInterfaceText(text)`) that the App-layer
|
||||||
composition (`InteractionRetainedUiComposition.CreateRetainedUi`) wires to
|
composition (`InteractionRetainedUiComposition.CreateRetainedUi`) wires to
|
||||||
|
|
@ -6261,6 +6268,14 @@ still missing); `src/AcDream.App/UI/Layout/LayoutImporter.cs`
|
||||||
|
|
||||||
## #367 — ChatCommandRouter's local-presentation fallbacks type-0x1A text still lands in the chat scroll, never the SpewBox
|
## #367 — ChatCommandRouter's local-presentation fallbacks type-0x1A text still lands in the chat scroll, never the SpewBox
|
||||||
|
|
||||||
|
**2026-09-07 owner-directed re-route:** the two fallbacks this issue named
|
||||||
|
(`RetailCommandHelpTable.UnknownCommand` in `EmitVerbHelp`, and the
|
||||||
|
degenerate-prefix "Unknown command: {verb}." refusal) now call
|
||||||
|
`ShowSystemMessage(...)` again — back to the chat scroll, by explicit owner
|
||||||
|
direction that unknown commands must be visible there rather than in the
|
||||||
|
SpewBox this issue's 2026-08-10 closure moved them to. See register row
|
||||||
|
AD-124; this is a deliberate re-reversal, not a regression of this issue.
|
||||||
|
|
||||||
**Status:** CLOSED 2026-08-10, closed as a side effect of #363's
|
**Status:** CLOSED 2026-08-10, closed as a side effect of #363's
|
||||||
interface-text seam (fix shape (a) from this issue's own filing).
|
interface-text seam (fix shape (a) from this issue's own filing).
|
||||||
`ChatVM.OnInterfaceText` is exactly the hook this issue asked for; both
|
`ChatVM.OnInterfaceText` is exactly the hook this issue asked for; both
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1170,9 +1170,12 @@ internal sealed class AppAutomationSurface
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Routed to retail's ClientLocal log type (0x1A) — the channel the client
|
/// Owner direction 2026-09-07 (register row AD-124): plugin-originated
|
||||||
/// uses for its own notices. Nothing reaches the server, so a plugin cannot
|
/// text now lands in the chat window (retail <c>Default</c>/0x00),
|
||||||
/// accidentally speak in the player's name.
|
/// matching Decal's own <c>AddChatText</c> behavior — not retail's
|
||||||
|
/// ClientLocal (0x1A) SpewBox-only channel this previously used.
|
||||||
|
/// Nothing reaches the server, so a plugin cannot accidentally speak in
|
||||||
|
/// the player's name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void PostSystemMessage(string text)
|
public void PostSystemMessage(string text)
|
||||||
{
|
{
|
||||||
|
|
@ -1181,7 +1184,7 @@ internal sealed class AppAutomationSurface
|
||||||
RuntimeCommunicationState? communication;
|
RuntimeCommunicationState? communication;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
communication = _communication;
|
communication = _communication;
|
||||||
communication?.AddText(text, RetailLogTextType.ClientLocal);
|
communication?.AddText(text, RetailLogTextType.Default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Submit(string text)
|
public bool Submit(string text)
|
||||||
|
|
|
||||||
|
|
@ -302,10 +302,20 @@ public interface IPluginChat
|
||||||
Array.Empty<PluginChatMessage>();
|
Array.Empty<PluginChatMessage>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Post a client-local system line, the channel retail uses for the
|
/// Post a plugin-originated system line into the chat window. It is
|
||||||
/// client's own notices. It is local to this client: nothing is sent to the
|
/// local to this client: nothing is sent to the server and no other
|
||||||
/// server and no other player sees it.
|
/// player sees it.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Owner direction 2026-09-07 (register row AD-124): this used to route
|
||||||
|
/// through retail's <c>ClientLocal</c> (0x1A) channel — the SpewBox
|
||||||
|
/// overlay every <c>ChatInterface</c> window's default filter excludes.
|
||||||
|
/// The owner explicitly overrode that for plugin text, matching Decal's
|
||||||
|
/// own <c>AddChatText</c> behavior: plugin output now lands in the chat
|
||||||
|
/// transcript (retail <c>Default</c>/0x00) so it is actually visible and
|
||||||
|
/// scrolls back, never the transient overlay. See
|
||||||
|
/// <c>AppAutomationSurface.PostSystemMessage</c> for the implementation.
|
||||||
|
/// </remarks>
|
||||||
void PostSystemMessage(string text);
|
void PostSystemMessage(string text);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -120,13 +120,19 @@ public static class ChatCommandRouter
|
||||||
// Command-shaped but no letter verb ("/", "//shrug", "@ x"):
|
// Command-shaped but no letter verb ("/", "//shrug", "@ x"):
|
||||||
// refuse locally rather than putting junk on the wire or in speech.
|
// refuse locally rather than putting junk on the wire or in speech.
|
||||||
// #363/#367: this is one of retail's DoHelp-family "Unknown
|
// #363/#367: this is one of retail's DoHelp-family "Unknown
|
||||||
// command" fallbacks (0x1A ClientLocal, SpewBox-only) — routed
|
// command" fallbacks — retail itself types it 0x1A ClientLocal
|
||||||
// through the interface-text seam now that one exists, instead of
|
// (SpewBox-only). Owner-directed override 2026-09-07 (register row
|
||||||
// the chat scroll.
|
// AD-124): unknown-command refusals specifically must reach the
|
||||||
|
// chat window instead, so ShowSystemMessage (chat scroll, retail
|
||||||
|
// Default/0x00) replaces ShowInterfaceText (SpewBox) HERE ONLY —
|
||||||
|
// do not "fix" this back to ShowInterfaceText; that would silently
|
||||||
|
// re-hide the refusal the owner asked to keep visible. Real
|
||||||
|
// retail-command bad-argument refusals (AP-183) are UNCHANGED and
|
||||||
|
// still use ShowInterfaceText/SpewBox elsewhere in this file.
|
||||||
if (trimmed[0] is '/' or '@'
|
if (trimmed[0] is '/' or '@'
|
||||||
&& (trimmed.Length == 1 || !char.IsLetter(trimmed[1])))
|
&& (trimmed.Length == 1 || !char.IsLetter(trimmed[1])))
|
||||||
{
|
{
|
||||||
feedback.ShowInterfaceText(
|
feedback.ShowSystemMessage(
|
||||||
$"Unknown command: {ChatInputParser.GetVerbToken(trimmed)}. Type /help for the list of supported commands.");
|
$"Unknown command: {ChatInputParser.GetVerbToken(trimmed)}. Type /help for the list of supported commands.");
|
||||||
return SubmitOutcome.UnknownCommand;
|
return SubmitOutcome.UnknownCommand;
|
||||||
}
|
}
|
||||||
|
|
@ -345,7 +351,11 @@ public static class ChatCommandRouter
|
||||||
// SAME fallback an unregistered verb gets — DoHelp's help-
|
// SAME fallback an unregistered verb gets — DoHelp's help-
|
||||||
// pointer-null guard skips its callback branch entirely. See
|
// pointer-null guard skips its callback branch entirely. See
|
||||||
// RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp's remarks.
|
// RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp's remarks.
|
||||||
feedback.ShowInterfaceText(RetailCommandHelpTable.UnknownCommand);
|
// Owner-directed override 2026-09-07 (register row AD-124):
|
||||||
|
// this is an "Unknown command" refusal, so ShowSystemMessage
|
||||||
|
// (chat scroll) replaces ShowInterfaceText (SpewBox) here —
|
||||||
|
// do not revert.
|
||||||
|
feedback.ShowSystemMessage(RetailCommandHelpTable.UnknownCommand);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -370,10 +380,14 @@ public static class ChatCommandRouter
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retail types this 0x1A (ClientLocal) -> SpewBox-only. #363/#367:
|
// Retail types this 0x1A (ClientLocal) -> SpewBox-only. #363/#367
|
||||||
// now routed through IChatCommandFeedback.ShowInterfaceText instead
|
// originally routed it through IChatCommandFeedback.ShowInterfaceText
|
||||||
// of the chat scroll — see RetailCommandHelpTable.UnknownCommand.
|
// for exactly that reason. Owner-directed override 2026-09-07
|
||||||
feedback.ShowInterfaceText(RetailCommandHelpTable.UnknownCommand);
|
// (register row AD-124): "Unknown command" refusals must reach the
|
||||||
|
// chat window instead, so ShowSystemMessage replaces
|
||||||
|
// ShowInterfaceText here — see RetailCommandHelpTable.UnknownCommand
|
||||||
|
// and do not revert this to ShowInterfaceText.
|
||||||
|
feedback.ShowSystemMessage(RetailCommandHelpTable.UnknownCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool EqAny(string value, params string[] options)
|
private static bool EqAny(string value, params string[] options)
|
||||||
|
|
|
||||||
|
|
@ -210,15 +210,29 @@ namespace AcDream.Runtime.Chat;
|
||||||
/// </para>
|
/// </para>
|
||||||
///
|
///
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>Issue #363 (2026-08-10):</b> <c>ChatCommandRouter</c> now routes this
|
/// <b>Issue #363 (2026-08-10):</b> <c>ChatCommandRouter</c> routed this
|
||||||
/// fallback (and every other <c>0x1A</c> command-refusal call site) through
|
/// fallback (and every other <c>0x1A</c> command-refusal call site) through
|
||||||
/// <c>IChatCommandFeedback.ShowInterfaceText</c> — an optional hook the host
|
/// <c>IChatCommandFeedback.ShowInterfaceText</c> — an optional hook the host
|
||||||
/// wires to <c>RuntimeCommunicationState.AddText</c>, the same SpewBox
|
/// wires to <c>RuntimeCommunicationState.AddText</c>, the same SpewBox
|
||||||
/// chokepoint every other producer of interface text uses. The retained
|
/// chokepoint every other producer of interface text uses. The retained
|
||||||
/// <c>ChatVM</c> implements this four-member feedback seam without entering
|
/// <c>ChatVM</c> implements this four-member feedback seam without entering
|
||||||
/// command-routing code. Closes ISSUES.md #367 and retires register row
|
/// command-routing code. Closed ISSUES.md #367 and retired register row
|
||||||
/// AP-186.
|
/// AP-186.
|
||||||
/// </para>
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <b>Owner-directed override 2026-09-07 (register row AD-124):</b> the
|
||||||
|
/// paragraph above still describes retail's own behavior faithfully, but
|
||||||
|
/// acdream no longer matches it for exactly this <see cref="UnknownCommand"/>
|
||||||
|
/// text (both its call sites in <c>ChatCommandRouter.EmitVerbHelp</c>) and
|
||||||
|
/// the sibling "Unknown command: {verb}." refusal in
|
||||||
|
/// <c>ChatCommandRouter.Submit</c>'s own body: those three sites now call
|
||||||
|
/// <c>IChatCommandFeedback.ShowSystemMessage</c> (the chat scroll, retail
|
||||||
|
/// <c>Default</c>/0x00) instead of <c>ShowInterfaceText</c> (SpewBox), so an
|
||||||
|
/// unknown command is actually visible and stays in the transcript. Every
|
||||||
|
/// OTHER <c>0x1A</c> refusal this class documents (bad-args, AP-183) is
|
||||||
|
/// unchanged and still SpewBox-only.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RetailCommandHelpTable
|
public static class RetailCommandHelpTable
|
||||||
{
|
{
|
||||||
|
|
@ -266,9 +280,16 @@ public static class RetailCommandHelpTable
|
||||||
// acclient_2013_pseudo_c.txt:395052 (u"Unknown command", UTF-16LE) --
|
// acclient_2013_pseudo_c.txt:395052 (u"Unknown command", UTF-16LE) --
|
||||||
// DoHelp's fallback when the verb hash lookup fails, or resolves to an
|
// DoHelp's fallback when the verb hash lookup fails, or resolves to an
|
||||||
// entry with no registered help callback. Retail types this 0x1A
|
// entry with no registered help callback. Retail types this 0x1A
|
||||||
// (ClientLocal) -- SpewBox-only; see the class remarks' routing note --
|
// (ClientLocal) -- SpewBox-only; see the class remarks' routing note.
|
||||||
// ChatCommandRouter routes it through IChatCommandFeedback.ShowInterfaceText
|
// Owner-directed override 2026-09-07 (register row AD-124): acdream
|
||||||
// (issue #363), closing #367.
|
// now routes THIS text (and the sibling "Unknown command: {verb}."
|
||||||
|
// refusal in ChatCommandRouter.Submit's own body) through
|
||||||
|
// IChatCommandFeedback.ShowSystemMessage (chat scroll) instead of
|
||||||
|
// ShowInterfaceText (SpewBox) — a deliberate deviation from retail's
|
||||||
|
// own 0x1A typing, scoped to unknown-command text only. Do not revert
|
||||||
|
// this to ShowInterfaceText without a fresh owner direction; every
|
||||||
|
// other 0x1A refusal in ChatCommandRouter (bad-args, AP-183) is
|
||||||
|
// unaffected and still uses ShowInterfaceText/SpewBox.
|
||||||
public const string UnknownCommand = "Unknown command";
|
public const string UnknownCommand = "Unknown command";
|
||||||
|
|
||||||
// @mr/@pr are registered with a NULL function pointer in the 2013
|
// @mr/@pr are registered with a NULL function pointer in the 2013
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,31 @@ public sealed class AppAutomationSurfaceTests
|
||||||
Assert.Equal(0, second.CommunicationOwner.SubscriberCount);
|
Assert.Equal(0, second.CommunicationOwner.SubscriberCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owner-directed override 2026-09-07 (register row AD-124): plugin
|
||||||
|
/// output ("Unknown commands like /vt or stuff from plugins ... should
|
||||||
|
/// go to the chatbox") must land in the chat log, never the transient
|
||||||
|
/// SpewBox overlay retail's own ClientLocal (0x1A) typing used to send
|
||||||
|
/// it to — the same VTank-faithful destination Decal's own
|
||||||
|
/// <c>AddChatText</c> uses.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void PostSystemMessage_RoutesToChatLog_NeverSpewBox()
|
||||||
|
{
|
||||||
|
using var runtime = GameRuntimeTestFactory.Create();
|
||||||
|
using var surface = new AppAutomationSurface();
|
||||||
|
surface.Bind(runtime, runtime.CharacterOwner, runtime.ActionOwner.SpellCast);
|
||||||
|
|
||||||
|
surface.PostSystemMessage("MossTank: buffs applied.");
|
||||||
|
|
||||||
|
var entry = Assert.Single(runtime.CommunicationOwner.Chat.Snapshot());
|
||||||
|
Assert.Equal("MossTank: buffs applied.", entry.Text);
|
||||||
|
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||||
|
|
||||||
|
runtime.CommunicationOwner.SpewBox.Tick(0d);
|
||||||
|
Assert.Equal(0, runtime.CommunicationOwner.SpewBox.Count);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void InventoryCompletionProjectsTheCanonicalRequestReceipt()
|
public void InventoryCompletionProjectsTheCanonicalRequestReceipt()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
using AcDream.Core.Chat;
|
||||||
|
using AcDream.Runtime.Chat;
|
||||||
|
using AcDream.Runtime.Gameplay;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Tests.Chat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owner direction 2026-09-07 (verbatim): "Unknown commands like /vt or
|
||||||
|
/// stuff from plugins shall now go to the SpewBox. They should go to the
|
||||||
|
/// chatbox." Register row AD-124 records the deviation from retail's own
|
||||||
|
/// ClientLocal (0x1A) typing for exactly these two families. This file pins
|
||||||
|
/// the <see cref="ChatCommandRouter"/> half of that change at the Runtime
|
||||||
|
/// layer — <see cref="RuntimeChatCommandFeedback"/> bound to a real
|
||||||
|
/// <see cref="RuntimeCommunicationState"/> — since the existing router
|
||||||
|
/// coverage in <c>AcDream.UI.Abstractions.Tests</c> only exercises the
|
||||||
|
/// <c>ChatVM</c> feedback implementation. The plugin-text half is pinned at
|
||||||
|
/// the App layer (<c>AppAutomationSurfaceTests.PostSystemMessage_RoutesToChatLog_NeverSpewBox</c>),
|
||||||
|
/// since <c>AppAutomationSurface</c> is the App-layer production
|
||||||
|
/// implementation of <c>IPluginChat</c>.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ChatCommandRouterFeedbackRoutingTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DegeneratePrefix_UnknownCommandRefusal_RoutesToChatLog_NeverSpewBox()
|
||||||
|
{
|
||||||
|
// "/" alone (no letter verb) is the degenerate-prefix guard's
|
||||||
|
// "Unknown command: {verb}." refusal — retail itself types this
|
||||||
|
// 0x1A (ClientLocal / SpewBox-only); the owner override moves it to
|
||||||
|
// the chat scroll (Default/0x00) instead.
|
||||||
|
using var communication = new RuntimeCommunicationState();
|
||||||
|
var feedback = new RuntimeChatCommandFeedback(communication);
|
||||||
|
|
||||||
|
SubmitOutcome outcome = ChatCommandRouter.Submit(
|
||||||
|
"/", feedback, NullCommandBus.Instance, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.UnknownCommand, outcome);
|
||||||
|
ChatEntry entry = Assert.Single(communication.Chat.Snapshot());
|
||||||
|
Assert.Contains("Unknown command:", entry.Text);
|
||||||
|
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||||
|
|
||||||
|
communication.SpewBox.Tick(0d);
|
||||||
|
Assert.Equal(0, communication.SpewBox.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpUnresolvedVerb_UnknownCommandText_RoutesToChatLog_NeverSpewBox()
|
||||||
|
{
|
||||||
|
// "/help nonsenseverb" hits EmitVerbHelp's final unresolved-verb
|
||||||
|
// fallback (RetailCommandHelpTable.UnknownCommand), the exact
|
||||||
|
// existing retail-swept text — only the destination changes.
|
||||||
|
using var communication = new RuntimeCommunicationState();
|
||||||
|
var feedback = new RuntimeChatCommandFeedback(communication);
|
||||||
|
|
||||||
|
SubmitOutcome outcome = ChatCommandRouter.Submit(
|
||||||
|
"/help nonsenseverb", feedback, NullCommandBus.Instance, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
|
ChatEntry entry = Assert.Single(communication.Chat.Snapshot());
|
||||||
|
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
|
||||||
|
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||||
|
|
||||||
|
communication.SpewBox.Tick(0d);
|
||||||
|
Assert.Equal(0, communication.SpewBox.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HelpConfirmedNullVerb_UnknownCommandText_RoutesToChatLog_NeverSpewBox()
|
||||||
|
{
|
||||||
|
// "index" is one of the four catalog verbs retail registers with a
|
||||||
|
// genuinely NULL help pointer (RetailCommandHelpTable.
|
||||||
|
// CatalogVerbsWithNoRetailHelp) — EmitVerbHelp's OTHER "Unknown
|
||||||
|
// command" call site, distinct from the unresolved-verb fallback
|
||||||
|
// above.
|
||||||
|
using var communication = new RuntimeCommunicationState();
|
||||||
|
var feedback = new RuntimeChatCommandFeedback(communication);
|
||||||
|
|
||||||
|
SubmitOutcome outcome = ChatCommandRouter.Submit(
|
||||||
|
"/help index", feedback, NullCommandBus.Instance, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
|
ChatEntry entry = Assert.Single(communication.Chat.Snapshot());
|
||||||
|
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
|
||||||
|
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||||
|
|
||||||
|
communication.SpewBox.Tick(0d);
|
||||||
|
Assert.Equal(0, communication.SpewBox.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RealCommandBadArguments_StillRoutesToSpewBox_NeverChatLog()
|
||||||
|
{
|
||||||
|
// Boundary pin: AP-183's bad-argument refusals of REAL retail
|
||||||
|
// commands are UNCHANGED by the owner's 2026-09-07 direction, which
|
||||||
|
// named only unknown commands and plugin text. "/ls now" (Lifestone
|
||||||
|
// with bad args) must still land in the SpewBox exclusively.
|
||||||
|
using var communication = new RuntimeCommunicationState();
|
||||||
|
var feedback = new RuntimeChatCommandFeedback(communication);
|
||||||
|
|
||||||
|
SubmitOutcome outcome = ChatCommandRouter.Submit(
|
||||||
|
"/ls now", feedback, NullCommandBus.Instance, ChatChannelKind.Say);
|
||||||
|
|
||||||
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
|
Assert.Empty(communication.Chat.Snapshot());
|
||||||
|
|
||||||
|
communication.SpewBox.Tick(0d);
|
||||||
|
Assert.Equal(1, communication.SpewBox.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
"Please see @help lifestone for more information on how to use this command.",
|
||||||
|
communication.SpewBox.Snapshot()[0].Text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -194,4 +194,36 @@ public sealed class ChatVMTests
|
||||||
// The stored body never carries the stamp in either state.
|
// The stored body never carries the stamp in either state.
|
||||||
Assert.Equal("hi", log.Snapshot()[0].Text);
|
Assert.Equal("hi", log.Snapshot()[0].Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owner-directed override 2026-09-07 (register row AD-124): plugin
|
||||||
|
/// output (<c>AppAutomationSurface.PostSystemMessage</c>, the
|
||||||
|
/// production implementation of <c>IPluginChat.PostSystemMessage</c>)
|
||||||
|
/// now funnels into <c>RuntimeCommunicationState.AddText(text,
|
||||||
|
/// RetailLogTextType.Default)</c>, which calls
|
||||||
|
/// <c>Chat.OnSystemMessage(text, (uint)Default)</c> — the exact call
|
||||||
|
/// this test performs directly on the shared <see cref="ChatLog"/>,
|
||||||
|
/// matching Decal's own <c>AddChatText</c> behavior for plugin text.
|
||||||
|
/// Any <see cref="ChatVM"/> bound to that log (the production chat
|
||||||
|
/// window) must show the line; it must never depend on the
|
||||||
|
/// <see cref="ChatVM.OnInterfaceText"/> SpewBox seam, which this call
|
||||||
|
/// never touches.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void RecentLines_ShowsPluginSystemMessage_TaggedDefault()
|
||||||
|
{
|
||||||
|
var log = new ChatLog();
|
||||||
|
var vm = new ChatVM(log, displayLimit: 50);
|
||||||
|
|
||||||
|
log.OnSystemMessage(
|
||||||
|
"MossTank: buffs applied.",
|
||||||
|
chatType: (uint)RetailLogTextType.Default);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"MossTank: buffs applied.",
|
||||||
|
Assert.Single(vm.RecentLines()));
|
||||||
|
Assert.Equal(
|
||||||
|
(uint)RetailLogTextType.Default,
|
||||||
|
Assert.Single(log.Snapshot()).LogTextType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -406,28 +406,35 @@ public class ChatCommandRouterTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HelpVerb_UnknownVerb_ShowsRetailUnknownCommandText_ViaInterfaceTextSeam()
|
public void HelpVerb_UnknownVerb_ShowsRetailUnknownCommandText_InChatLog_TaggedDefault()
|
||||||
{
|
{
|
||||||
// Campaign CH user-gate round 3 (2026-08-10): retail's own DoHelp
|
// Campaign CH user-gate round 3 (2026-08-10): retail's own DoHelp
|
||||||
// fallback text is "Unknown command" (swept verbatim), not an
|
// fallback text is "Unknown command" (swept verbatim), not an
|
||||||
// acdream-invented "No help available" message. Retail types this
|
// acdream-invented "No help available" message. Retail itself types
|
||||||
// 0x1A (ClientLocal / SpewBox-only). Issue #363/#367: now routed
|
// this 0x1A (ClientLocal / SpewBox-only). Owner-directed override
|
||||||
// through the interface-text seam as ONE entry (no HelpPrefixNote
|
// 2026-09-07 (register row AD-124): "Unknown command" refusals now
|
||||||
// wrapper — DoHelp's fallback bypasses the two-entry shape
|
// route to the CHAT SCROLL (ShowSystemMessage, Default/0x00) instead
|
||||||
// entirely), not the chat scroll.
|
// of the interface-text/SpewBox seam — the seam stays empty.
|
||||||
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
|
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
|
||||||
|
|
||||||
var outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say);
|
var outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
Assert.Empty(bus.Published);
|
Assert.Empty(bus.Published);
|
||||||
Assert.Equal(RetailCommandHelpTable.UnknownCommand, Assert.Single(interfaceTexts));
|
Assert.Empty(interfaceTexts);
|
||||||
Assert.Empty(log.Snapshot());
|
var entry = Assert.Single(log.Snapshot());
|
||||||
|
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
|
||||||
|
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void HelpVerb_UnknownVerb_NoInterfaceSinkWired_FallsBackToChatLog_TaggedClientLocal()
|
public void HelpVerb_UnknownVerb_NoInterfaceSinkWired_StillRoutesToChatLog_TaggedDefault()
|
||||||
{
|
{
|
||||||
|
// Owner-directed override 2026-09-07 (register row AD-124):
|
||||||
|
// ShowSystemMessage never depended on OnInterfaceText wiring in the
|
||||||
|
// first place, so headless / no-window hosts see the identical
|
||||||
|
// chat-log entry whether or not a sink is wired — unlike the old
|
||||||
|
// ShowInterfaceText null-fallback this test used to pin.
|
||||||
var (vm, log, bus) = Fixture();
|
var (vm, log, bus) = Fixture();
|
||||||
|
|
||||||
var outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say);
|
var outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
@ -435,7 +442,7 @@ public class ChatCommandRouterTests
|
||||||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||||
var entry = Assert.Single(log.Snapshot());
|
var entry = Assert.Single(log.Snapshot());
|
||||||
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
|
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
|
||||||
Assert.Equal((uint)RetailLogTextType.ClientLocal, entry.LogTextType);
|
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -588,19 +595,23 @@ public class ChatCommandRouterTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DegeneratePrefix_UnknownCommand_ShowsRefusal_ViaInterfaceTextSeam()
|
public void DegeneratePrefix_UnknownCommand_ShowsRefusal_InChatLog_TaggedDefault()
|
||||||
{
|
{
|
||||||
// "/" alone (no letter verb) — the pre-existing "Unknown command:
|
// "/" alone (no letter verb) — the pre-existing "Unknown command:
|
||||||
// {verb}." refusal, now also routed through the interface-text
|
// {verb}." refusal. Owner-directed override 2026-09-07 (register
|
||||||
// seam (issue #367).
|
// row AD-124): routed to the chat scroll (ShowSystemMessage,
|
||||||
|
// Default/0x00), NOT the interface-text/SpewBox seam issue #367
|
||||||
|
// originally moved it to.
|
||||||
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
|
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
|
||||||
|
|
||||||
var outcome = ChatCommandRouter.Submit("/", vm, bus, ChatChannelKind.Say);
|
var outcome = ChatCommandRouter.Submit("/", vm, bus, ChatChannelKind.Say);
|
||||||
|
|
||||||
Assert.Equal(SubmitOutcome.UnknownCommand, outcome);
|
Assert.Equal(SubmitOutcome.UnknownCommand, outcome);
|
||||||
Assert.Empty(bus.Published);
|
Assert.Empty(bus.Published);
|
||||||
Assert.Contains("Unknown command:", Assert.Single(interfaceTexts));
|
Assert.Empty(interfaceTexts);
|
||||||
Assert.Empty(log.Snapshot());
|
var entry = Assert.Single(log.Snapshot());
|
||||||
|
Assert.Contains("Unknown command:", entry.Text);
|
||||||
|
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -395,10 +395,13 @@ public sealed class RetailCommandHelpTableTests
|
||||||
// assignment, unlike every extracted verb above -- confirming a
|
// assignment, unlike every extracted verb above -- confirming a
|
||||||
// genuinely NULL help function pointer. Retail's own DoHelp skips
|
// genuinely NULL help function pointer. Retail's own DoHelp skips
|
||||||
// its help-callback branch entirely for these and falls to the
|
// its help-callback branch entirely for these and falls to the
|
||||||
// SAME "Unknown command" 0x1A text an unregistered verb gets, even
|
// SAME "Unknown command" text an unregistered verb gets (retail
|
||||||
// though the verb dispatches fine for ordinary (non-help) use.
|
// itself types it 0x1A), even though the verb dispatches fine for
|
||||||
// Showing the catalog's own invented summary here would be
|
// ordinary (non-help) use. Showing the catalog's own invented
|
||||||
// retail-inaccurate.
|
// summary here would be retail-inaccurate. Owner-directed override
|
||||||
|
// 2026-09-07 (register row AD-124): acdream routes this "Unknown
|
||||||
|
// command" text to the CHAT SCROLL (Default/0x00) rather than
|
||||||
|
// retail's own SpewBox-only 0x1A typing.
|
||||||
var log = new AcDream.Core.Chat.ChatLog();
|
var log = new AcDream.Core.Chat.ChatLog();
|
||||||
var vm = new ChatVM(log, displayLimit: 50);
|
var vm = new ChatVM(log, displayLimit: 50);
|
||||||
var bus = new RecordingCommandBus();
|
var bus = new RecordingCommandBus();
|
||||||
|
|
@ -411,7 +414,7 @@ public sealed class RetailCommandHelpTableTests
|
||||||
Assert.Single(entries);
|
Assert.Single(entries);
|
||||||
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entries[0].Text);
|
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entries[0].Text);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
(uint)AcDream.Core.Chat.RetailLogTextType.ClientLocal,
|
(uint)AcDream.Core.Chat.RetailLogTextType.Default,
|
||||||
entries[0].LogTextType);
|
entries[0].LogTextType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue