fix(chat): Campaign CH round 3 — SpewBox flush-top/font, /help exact print sequence

User-gate round 3 findings (a)-(c):

(a) SpewBox: TopOffset moves from the round-1 60px placeholder to 0 (flush
to the viewport top). SpewBoxController never wired DatFont/Font at all
before this round, so it silently rendered through the 15px debug
BitmapFont fallback; it now resolves retail dat Font 0x40000025
(MaxCharHeight=11px) through a new RetailUiRuntime.Assets accessor —
the smallest font id confirmed in use by any currently-imported retail
LayoutDesc fixture, cross-referenced against every
tests/AcDream.App.Tests/UI/Layout/fixtures/*.json dump and confirmed
against the installed DAT via AcDream.Cli dump-font-atlas. It is also the
chat window's own smallest font (the 0x2100006F floating-window 1/2/3/4
indicator badges), so both selection criteria the brief offered agree.
Both remain best-available approximations, not resolved retail values —
register row AP-178 updated accordingly.

(b)/(c) /help and /help death: round 2 extracted the individual retail
strings byte-exact but never traced ClientCommunicationSystem::DoHelp's
complete print sequence. Byte-swept DoHelp's own range plus the five
Summary-branch functions it calls into (HelpEmote/HelpSquelch/
HelpStatusGroup/HelpTextGroup/HelpAllGroup) against the PDB-paired
acclient.exe. Retail's real shape: bare /help prints exactly TWO scroll
entries (HelpPrefixNote, then the 13-item AvailableHelpListing built from
DoHelp's own literals and each group's Summary_HelpType branch, in exact
source order) — not the acdream-invented cheat sheet BuildHelpText()
built before. Any resolved /help <verb> gets the SAME two-entry shape:
HelpPrefixNote, then ForMoreInformationPrefix concatenated directly onto
the verb's own Detail text (retail's own unsubstituted "<command>"
literal, ported verbatim). ChatCommandRouter.EmitVerbHelp applies this
uniformly to every resolved verb, not just death. An unresolved verb now
shows retail's real "Unknown command" fallback text; that fallback types
0x1A (ClientLocal), which retail routes to the SpewBox exclusively — a
gap ChatVM's UI.Abstractions layer can't yet reach, filed as ISSUES #367
/ register AP-186 rather than left silently unregistered.

Jump-in-air (round 2's open item 1) was root-caused and fixed separately
at a5a7eb4f between rounds — recorded in the campaign ledger.

Debug suite (all projects): 12,329 passed / 4 skipped / 1 failed — the
one failure is issue #351, a pre-existing Debug-only streaming flake
confirmed reproducing identically on the pristine pre-round-3 commit via
git stash, not a regression. Release verification covers every project
reachable without rebuilding AcDream.App: a live client process (PID
15064) held its own Release binaries locked for the session and was not
killed per project policy — AcDream.UI.Abstractions.Tests (867/867, the
layer both /help fixes live in) plus every other non-App-dependent
project, all 0 failed. AcDream.App/AcDream.App.Tests/AcDream.Core.Tests
(the SpewBox fix's layer) are green in Debug only this session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 10:40:19 +02:00
parent a5a7eb4fb6
commit 98de4f5ab3
14 changed files with 665 additions and 105 deletions

View file

@ -192,6 +192,49 @@ click — not decoded by CH6a.
`src/AcDream.App/UI/Layout/LayoutImporter.cs`
(`BuildWidget`/`ConsumesDatChildren` handling); `src/AcDream.App/UI/UiText.cs`.
## #367 — ChatCommandRouter's local-presentation fallbacks type-0x1A text still lands in the chat scroll, never the SpewBox
**Status:** OPEN — filed 2026-08-10, Campaign CH user-gate round 3, while
tracing `ClientCommunicationSystem::DoHelp @0x0057f9e0`'s complete print
sequence for findings (b)/(c). Retail's DoHelp fallback for an unresolved
`/help <verb>` is `AddTextToScroll(u"Unknown command", 0x1A, 1, 0)` — type
`0x1A` (`ClientLocal`) is HARDCODED to the SpewBox, never the chat window
(`docs/research/2026-08-09-chat-retail-interface-text.md` §2.1/§2.2, the
same routing rule Campaign CH user-gate round 2 item 2 already ported for
the portal-space notice). `RuntimeCommunicationState.AddText` (Runtime
layer) implements this rule correctly — `type == ClientLocal` routes to
`SpewBox` only. But `ChatCommandRouter`/`ChatVM` live in
`AcDream.UI.Abstractions`, a layer beneath Runtime that must stay
presentation/Runtime-independent (Code Structure Rules), so they have no
path to the SpewBox at all — every local-presentation fallback
(`RetailCommandHelpTable.UnknownCommand` now; also the pre-existing
"Unknown command: {verb}." command-shaped-input refusal in
`ChatCommandRouter.Submit`'s main body) still writes through
`ChatVM.ShowSystemMessage`, which only ever reaches `ChatLog`. Not a
regression this round — the fallback text was already wrong AND
already routed to the chat window before this round's fix; this round
corrected the TEXT ("Unknown command", byte-exact) and traced the
routing divergence clearly enough to file it. Register row AP-186.
**Fix shape:** either (a) give `ChatVM` (or a sibling in UI.Abstractions)
an optional `Action<string>? OnClientLocalText` hook the App-layer host
wires to `RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal)`
the same way `ChatWindowController`/other retained-UI controllers already
receive delegates from the composition layer, or (b) accept the
divergence permanently as an acdream simplification (all local-presentation
refusals show in the chat window instead of splitting across two surfaces)
and retire AP-186 as an accepted Intentional Architecture row instead. Needs
a product decision, not just an implementation — small either way once
decided.
**Where:** `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`;
`src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs`;
`src/AcDream.Runtime/Gameplay/RuntimeCommunicationState.cs` (`AddText`, the
correctly-implemented Runtime-layer oracle this should eventually reach).
**Campaign:** `docs/plans/2026-08-09-chat-parity-campaign.md` (Campaign CH,
user gate round 3).
## #364 — Three `/help` group topics still partial: HelpStupidChannelHack unresolved
**Status:** OPEN — filed 2026-08-09, Campaign CH user-gate round 2, item 3.

File diff suppressed because one or more lines are too long

View file

@ -33,10 +33,20 @@ character on the probe account), and two temporary graphical-only probes
are left behind for the next round. **CH6a (main-window layout + 8-grip
resize) landed CODE-COMPLETE the same day** — see its ledger row and the
updated round-1 items H/I and round-2 item 6 dispositions below; CH6b
(floating windows 14) and CH6c (opacity) remain not started. Status
stays CODE-COMPLETE pending the next user gate round (still needed for
item 1, CH6a's own visual confirmation, CH6b/CH6c, and a final in-client
visual pass on everything fixed so far).
(floating windows 14) and CH6c (opacity) remain not started. Round 2's
open item 1 (jump-in-air) was root-caused between rounds from live probe
evidence and FIXED at `a5a7eb4f` — a production-controller install path
never wired `OnInterfaceText`. **User gate round 3 ran 2026-08-10 and
found three more findings, all presentation; see "User gate — round 3"
below.** All three are fixed in this round's commit: the SpewBox now
mounts flush to the viewport top and resolves a real (smaller) retail dat
font instead of the unwired 15px debug fallback; bare `/help` and
`/help <verb>` (including `/help death`) now print retail's exact
`DoHelp` shape — two scroll entries in the right order, not one
acdream-invented blob. Status stays CODE-COMPLETE pending the next user
gate round (still needed for CH6a's own visual confirmation, CH6b/CH6c,
round 3's fixes, and a final in-client visual pass on everything fixed so
far).
**Why now:** first track of the alpha-release program (chat is the most
visible daily surface for the friend-alpha). User-directed 2026-08-09.
@ -201,6 +211,9 @@ implementer per slice against a pinned contract (per
| CH6a main-window layout + 8-grip resize | (this commit) | 12,317 passed / 4 skipped / 0 failed | pending (no subagent review pass this session — implementer-only) | pending — needs the next in-client round (items H/I round 1, item 6 round 2) |
| CH6b/CH6c floating windows + opacity | not started | — | — | not started |
| User gate round 2 | (this commit) | 12,267 passed / 4 skipped / 0 failed | — | items 2/4/5 fixed this commit, item 3 confirmed-fixed, item 6 folded into CH6a's spec, item 1 NOT reproduced (see "User gate — round 2" below) |
| CH6a main-window layout + 8-grip resize | `1fd51543` | 12,317 passed / 4 skipped / 0 failed | pending | pending — landed same day as round 2 |
| Jump-in-air root cause (round-2 item 1, resolved) | `a5a7eb4f` | Runtime tests 1,323/0 | — | round-3 probe evidence pinpointed a missing `OnInterfaceText` wire on the production controller-commit path (`RuntimeLocalPlayerMovementState.CommitRuntimeOwnedController`); FIXED, regression test added |
| User gate round 3 | (this commit) | Debug (all projects): 12,329 passed / 4 skipped / 1 failed (pre-existing #351 Debug-only flake — reproduces identically on the pristine pre-round-3 commit, not a regression); Release (every project reachable while a live `AcDream.App.exe` client — PID 15064, must not be killed per project policy — holds its own Release binaries locked, blocking `AcDream.App`/`AcDream.App.Tests`/`AcDream.Core.Tests` specifically): `AcDream.UI.Abstractions.Tests` (the layer this round's `/help` fix lives in) 867/867, plus `Core.Net.Tests` 823/823, `Runtime.Tests` 1,323/1,323, `Content.Tests` 130/130, `Headless.Tests` 89/89, `Bake.Tests` 15/15, `Cli.Tests` 4/4 — all 0 failed | — | findings (a)-(c) fixed this commit — SpewBox flush-top + retail dat font, `/help`/`/help death` exact retail print sequence (see "User gate — round 3" below) |
### CH4 closeout (2026-08-09)
@ -590,3 +603,86 @@ chrome mount), plus new/updated tests in
`tests/AcDream.App.Tests/UI/UiRootInputTests.cs`,
`tests/AcDream.App.Tests/UI/CursorFeedbackControllerTests.cs`, and
`tests/AcDream.App.Tests/UI/Layout/ChatLayoutConformanceTests.cs`.
## User gate — round 3 (2026-08-10)
The user tested round 2's fixes live and reported three more findings, all
presentation. Between rounds, the main session also root-caused round 2's
open item 1 (jump-in-air silence) from live probe evidence and landed the
fix at `a5a7eb4f` — see the standalone ledger row above; not repeated here
since it needed no further work this round.
| # | User finding (condensed) | Disposition |
|---|---|---|
| (a) | The SpewBox yellow text is "still not aligned all the way to the top" and not "the correct font and size" (retail's is SMALLER than ours). | **FIXED this SHA.** Two independent sub-fixes, both best-available APPROXIMATIONS (the true retail values remain unmeasurable — `SpewBoxLayoutDumpDiagnostic` re-run this round still finds no `FontDid`/colour property on element `0x10000048` or its ListBox child): **position**`SpewBoxController.TopOffset` moves from round 1's 60px placeholder to `0` (flush to the viewport top), per the user's explicit direction. **Font** — the controller never wired `DatFont`/`Font` at all before this round, so it silently fell through to the retained-UI host's 15px debug `BitmapFont`; it now resolves retail dat Font `0x40000025` (`MaxCharHeight=11px`) through `RetailUiRuntime.Assets.ResolveFont` (a new public accessor), the SAME memoized resolver the rest of the retained UI uses. `0x40000025` was picked by cross-referencing every currently-imported retail LayoutDesc fixture (`tests/AcDream.App.Tests/UI/Layout/fixtures/*.json`) for the smallest FontDid actually in use — it is 11px, smaller than every other font found (`0x40000000`=16px, `0x40000002`=14px, etc.), confirmed against the installed DAT via `AcDream.Cli dump-font-atlas` sweeping every populated font id `0x40000000`-`0x40000032`. It is ALSO the chat window's own smallest font (the `0x2100006F` floating-window 1/2/3/4 indicator badges use the same id) — both selection criteria the round-3 brief offered landed on the same answer, no tie to break. Register row AP-178 updated (not retired — position/font remain approximations, not resolved retail values; only vertical content flow stays fully OPEN). |
| (b) | `/help` output is "still not what retail displays". | **FIXED this SHA.** Round 2 extracted the individual STRINGS byte-exact but never traced `ClientCommunicationSystem::DoHelp @0x0057f9e0`'s complete PRINT SEQUENCE, so the bare `/help` listing was still an acdream-invented cheat sheet ("Chat: /say...", "Channels: /general...", etc. — none of it retail text). Traced this round via a byte-sweep of DoHelp's own range (`0x57f9e0`-`0x57fe7e`) plus the five Summary-branch functions it calls into, against the PDB-paired `C:\Users\erikn\Downloads\acclient.exe` (verified MATCH). Retail's real bare-`/help` output is exactly TWO scroll entries — `RetailCommandHelpTable.HelpPrefixNote` (now re-swept with its leading blank line and trailing double newline, previously dropped) then `RetailCommandHelpTable.AvailableHelpListing`, a 13-item straight-line concatenation of retail's real topic-group one-liners (allegiances/channels/chatting/death/emote/fillcomps/friends/house/squelch/status/text/commands) in DoHelp's exact source order — never one concatenated blob. `ChatCommandRouter.EmitBareHelp` now emits two `ShowSystemMessage` calls, matching. The acdream-only `RetailClientCommandCatalog.BuildHelpText()`/router `BuildHelpText()` methods that built the old fabricated listing are deleted outright (dead code once nothing calls them). |
| (c) | `/help death` is "still not formatted correctly". | **FIXED this SHA.** The CONTENT (`DeathGroupDetail`'s 8 lines) was already byte-exact from round 2 — what was still wrong was the SHAPE. DoHelp wraps EVERY successfully-resolved `/help <verb>` in the SAME two-entry shape as the bare listing: `HelpPrefixNote` as its own entry, then a SECOND entry that is `ForMoreInformationPrefix` ("For more information, type @help <command>.\n" — retail's own literal; "<command>" is NOT a substituted placeholder, confirmed by the absence of any sprintf/substitution call in the decomp) concatenated DIRECTLY onto the verb's own detail text — no blank line, no third entry, because retail's own handler call appends into the SAME string accumulator the prefix was built into. `ChatCommandRouter.EmitVerbHelp` now applies this wrap UNIFORMLY to every resolved verb (both `RetailClientCommandCatalog` and `RetailCommandHelpTable` lookups), not just death — the general fix, not a death-specific patch, per CLAUDE.md's root-cause discipline. An unresolved verb now shows retail's real fallback text, `RetailCommandHelpTable.UnknownCommand` ("Unknown command", swept verbatim) instead of the acdream-invented "No help available for '{verb}'." — this surfaced a SEPARATE finding: retail types this fallback `0x1A` (`ClientLocal`), which routes to the SpewBox exclusively, never the chat window; `ChatCommandRouter`/`ChatVM` live in `AcDream.UI.Abstractions`, a layer beneath Runtime with no SpewBox access, so the fallback still renders in the chat scroll — not a regression (it was already there, just with fabricated text), now tracked as ISSUES.md #367 / register row AP-186 instead of silently continuing unregistered. |
Evidence: this commit's diff + `tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs`
(new pinning tests for `HelpPrefixNote`, `ForMoreInformationPrefix`,
`UnknownCommand`, `AvailableHelpListing`, and the router-level `/help death`
two-entry shape) + `tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs`
(updated `/help` tests for the new shape) +
`tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs`
(updated entry-count assertions) + `tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs`
(new flush-top / resize / font-wiring tests). Debug suite (all projects)
green — 12,329 passed / 4 skipped / 1 failed, the single failure being
issue #351 (a pre-existing, load-sensitive Debug-only flake in an
unrelated streaming test, confirmed reproducing identically on the
pristine pre-round-3 commit via `git stash`, not a regression from this
work). Release verification was possible for every project reachable
without rebuilding `AcDream.App` — a live client process (PID 15064) held
its own Release binaries locked for the whole session and was not killed
per project policy (`feedback_dont_kill_clients_before_launch`) —
including `AcDream.UI.Abstractions.Tests` (867/867, the layer both `/help`
fixes live in) and every other non-App-dependent test project, all 0
failed. `AcDream.App`/`AcDream.App.Tests`/`AcDream.Core.Tests` (which the
SpewBox fix and the SpewBox tests live in) could not be Release-verified
this session; they are green in Debug.
### Byte-sweep method for findings (b)/(c)
`ClientCommunicationSystem::DoHelp @0x0057f9e0` has two branches
(`arg2 > 0` = verb specified, `arg2 <= 0` = bare `/help`). Both ALWAYS
print via exactly two `AddTextToScroll` calls, both typed `0`
(informational) — never a single concatenated string:
1. The SAME `HelpPrefixNote` line, unconditionally, in both branches.
2. Bare: `AvailableHelpListing` — 13 items, 8 inline literals DoHelp
builds itself (allegiances/channels/chatting/death/fillcomps/friends/
house, plus the header) interleaved with 5 items DoHelp gets by calling
each group's own `Summary_HelpType` branch (`HelpEmote`/`HelpSquelch`/
`HelpStatusGroup`/`HelpTextGroup`/`HelpAllGroup` — every one of those 5
functions has the identical `if (arg2 != Summary_HelpType) {Detail}
else {"@help X - ..."}` shape `HelpEmote` makes explicit at
`acclient_2013_pseudo_c.txt:388664`). Verb-specified (verb resolves):
`ForMoreInformationPrefix` immediately followed (string concatenation,
no separator) by the verb's own Detail-branch text.
When the verb does NOT resolve, DoHelp instead prints ONE entry,
`UnknownCommand`, typed `0x1A` — the SpewBox-exclusive routing type CH
user-gate round 2 item 2 already ported for the portal-space notice.
Tool: `py tools/pdb-extract/sweep_weenie_strings.py
C:/Users/erikn/Downloads/acclient.exe --range 0x57f9e0 0x57fe7e
--ascii-only --min-len 3` (DoHelp's own range) plus the same tool against
each of the five Summary-branch functions' own ranges (`HelpEmote`
`0x578b80`-`0x578c60`, `HelpSquelch` `0x57c190`-`0x57c2d0`,
`HelpStatusGroup` `0x57c410`-`0x57c5a0`, `HelpTextGroup`
`0x57c6c0`-`0x57c860`, `HelpAllGroup` `0x57e7f0`-`0x57eba0`), each range
read from the pseudo-C's own function-header addresses. `check_exe_pdb.py`
confirmed the candidate binary MATCH before any of this.
### SpewBox font selection method for finding (a)
`AcDream.Cli dump-font-atlas <dat-dir> 0x400000XX` (already-existing
tooling, extended nowhere — used as-is) against every populated font id
from `0x40000000` to `0x40000032` (38 of 50 candidate ids populated in the
installed `client_portal.dat`), reading each `Font` DBObj's own
`MaxCharHeight`/`BaselineOffset`/glyph-table-size fields. Cross-referenced
against every `FontDid` value appearing in
`tests/AcDream.App.Tests/UI/Layout/fixtures/*.json` (24 already-imported
retail LayoutDesc dumps) to find the smallest font id actually CONFIRMED
in use by any retail UI import acdream has ported, rather than merely
present in the DAT. `0x40000025` (11px) was both the global minimum across
those fixtures AND the chat window's own minimum — no tie-break needed.

View file

@ -8,14 +8,18 @@ FEEL right".
## 1. On-screen interface text (CH2 — the SpewBox)
- Jump, and press jump again while airborne → **"You can't jump while in
the air"** appears as transient text near the top of the viewport, NOT
in the chat window. Chat gets no line at all.
the air"** appears as transient text flush to the very TOP of the
viewport (round 3, 2026-08-10 — moved off the earlier 60px-down
placeholder), NOT in the chat window. Chat gets no line at all.
- Spam it 5+ times fast: the line refreshes in place (no stacking of
identical text); distinct refusals stack newest-on-top, max 4 lines.
- Try `@version` → its output goes to CHAT (green), not the SpewBox.
- **Report presentation impressions**: position, color, and how long lines
linger are placeholders pending measurement (register AP-177/AP-178) —
say what looks wrong vs your retail memory.
- The SpewBox text should now read visibly SMALLER than round 2 (retail
dat Font `0x40000025`, 11px, replacing the earlier 15px debug font).
- **Report presentation impressions**: position and font are now
best-available approximations, not resolved retail values (register
AP-178); line lifetime is still a placeholder (AP-177) — say what looks
wrong vs your retail memory.
## 2. Chat colors (CH1)
@ -58,11 +62,31 @@ Side-by-side vs retail if possible:
- An unknown verb like `@somenonsense` → passes through to ACE (server
answers, client does not swallow it).
## 5. Help text (round 3, 2026-08-10)
- `/help` (no args) → TWO lines in the chat window: a "Note: You may
substitute a forward slash..." line, then "Available help:" followed by
13 real retail topic one-liners (allegiances/channels/chatting/death/
emote/fillcomps/friends/house/squelch/status/text/commands). This
replaced an acdream-invented cheat sheet ("Chat: /say...", "Client:
/help...") — that text should no longer appear at all.
- `/help death` → the SAME "Note:" line first, then a SECOND line starting
"For more information, type @help <command>." immediately followed by
the 8-line corpse/death command listing — not just the 8 lines alone.
- `/help somenonsenseverb` → "Unknown command" (no "Note:" line before it;
retail's fallback skips the wrapper). This still shows in the CHAT
window rather than the SpewBox — a known, tracked gap (#367/AP-186), not
a new bug to report.
## Known-open, do not report as new
- Ctrl+M mute chord (#358) — still broken, separate from this campaign.
- SpewBox lifetime/position/color are registered placeholders
(AP-177/AP-178) pending a retail measurement session.
- SpewBox line lifetime is still a placeholder (AP-177) pending a retail
measurement session; position/font are now best-available
approximations (AP-178), not confirmed retail pixel values.
- `/help <verb>` fallback ("Unknown command") shows in the chat window,
not the SpewBox — retail types it for the SpewBox exclusively, but
`ChatCommandRouter` has no path there yet (#367/AP-186).
- Allegiance management subcommands print the help refusal instead of
executing (#360); `@day`/`@log`/`@render` deferred (#361); four request
commands send but responses aren't rendered yet (#362).