diff --git a/docs/ISSUES.md b/docs/ISSUES.md
index 32c4eeb7..263939d7 100644
--- a/docs/ISSUES.md
+++ b/docs/ISSUES.md
@@ -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
runtime-repositioned imported/programmatic elements).
-## #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
-slice 1 Part A (`f58e997b1`), not reachable from the UI.
-**Severity:** LOW (latent)
-**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
-`Expression` text writes `expression.Length` as a length prefix and then the raw
-text; `WriteBlock` normalizes the whole payload afterwards, rewriting a lone
-`
+## #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
+slice 1 Part A (`f58e997b1`), not reachable from the UI.
+**Severity:** LOW (latent)
+**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
+`Expression` text writes `expression.Length` as a length prefix and then the raw
+text; `WriteBlock` normalizes the whole payload afterwards, rewriting a lone
+`
`/`
` to `
-`. An expression containing a bare newline therefore grows
-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
-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).
-
-**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
-it back through `VtankLootProfileSerializer.TryRead`. Companion cosmetics from
-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.
-
+`. An expression containing a bare newline therefore grows
+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
+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).
+
+**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
+it back through `VtankLootProfileSerializer.TryRead`. Companion cosmetics from
+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.
+
## #487 — Radar compass tokens may be pinned by the anchor pass (candidate)
**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
+**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
(`OnInterfaceText` init hook + `ShowInterfaceText(text)`) that the App-layer
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
+**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
interface-text seam (fix shape (a) from this issue's own filing).
`ChatVM.OnInterfaceText` is exactly the hook this issue asked for; both
diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md
index 509afc36..655bfb6c 100644
--- a/docs/architecture/retail-divergence-register.md
+++ b/docs/architecture/retail-divergence-register.md
@@ -76,7 +76,7 @@ accepted-divergence entries (#96, #49, #50).
---
-## 2. Adaptation (AD) — 94 active rows (AD-123 filed 2026-09-07 at Campaign VT slice 1 Part A round 3 item 9 — MossTank keeps a ByCharacter auto loot .utl file for internal consistency across all four stores, where retail's own loot picker seeds only [None] and has no per-character auto file at all; AD-122 filed 2026-09-07 at Campaign VT slice 1 Part A round 3 item 7 — MossTank's .cdf writes real VTank-native Nav/Meta filenames as its own .af format instead of .nav/.met, unreadable by a real VTank instance sharing the same profile directory; AD-120 filed 2026-09-04 at the S4-c2 fix round 1 (M3) — a translucent building-shell instance under building detail draws immediately at its own walk-stream alpha-submission mark rather than "in place" mid-mesh-call, since acdream's opaque instances are stream-batched and retail's mesh call has no equivalent; AD-119 filed 2026-09-03 at Campaign OVERHAUL v2 S4 chunk 1 (S4-c1 C2) — the portal-depth color path substitutes a `ColorWrite=false` write mask for retail's zero-source-alpha `SRCALPHA`/`INVSRCALPHA` blend, a provably pixel-identical no-op either way; AD-117 filed 2026-09-03 at the Campaign OVERHAUL S2 review fix round — three residual Contract A/B approximations the S2 retail-lens review named (visual-AABB circumsphere cheap reject, part rows published into unloaded neighbour cells, the unported `state & 0x1000` particle branch) — its original item 1, the render-only destination-cell move rule, was VERIFIED the same night as retail's own zero-sphere `CObjCell::find_cell_list` 0x0052b4e0 mechanism and is not a deviation; AD-116 filed 2026-09-03 at Campaign OVERHAUL S2 chunk 5 — `WalkProductionWorldData`'s borrowed per-cell view contributes NO cell for an entity the registry has flooded but the presentation scene cannot resolve yet (the deleted parent-cell/root-position fallbacks are gone), counted once per distinct entity per frame in `UnregisteredRenderMembershipCount`; AD-115 filed 2026-08-25 at Campaign AS slice AS2 review fix round (F16) — `BuildCharacterTitleDisplay` clears the Profession element (`0x10000151`) when neither Int 261 CharacterTitleId nor String 5 Template resolves, where retail never clears `0x10000150`/`51`/`52` anywhere and would instead show the PREVIOUS target's stale title; AD-114 filed 2026-08-25 at Campaign AS slice AS2, owner-ruled ("we animate it, and I like it") — the examination window's preview clone tracks the assessed creature's live current animated pose every frame, where retail's clone plays its own private `CreatureMode` cycle decoupled from the live target's actual motion; AD-113 filed 2026-08-25 at Campaign CT slice CT-GF1 — `UiMenu`'s inline-drawn popup opts out of the new client-wide ancestor-clip default (`ExpandsClipForPopup`), standing in for retail's separate top-level popup region; AD-112 filed 2026-08-23 with the sky default-script port — camera-anchored synthetic script owners instead of retail's sky-cell physics objects; AD-110 filed 2026-08-17 at the entry/exit presentation round — the in-world logoff's single confirmed-echo handoff edge versus retail's two independent ExecuteLogOff/CharacterList edges, and the Tunnel-hold tail; AD-74 RETIRED 2026-08-17 at the same round — the Exit to Character Selection "behaves as Exit Game" adaptation is deleted: the confirmed grounded exit now runs the REAL retail flow (0xF653 request, server LogOut motion, 3 s hold, reverse wormhole, return to the live-connection character-select screen via LiveSessionController.CompleteCharacterLogOff), and the previously-missing indicator-bar grounded gate now runs retail's shared three-way branch; AD-109 filed 2026-08-17 at the entry/exit presentation round — the click-armed login tunnel: the wormhole presentation + enter cue now begin at the character-select Enter click instead of retail's black CreatePlayer wait, USER-DIRECTED; AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 RETIRED 2026-08-28 — #386's named-retail message trace confirmed the vendor popup is content-sized and installed-DAT property 0x79 hides its disabled scrollbar; both behaviors are now ported; AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice; AD-111 (renumbered from a parallel-round AD-109 collision) filed 2026-08-17 at the systemic escape-normalization round — the appraisal report's wire-domain literal-
+## 2. Adaptation (AD) — 95 active rows (AD-124 filed 2026-09-07, owner-directed — ChatCommandRouter's "Unknown command" refusals and plugin-originated system text (IPluginChat.PostSystemMessage) now route to the chat scroll instead of retail's own ClientLocal/0x1A SpewBox-only typing; AD-123 filed 2026-09-07 at Campaign VT slice 1 Part A round 3 item 9 — MossTank keeps a ByCharacter auto loot .utl file for internal consistency across all four stores, where retail's own loot picker seeds only [None] and has no per-character auto file at all; AD-122 filed 2026-09-07 at Campaign VT slice 1 Part A round 3 item 7 — MossTank's .cdf writes real VTank-native Nav/Meta filenames as its own .af format instead of .nav/.met, unreadable by a real VTank instance sharing the same profile directory; AD-120 filed 2026-09-04 at the S4-c2 fix round 1 (M3) — a translucent building-shell instance under building detail draws immediately at its own walk-stream alpha-submission mark rather than "in place" mid-mesh-call, since acdream's opaque instances are stream-batched and retail's mesh call has no equivalent; AD-119 filed 2026-09-03 at Campaign OVERHAUL v2 S4 chunk 1 (S4-c1 C2) — the portal-depth color path substitutes a `ColorWrite=false` write mask for retail's zero-source-alpha `SRCALPHA`/`INVSRCALPHA` blend, a provably pixel-identical no-op either way; AD-117 filed 2026-09-03 at the Campaign OVERHAUL S2 review fix round — three residual Contract A/B approximations the S2 retail-lens review named (visual-AABB circumsphere cheap reject, part rows published into unloaded neighbour cells, the unported `state & 0x1000` particle branch) — its original item 1, the render-only destination-cell move rule, was VERIFIED the same night as retail's own zero-sphere `CObjCell::find_cell_list` 0x0052b4e0 mechanism and is not a deviation; AD-116 filed 2026-09-03 at Campaign OVERHAUL S2 chunk 5 — `WalkProductionWorldData`'s borrowed per-cell view contributes NO cell for an entity the registry has flooded but the presentation scene cannot resolve yet (the deleted parent-cell/root-position fallbacks are gone), counted once per distinct entity per frame in `UnregisteredRenderMembershipCount`; AD-115 filed 2026-08-25 at Campaign AS slice AS2 review fix round (F16) — `BuildCharacterTitleDisplay` clears the Profession element (`0x10000151`) when neither Int 261 CharacterTitleId nor String 5 Template resolves, where retail never clears `0x10000150`/`51`/`52` anywhere and would instead show the PREVIOUS target's stale title; AD-114 filed 2026-08-25 at Campaign AS slice AS2, owner-ruled ("we animate it, and I like it") — the examination window's preview clone tracks the assessed creature's live current animated pose every frame, where retail's clone plays its own private `CreatureMode` cycle decoupled from the live target's actual motion; AD-113 filed 2026-08-25 at Campaign CT slice CT-GF1 — `UiMenu`'s inline-drawn popup opts out of the new client-wide ancestor-clip default (`ExpandsClipForPopup`), standing in for retail's separate top-level popup region; AD-112 filed 2026-08-23 with the sky default-script port — camera-anchored synthetic script owners instead of retail's sky-cell physics objects; AD-110 filed 2026-08-17 at the entry/exit presentation round — the in-world logoff's single confirmed-echo handoff edge versus retail's two independent ExecuteLogOff/CharacterList edges, and the Tunnel-hold tail; AD-74 RETIRED 2026-08-17 at the same round — the Exit to Character Selection "behaves as Exit Game" adaptation is deleted: the confirmed grounded exit now runs the REAL retail flow (0xF653 request, server LogOut motion, 3 s hold, reverse wormhole, return to the live-connection character-select screen via LiveSessionController.CompleteCharacterLogOff), and the previously-missing indicator-bar grounded gate now runs retail's shared three-way branch; AD-109 filed 2026-08-17 at the entry/exit presentation round — the click-armed login tunnel: the wormhole presentation + enter cue now begin at the character-select Enter click instead of retail's black CreatePlayer wait, USER-DIRECTED; AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 RETIRED 2026-08-28 — #386's named-retail message trace confirmed the vendor popup is content-sized and installed-DAT property 0x79 hides its disabled scrollbar; both behaviors are now ported; AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice; AD-111 (renumbered from a parallel-round AD-109 collision) filed 2026-08-17 at the systemic escape-normalization round — the appraisal report's wire-domain literal-
-to-line-break shaping, which retail's `ItemExamineUI::AddItemInfo @0x004AC050` does not do (wire text appends verbatim; the escape decode retail runs at `StringInfo` resolution now lives at our string source, `DatStringResolver` → `RetailStringEscapes`); AD-108 filed 2026-08-17 at the night-round review fix round (F9), mechanism REPLACED same day at the overnight round's final fix — the Map tab's player/house icons, swallowed as `UiButton` dat children by `m_pMap`'s own Type-1 authoring, are now found in the panel-slot resolve's own info tree and rebuilt via `MapPageController.Bindings.IconBuilder` (the original standalone re-import resolved nothing on the live DAT); AD-107 RETIRED 2026-08-17 at the night-round review fix round (F2) — HouseQuery now fires once at the canonical local-player first-placement-completion edge (the same "initial session bootstrap" moment `GameActionLoginComplete`'s non-portal send sites already use), matching the byte-decoded retail truth that `CM_House::Event_QueryHouse @0x006aaa00` is tail-called, unconditionally, from the END of `CPlayerSystem::InitializePlayer @0x00563570` — the ONE-TIME-per-session function `AttemptSendLoginCompleteNotification` also lives in, guarded by the same `player_initialized` flag — right after that notification, not from any tab-open UI event; the invented tab-open trigger this row described is deleted outright, not merely narrowed; AD-106 filed 2026-08-16 at #409 (client-wide retail tooltip system) — RetailTooltipPresenter mounts the popup as an ordinary UiRoot sibling and keeps it topmost via its own per-tick BringToFront, scheduled after both RetailDialogFactory.Tick and Host.Tick, rather than porting retail's separate always-on-top presentation layer (m_pTooltipElement) — same adaptation shape AP-229 already accepted for dialogs-vs-screens, extended one layer further; AD-105 filed 2026-08-16 at Campaign CC gate round 1 re-test 3, finding R4-3 — the Skills info-box description-pane Height clamp to the SIBLING gold frame's own authored bottom edge, since retail's `ShowSkillsText` has no code relationship between the pane and the frame to cite directly. AD-104 filed 2026-08-16 at Campaign CC gate round 1 re-test 2, finding R3-3 — the Skills info-box title/description VerticalJustify page-scoped override, ISSUES.md #410 tracks the shared client-wide VJustify-default fix this compensates for. F12 correction, Campaign CC gate round 1 closeout, 2026-08-16: this header undercounted by 2 — a direct count of the physical `| AD-` rows below found 79, not the 77 this header carried; corrected to the counted total, matching AP-213's own row-count reconciliation the same closeout. AD-103 RETIRED 2026-08-16 at the Campaign CC gate round 1 Batch C fix (GF-4a) — the swallowed Type-12 value child (`0x100002f1`/`0x100002f3` under the avail/health/stamina/mana/credits badge buttons) is now surfaced as its OWN addressable `UiButton.ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor` slot, built from the child's OWN authored rect/font/color (`DatWidgetFactory.BuildButton`) — closing both the container-Label-substitution shape AND F5's unmeasured-pixel-equivalence concern outright, since the value now renders at the child's own dat-local geometry instead of discarding it for the button's own Label font/rect; AD-101 RETIRED 2026-08-15 at Campaign CC slice CC6b-MOUNT — the Heritage-page auto-gender-select interim default is deleted outright now that the Appearance page's real gender buttons (`0x100003a7`/`0x100003a8`) exist; AD-102/AD-103 filed 2026-08-15 at Campaign CC slice CC4 — the Viamontian/Sanamar ToD-account-ownership gate omission, and the avail/health/stamina/mana/credits-meter UiButton-Label substitution for retail's swallowed Text-child overlays; AD-100 filed 2026-08-15 at the Campaign CC CC2 review (F2) — an unrequested `0xF643` CharGenVerificationResponse is DROPPED with a once-per-session log, where retail's handler has no armed-request gate and processes whatever arrives; AD-99 filed 2026-08-15 at Campaign LA gate round 2 finding 1 — the char-select Exit-confirmed close routes through the existing graceful window-close seam instead of retail's post-confirm `gmEpilogueUI` transition; AD-98 filed 2026-08-15 at Campaign LA gate round 2, COMPLETED same day — the char-select screen keeps its authored 800x600 root and the whole tree (widgets, glyphs, art, dialogs) stretches as one canvas via `UiRoot.FixedCanvasSize` scaling every quad at `TextRenderer.AppendQuad` with inverse mouse mapping, substituting one stage earlier for retail's fixed-canvas-stretched-at-presentation mechanism (the first resize-the-root substitution was deleted at 73041d70); AD-95 RETIRED same-day 2026-08-14 at trade gate round 3 — ID_SecureTrade_TotalItemsLabel probe-verified token-free (fragments ["Total Items: ", ""], one ITEMS variable) and now composed via ResolveTemplate; AD-94 filed 2026-08-14 at the secure-trade feature — the ACE-discarded AcceptTrade echo's zero-count item lists; AD-93 filed 2026-08-13 at social gate round 2 item 5 — the refused-drop notice port's two narrow gaps: wire-guid-match instead of retail's latched-guid preference, and no Move/Wield latch kinds; AD-85 NARROWED + AD-81 AMENDED 2026-08-13 at social gate round 2 — the five confirmation-dialog templates now compose exactly via the new `DatStringResolver.ResolveTemplate` port of `StringTable::GetString @0x004300D0`'s token-free fragment/PLAYER interleave; AD-85 keeps only its numeric-field item, AD-81 keeps the meta-token engine + `FormatName`; AD-92 filed 2026-08-13 at the #376/#388 fix round — highest-refresh-for-WxH selection + refuse-and-log invalid fullscreen requests, versus retail's pass-through-and-error `ForceDisplayResolution`; AD-91 filed 2026-08-13 at the #390 port — the display-change clamp covers floating chats too, which retail leaves unclamped/strandable; AD-90 filed 2026-08-13 at the #389 fix round — retail's smartbox aspect runs through the `Render.AspectRatio` preference (`ComputeAspectForViewport @0x0054f150`), exactly raw w/h at its default, which is what acdream assumes; AD-89 RETIRED same-day 2026-08-13 — the SmartboxFOV port landed (#389): `RetailFieldOfView` + `CameraController.SetGameFov` now apply retail's `gameFOV/(aspect−0.1)` law with the 90°-degrees option semantics, and the invented 60° camera constants are deleted; AD-88 filed 2026-08-13 at the #385 dropdown fix — the vendor category dropdown keeps G5's fixed 6-row scrollable window although its authored popup ListBox is edge-docked, the condition that arms retail's `RecalculatePopupSize` size-to-content resize; classification UNCLEAR pending a retail side-by-side (ISSUES #386); AD-87 filed 2026-08-12 at Campaign FA slice FA6 — the allegiance-swear half of the two-bot headless gate is written+wired but `AllegianceGateEnabled=false` (disabled by default), unverified end-to-end over the wire because ACE returns nothing to the `0x001D` swear (ISSUES #384); the FELLOWSHIP two-session gate passed live and ships as FA6's automated proof; AD-86 filed 2026-08-12 at Campaign FA slice FA5, item 4 — ACE's deliberate zeroing of officers/officer titles/MOTD/MOTD-set-by/name-last-set-time/lock/approved-vassal/timeOnline/allegianceAge, dropped past acdream's own parse layer to match retail's own no-widget presentation; AD-85 filed 2026-08-12 at Campaign FA slice FA5 — the Allegiance page's numeric-only fields and its three local confirmation dialogs' unsubstituted-verbatim-or-bare-name text, the same unported `StringInfo` gap AD-81 filed for Fellowship; AD-84 filed 2026-08-12 at Campaign FA slice FA5 — the Swear button's missing "target is a player" gate, the same class as AD-83's Recruit-button gap; AD-83 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 5) — the Recruit button's missing "target is a player" gate, previously an inline comment not a row; AD-82 filed 2026-08-12 at the Campaign FA slice FA4 fix round (mechanism MUST-FIX 4/5) — the invented leader-tint/selection-tint colors, the name-text-only row click target, and the page-local (not generic-`UiTemplateListBox`) world→panel selection sync; AD-81 filed 2026-08-12 at Campaign FA slice FA4 — the fellowship roster/create-flow text-composition gap (unported `StringInfo` variable substitution + `ACCharGenData::FormatName`); AD-80 filed 2026-08-12 at Campaign FA slice FA4, D5 — the panel's retail-exact XP-share percentage display versus the currently-targeted ACE server's slightly different actual grant; AD-79 filed 2026-08-12 at Campaign FA slice FA3, D1 — the social panel's Friends/Squelch page action buttons (add/remove friend, appear offline, squelch add/remove/clear) are honest INERT, no wire implemented this campaign; AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice)
Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate
@@ -120,6 +120,7 @@ readiness/requeue adaptation. See
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
+| AD-124 | **Filed 2026-09-07, owner-directed.** Retail types two families of chat feedback as `ClientLocal` (0x1A) — the bit every `ChatInterface` window's default filter excludes, so they only ever reach the transient SpewBox overlay: (1) `ChatCommandRouter`'s "Unknown command" refusals (the degenerate-prefix guard's "Unknown command: {verb}." text, and both `EmitVerbHelp` fallbacks that print `RetailCommandHelpTable.UnknownCommand`); (2) plugin-originated system text (`IPluginChat.PostSystemMessage`, e.g. MossTank/VTank output). The owner explicitly overrode both for acdream: unknown-command refusals and plugin text now call `IChatCommandFeedback.ShowSystemMessage` / `RuntimeCommunicationState.AddText(text, RetailLogTextType.Default)` respectively, landing in the chat scroll instead — matching how Decal's own `AddChatText` surfaced plugin output. Every OTHER `ClientLocal` refusal (real retail commands with bad arguments, AP-183; movement/interaction refusals) is UNCHANGED and still SpewBox-only. | `src/AcDream.Runtime/Chat/ChatCommandRouter.cs` (three call sites: the no-letter-verb guard, `EmitVerbHelp`'s confirmed-null-help branch, `EmitVerbHelp`'s unresolved-verb fallback); `src/AcDream.Runtime/Chat/RetailCommandHelpTable.cs` (`UnknownCommand`'s own remarks); `src/AcDream.App/Plugins/AppAutomationSurface.cs` (`PostSystemMessage`); `src/AcDream.Plugin.Abstractions/Automation.cs` (`IPluginChat.PostSystemMessage`) | Direct owner instruction 2026-09-07: "Unknown commands like /vt or stuff from plugins shall now go to the SpewBox. They should go to the chatbox." Resolves a real discoverability gap — an unknown command or plugin notice silently flashed in the barely-visible SpewBox overlay with no transcript record, easy to miss and impossible to scroll back to. | A future re-read of `DoHelp`'s decomp could "fix" this back to `ShowInterfaceText`/`ClientLocal` per retail-faithful defaults, silently re-hiding the feedback the owner asked to keep visible; a NEW producer of "Unknown command" text or plugin system text that bypasses these exact call sites (a future command-dispatch path, a second plugin chat sink) would still hide in the SpewBox unless routed through the same seam. | `ClientCommunicationSystem::DoHelp @0x0057F9E0` (unresolved-verb branch, retail's own `ClientLocal`/0x1A typing — the behavior being overridden); `docs/ISSUES.md` #363/#367 |
| AD-123 | **Filed 2026-09-07 at Campaign VT slice 1 Part A round 3 item 9.** Retail's real loot-profile picker (`aa()`, `uTank2/PluginCore.cs:7127-7154`) seeds ONLY `[None]` — there is no per-character auto loot file and no "mine only" filter for loot at all (`docs/research/vtank-kb/01-settings-and-profiles.md` section 3: "the loot default has no equivalent auto-name; loot profiles default to none"). MossTank keeps its own `ByCharacter`/"By char" auto-profile convention for loot anyway, for internal consistency with the Settings/Nav/Meta stores (all three of which DO have a real retail auto-file). | `src/AcDream.Plugins.MossTank/MossTankLootProfileStore.cs` (`ByCharacter`, `CurrentFileName`); `src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs` (`ListLootProfiles`) | The auto file is a normal `.utl`, named the same `--Name_Server.utl` shape the other three auto-files use; a real VTank install never creates or reads this convention itself, so it is additive, not a collision with anything retail writes. | A user comparing acdream's loot picker to real VTank's own `cmbLootSet` sees an extra "By char" entry retail never shows, and (harmlessly) an extra `--Name_Server.utl` file in a shared real-VTank profile directory. | `aa()` (`uTank2/PluginCore.cs:7127-7154`). |
| AD-122 | **Filed 2026-09-07 at Campaign VT slice 1 Part A round 3 item 7; naming updated 2026-09-07 at Campaign VT slice 1c.** `VtankProfileDirectory.WriteCharacterBinding` writes a real `.cdf`'s Nav/Meta lines (4-5) as MossTank's own `.af` names (metaf's human-readable grammar), not VTank's native binary `.nav`/`.met` — and, since slice 1c's two-folder layout (owner decision 2026-09-07: Meta and Nav profiles both use `.af` and are told apart by folder, not a file-name marker), those two lines are now the folder-relative real storage keys `metas/Name.af`/`navs/Name.af`, not a bare file name. When `ACDREAM_VTANK_PROFILE_DIR` points at a REAL installed VirindiTank profile folder for direct interop, the `.cdf` this store writes there names files a real VTank instance cannot load (wrong format, and — now — a subfolder path a real VTank's own flat-directory `.cdf` reader was never built to resolve). | `src/AcDream.Plugins.MossTank/VtankProfileDirectory.cs` (`WriteCharacterBinding`, `MetaFolder`/`NavFolder`); `src/AcDream.Plugins.MossTank/MossTankMetaProfileStore.cs`/`MossTankRouteProfileStore.cs` (`.af` naming, folder-qualified `CurrentFileName`) | `.usd` settings and `.utl` loot stay real/binary-compatible; only Nav/Meta went `.af`-only for slice 1 (see `docs/research/vtank-kb/06-navigation-and-nav.md`/`07-meta-and-expressions.md`). acdream itself only ever reads its own `.cdf` writes back, so this is self-consistent as long as the two clients never share one profile directory. | A user pointing `ACDREAM_VTANK_PROFILE_DIR` at their real VTank install and then opening that character in real VTank gets a Nav/Meta load failure (wrong format AND wrong path for the recorded filename) even though Settings/Loot still work. Additionally, MossTank's first load MOVES every flat `.af` in that directory into `metas/`/`navs/` (slice 1c migration; one-time, logged, no overwrite on collision). | `da.q()`/`da.e()` (`refs/vtank/decompiled/da.cs:105-164`) — real VTank's own `.cdf` read/write. |
| AD-119 | **Filed 2026-09-03 at Campaign OVERHAUL v2 S4 chunk 1 (S4-c1 C2; `docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md` §6 R3).** Retail's portal-depth draws (`D3DPolyRender::DrawPortalPolyInternal` @0x0059bc90, the `BLEND_SRCALPHA`/`BLEND_INVSRCALPHA` `SetBlendFunction` call) keep color writes ENABLED with a zero-source-alpha `SRCALPHA`/`INVSRCALPHA` blend — every OTHER piece of R3's state (`DEPTHTEST_ALWAYS`, depth write on, `CULLMODE_NONE`, no stencil) is ported exactly. acdream instead disables the color-write mask outright on the SAME pipeline (`ColorWrite = false` alongside `Blend = GpuBlendMode.None`) and `portal_depth.frag`'s `main()` writes no color output at all — a write-mask substituting for a zero-alpha blend. | `src/AcDream.App/Rendering/PortalDepthMaskRenderer.Rhi.cs:92,100` (`CreatePortalPipeline`'s `Blend`/`ColorWrite` fields); `src/AcDream.App/Rendering/Shaders/portal_depth.frag` (empty `main()`, no color output) | Retail's blend equation is `dst' = src*srcAlpha + dst*(1-srcAlpha)`; with `srcAlpha` fixed at 0 this collapses to `dst' = dst` for every fragment regardless of its RGB — the destination color buffer is left byte-identical either way. A write mask reaches the SAME outcome (the destination is never touched) through a structurally simpler path — no blend-unit work per fragment, no fragment color output to author or keep in sync with a "must stay zero" alpha invariant — so the two are pixel-identical, not merely usually-equivalent. | None expected: the equivalence is provable from the blend algebra above, not measured, so no capture, transcript, or visual gate can distinguish the two. The write mask is in fact the SAFER of the two going forward — a future edit that gives `portal_depth.frag` a real color output (e.g. an authored debug tint) still writes nothing under today's mask, where a ported zero-alpha blend would depend on that same edit remembering to keep alpha at exactly 0. | `D3DPolyRender::DrawPortalPolyInternal` @0x0059bc90 (`SetBlendFunction(BLEND_SRCALPHA, BLEND_INVSRCALPHA, BLENDOP_ADD)`, `SetDepthBufferMode(DEPTHTEST_ALWAYS, ...)`, `SetCullMode(CULLMODE_NONE)`); `PortalDepthMaskRenderer.Rhi.cs` |
diff --git a/src/AcDream.App/Plugins/AppAutomationSurface.cs b/src/AcDream.App/Plugins/AppAutomationSurface.cs
index a4f87d0b..9ae412f7 100644
--- a/src/AcDream.App/Plugins/AppAutomationSurface.cs
+++ b/src/AcDream.App/Plugins/AppAutomationSurface.cs
@@ -1170,9 +1170,12 @@ internal sealed class AppAutomationSurface
}
///
- /// Routed to retail's ClientLocal log type (0x1A) — the channel the client
- /// uses for its own notices. Nothing reaches the server, so a plugin cannot
- /// accidentally speak in the player's name.
+ /// Owner direction 2026-09-07 (register row AD-124): plugin-originated
+ /// text now lands in the chat window (retail Default/0x00),
+ /// matching Decal's own AddChatText 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.
///
public void PostSystemMessage(string text)
{
@@ -1181,7 +1184,7 @@ internal sealed class AppAutomationSurface
RuntimeCommunicationState? communication;
lock (_gate)
communication = _communication;
- communication?.AddText(text, RetailLogTextType.ClientLocal);
+ communication?.AddText(text, RetailLogTextType.Default);
}
public bool Submit(string text)
diff --git a/src/AcDream.Plugin.Abstractions/Automation.cs b/src/AcDream.Plugin.Abstractions/Automation.cs
index 894d062b..2e968d1e 100644
--- a/src/AcDream.Plugin.Abstractions/Automation.cs
+++ b/src/AcDream.Plugin.Abstractions/Automation.cs
@@ -302,10 +302,20 @@ public interface IPluginChat
Array.Empty();
///
- /// Post a client-local system line, the channel retail uses for the
- /// client's own notices. It is local to this client: nothing is sent to the
- /// server and no other player sees it.
+ /// Post a plugin-originated system line into the chat window. It is
+ /// local to this client: nothing is sent to the server and no other
+ /// player sees it.
///
+ ///
+ /// Owner direction 2026-09-07 (register row AD-124): this used to route
+ /// through retail's ClientLocal (0x1A) channel — the SpewBox
+ /// overlay every ChatInterface window's default filter excludes.
+ /// The owner explicitly overrode that for plugin text, matching Decal's
+ /// own AddChatText behavior: plugin output now lands in the chat
+ /// transcript (retail Default/0x00) so it is actually visible and
+ /// scrolls back, never the transient overlay. See
+ /// AppAutomationSurface.PostSystemMessage for the implementation.
+ ///
void PostSystemMessage(string text);
///
diff --git a/src/AcDream.Runtime/Chat/ChatCommandRouter.cs b/src/AcDream.Runtime/Chat/ChatCommandRouter.cs
index 3bb86ed9..ffd0e8d2 100644
--- a/src/AcDream.Runtime/Chat/ChatCommandRouter.cs
+++ b/src/AcDream.Runtime/Chat/ChatCommandRouter.cs
@@ -120,13 +120,19 @@ public static class ChatCommandRouter
// Command-shaped but no letter verb ("/", "//shrug", "@ x"):
// refuse locally rather than putting junk on the wire or in speech.
// #363/#367: this is one of retail's DoHelp-family "Unknown
- // command" fallbacks (0x1A ClientLocal, SpewBox-only) — routed
- // through the interface-text seam now that one exists, instead of
- // the chat scroll.
+ // command" fallbacks — retail itself types it 0x1A ClientLocal
+ // (SpewBox-only). Owner-directed override 2026-09-07 (register row
+ // 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 '@'
&& (trimmed.Length == 1 || !char.IsLetter(trimmed[1])))
{
- feedback.ShowInterfaceText(
+ feedback.ShowSystemMessage(
$"Unknown command: {ChatInputParser.GetVerbToken(trimmed)}. Type /help for the list of supported commands.");
return SubmitOutcome.UnknownCommand;
}
@@ -345,7 +351,11 @@ public static class ChatCommandRouter
// SAME fallback an unregistered verb gets — DoHelp's help-
// pointer-null guard skips its callback branch entirely. See
// 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;
}
@@ -370,10 +380,14 @@ public static class ChatCommandRouter
return;
}
- // Retail types this 0x1A (ClientLocal) -> SpewBox-only. #363/#367:
- // now routed through IChatCommandFeedback.ShowInterfaceText instead
- // of the chat scroll — see RetailCommandHelpTable.UnknownCommand.
- feedback.ShowInterfaceText(RetailCommandHelpTable.UnknownCommand);
+ // Retail types this 0x1A (ClientLocal) -> SpewBox-only. #363/#367
+ // originally routed it through IChatCommandFeedback.ShowInterfaceText
+ // for exactly that reason. Owner-directed override 2026-09-07
+ // (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)
diff --git a/src/AcDream.Runtime/Chat/RetailCommandHelpTable.cs b/src/AcDream.Runtime/Chat/RetailCommandHelpTable.cs
index 001920f5..5b0b7c62 100644
--- a/src/AcDream.Runtime/Chat/RetailCommandHelpTable.cs
+++ b/src/AcDream.Runtime/Chat/RetailCommandHelpTable.cs
@@ -210,15 +210,29 @@ namespace AcDream.Runtime.Chat;
///
///
///
-/// Issue #363 (2026-08-10): ChatCommandRouter now routes this
+/// Issue #363 (2026-08-10): ChatCommandRouter routed this
/// fallback (and every other 0x1A command-refusal call site) through
/// IChatCommandFeedback.ShowInterfaceText — an optional hook the host
/// wires to RuntimeCommunicationState.AddText, the same SpewBox
/// chokepoint every other producer of interface text uses. The retained
/// ChatVM 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.
///
+///
+///
+/// Owner-directed override 2026-09-07 (register row AD-124): the
+/// paragraph above still describes retail's own behavior faithfully, but
+/// acdream no longer matches it for exactly this
+/// text (both its call sites in ChatCommandRouter.EmitVerbHelp) and
+/// the sibling "Unknown command: {verb}." refusal in
+/// ChatCommandRouter.Submit's own body: those three sites now call
+/// IChatCommandFeedback.ShowSystemMessage (the chat scroll, retail
+/// Default/0x00) instead of ShowInterfaceText (SpewBox), so an
+/// unknown command is actually visible and stays in the transcript. Every
+/// OTHER 0x1A refusal this class documents (bad-args, AP-183) is
+/// unchanged and still SpewBox-only.
+///
///
public static class RetailCommandHelpTable
{
@@ -266,9 +280,16 @@ public static class RetailCommandHelpTable
// acclient_2013_pseudo_c.txt:395052 (u"Unknown command", UTF-16LE) --
// DoHelp's fallback when the verb hash lookup fails, or resolves to an
// entry with no registered help callback. Retail types this 0x1A
- // (ClientLocal) -- SpewBox-only; see the class remarks' routing note --
- // ChatCommandRouter routes it through IChatCommandFeedback.ShowInterfaceText
- // (issue #363), closing #367.
+ // (ClientLocal) -- SpewBox-only; see the class remarks' routing note.
+ // Owner-directed override 2026-09-07 (register row AD-124): acdream
+ // 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";
// @mr/@pr are registered with a NULL function pointer in the 2013
diff --git a/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceTests.cs b/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceTests.cs
index 9982ace3..b3221fde 100644
--- a/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceTests.cs
+++ b/tests/AcDream.App.Tests/Plugins/AppAutomationSurfaceTests.cs
@@ -146,6 +146,31 @@ public sealed class AppAutomationSurfaceTests
Assert.Equal(0, second.CommunicationOwner.SubscriberCount);
}
+ ///
+ /// 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
+ /// AddChatText uses.
+ ///
+ [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]
public void InventoryCompletionProjectsTheCanonicalRequestReceipt()
{
diff --git a/tests/AcDream.Runtime.Tests/Chat/ChatCommandRouterFeedbackRoutingTests.cs b/tests/AcDream.Runtime.Tests/Chat/ChatCommandRouterFeedbackRoutingTests.cs
new file mode 100644
index 00000000..becd3ea2
--- /dev/null
+++ b/tests/AcDream.Runtime.Tests/Chat/ChatCommandRouterFeedbackRoutingTests.cs
@@ -0,0 +1,111 @@
+using AcDream.Core.Chat;
+using AcDream.Runtime.Chat;
+using AcDream.Runtime.Gameplay;
+
+namespace AcDream.Runtime.Tests.Chat;
+
+///
+/// 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 half of that change at the Runtime
+/// layer — bound to a real
+/// — since the existing router
+/// coverage in AcDream.UI.Abstractions.Tests only exercises the
+/// ChatVM feedback implementation. The plugin-text half is pinned at
+/// the App layer (AppAutomationSurfaceTests.PostSystemMessage_RoutesToChatLog_NeverSpewBox),
+/// since AppAutomationSurface is the App-layer production
+/// implementation of IPluginChat.
+///
+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);
+ }
+}
diff --git a/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs b/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs
index 15cb0cc5..770aae9c 100644
--- a/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs
+++ b/tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs
@@ -194,4 +194,36 @@ public sealed class ChatVMTests
// The stored body never carries the stamp in either state.
Assert.Equal("hi", log.Snapshot()[0].Text);
}
+
+ ///
+ /// Owner-directed override 2026-09-07 (register row AD-124): plugin
+ /// output (AppAutomationSurface.PostSystemMessage, the
+ /// production implementation of IPluginChat.PostSystemMessage)
+ /// now funnels into RuntimeCommunicationState.AddText(text,
+ /// RetailLogTextType.Default), which calls
+ /// Chat.OnSystemMessage(text, (uint)Default) — the exact call
+ /// this test performs directly on the shared ,
+ /// matching Decal's own AddChatText behavior for plugin text.
+ /// Any bound to that log (the production chat
+ /// window) must show the line; it must never depend on the
+ /// SpewBox seam, which this call
+ /// never touches.
+ ///
+ [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);
+ }
}
diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs
index f9f50ad6..72cf6f1c 100644
--- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs
+++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs
@@ -406,28 +406,35 @@ public class ChatCommandRouterTests
}
[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
// fallback text is "Unknown command" (swept verbatim), not an
- // acdream-invented "No help available" message. Retail types this
- // 0x1A (ClientLocal / SpewBox-only). Issue #363/#367: now routed
- // through the interface-text seam as ONE entry (no HelpPrefixNote
- // wrapper — DoHelp's fallback bypasses the two-entry shape
- // entirely), not the chat scroll.
+ // acdream-invented "No help available" message. Retail itself types
+ // this 0x1A (ClientLocal / SpewBox-only). Owner-directed override
+ // 2026-09-07 (register row AD-124): "Unknown command" refusals now
+ // route to the CHAT SCROLL (ShowSystemMessage, Default/0x00) instead
+ // of the interface-text/SpewBox seam — the seam stays empty.
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
var outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
Assert.Empty(bus.Published);
- Assert.Equal(RetailCommandHelpTable.UnknownCommand, Assert.Single(interfaceTexts));
- Assert.Empty(log.Snapshot());
+ Assert.Empty(interfaceTexts);
+ var entry = Assert.Single(log.Snapshot());
+ Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
+ Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
}
[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 outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say);
@@ -435,7 +442,7 @@ public class ChatCommandRouterTests
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
var entry = Assert.Single(log.Snapshot());
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
- Assert.Equal((uint)RetailLogTextType.ClientLocal, entry.LogTextType);
+ Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
}
[Fact]
@@ -588,19 +595,23 @@ public class ChatCommandRouterTests
}
[Fact]
- public void DegeneratePrefix_UnknownCommand_ShowsRefusal_ViaInterfaceTextSeam()
+ public void DegeneratePrefix_UnknownCommand_ShowsRefusal_InChatLog_TaggedDefault()
{
// "/" alone (no letter verb) — the pre-existing "Unknown command:
- // {verb}." refusal, now also routed through the interface-text
- // seam (issue #367).
+ // {verb}." refusal. Owner-directed override 2026-09-07 (register
+ // 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 outcome = ChatCommandRouter.Submit("/", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.UnknownCommand, outcome);
Assert.Empty(bus.Published);
- Assert.Contains("Unknown command:", Assert.Single(interfaceTexts));
- Assert.Empty(log.Snapshot());
+ Assert.Empty(interfaceTexts);
+ var entry = Assert.Single(log.Snapshot());
+ Assert.Contains("Unknown command:", entry.Text);
+ Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
}
[Fact]
diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs
index 85c43d41..03f47abd 100644
--- a/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs
+++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs
@@ -395,10 +395,13 @@ public sealed class RetailCommandHelpTableTests
// assignment, unlike every extracted verb above -- confirming a
// genuinely NULL help function pointer. Retail's own DoHelp skips
// its help-callback branch entirely for these and falls to the
- // SAME "Unknown command" 0x1A text an unregistered verb gets, even
- // though the verb dispatches fine for ordinary (non-help) use.
- // Showing the catalog's own invented summary here would be
- // retail-inaccurate.
+ // SAME "Unknown command" text an unregistered verb gets (retail
+ // itself types it 0x1A), even though the verb dispatches fine for
+ // ordinary (non-help) use. Showing the catalog's own invented
+ // 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 vm = new ChatVM(log, displayLimit: 50);
var bus = new RecordingCommandBus();
@@ -411,7 +414,7 @@ public sealed class RetailCommandHelpTableTests
Assert.Single(entries);
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entries[0].Text);
Assert.Equal(
- (uint)AcDream.Core.Chat.RetailLogTextType.ClientLocal,
+ (uint)AcDream.Core.Chat.RetailLogTextType.Default,
entries[0].LogTextType);
}