acdream/docs/plans/2026-08-09-chat-parity-campaign.md
Erik 1aa7709988 fix(chat): CH6a/b rework — grip media, retail window-id model, floaty fixture
Applies docs/research/2026-08-10-ch6ab-review-findings.md in full:

- BLOCKER 1: UiResizeGrip now carries its ElementInfo/resolve pair and
  draws its own authored DirectState media (a synthetic parameterless
  grip still draws nothing, preserving existing resize-drag tests).
  DatWidgetFactory.BuildResizeGrip threads resolve through. All seven
  live grips on the main chat window now resolve a non-zero sprite,
  restoring the visible borders/corners CH6a silently dropped.

- SHOULD-FIX 2: ChatWindowState gains BroadcastTargetWindow, a sentinel
  distinct from every real window id (0-4), fixing the bug where the
  main window's explicit-addressing branch coincided with the broadcast
  check (both were literal 0). SetFilter's main-window no-op is dropped
  — the main window's filter is now genuinely settable. ChatWindowController
  .Bind takes a ChatWindowState (the same canonical instance the floating
  windows already share) and GetTranscriptLines builds a real accept
  predicate instead of accept:null. Verified safe: ClientLocal (0x1A)
  never reaches ChatLog (AddText routes it to the SpewBox and returns),
  so nothing observable regresses.

- SHOULD-FIX 3: UiButton.SuppressSelfToggle stops the four chat-window
  indicator buttons (DAT property 0x0B=true, no retail click handler)
  from flipping their own Selected mirror on a stray click.

- SHOULD-FIX 4: generated and committed chat_floaty_2100005b.json from
  the real installed dats; added the permanent RetailLayoutFixtureGenerator
  entry. All three flagged FloatingChatWindowController assumptions
  (input field, title bar, close button) are confirmed correct against
  real data — no controller code changes needed. New finding: unlike the
  main window, ALL EIGHT floaty border/corner elements are live Type-9
  grips (the floaty's own title bar is its move handle), so a floaty
  window resizes from every edge and corner.

- SHOULD-FIX 5: register row AP-189 documents the shared-500-entry/
  200-line-tail vs retail's per-window 10,000-line scrollback depth gap.

- NITs 1-5: documented the filter-persistence-only-on-/saveautoui
  asymmetry and the reconnect-preserves-filters intent; corrected the
  research doc's modifier-mask mislabel and the "ONLY function" false
  superlative; moved WrapText off ChatWindowController onto
  ChatTranscriptRenderer, closing the circular dependency.

Full Release suite: 12,420 passed / 4 skipped / 0 failed (baseline
12,392/4/0 at 22020ef2; net +28 tests, zero regressions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 13:09:32 +02:00

65 KiB
Raw Blame History

Campaign CH — chat & interface-text retail parity

Status: CODE-COMPLETE 2026-08-09 — all four implementation slices landed and dual-Opus-reviewed; pending the connected user gate (test script: docs/research/2026-08-09-campaign-ch-test-script.md). CH1 complete (implementation 172c6f9a + review fixes 34d8a3c0); CH2 CODE-COMPLETE and CLOSED pending the user gate (77c8296e, REJECT-reviewed at docs/research/2026-08-09-ch2-review-findings.md, reworked e0e78883, re-reviewed APPROVE-WITH-FIXES with nits applied at 233c30d1) — the sole outstanding item is the in-client user gate (jump-in-air / jump-loaded refusals showing on-screen, not in chat). CH3 (side channels) CODE-COMPLETE, pending the connected user gate — see the CH3 row below and docs/research/2026-08-09-chat-side-channels-vs-ace.md. CH4 (command registry completion) CODE-COMPLETE, closed at 5d247d55 — see the CH4 closeout below; 138 of 152 registry verbs now execute locally, 5 are deliberately deferred (issues #360/#361/#362, register rows TS-68/TS-69/TS-70), and 9 are retail's own null-handler help-only nodes. CH5 (this closeout sweep) is COMPLETE — plan/register/ISSUES/ CLAUDE.md/roadmap ledger flip plus the chat memory digest. User gate round 1 ran 2026-08-09 and found ten live defects; see "User gate — round 1" below. Items AG are fixed in round 1's commit; the three remaining findings (extra chat windows on 1/2/3/4, resize only working in one corner, transparency/artifacts) are out of round 1's scope and filed as a new slice, CH6. User gate round 2 ran 2026-08-09 and found six more findings; see "User gate — round 2" below. Items 2 ("In Portal Space…" on the wrong surface) and 4+5 (/help text fabricated/misaligned) are fixed in this round's commit; item 3 is confirmed-fixed with no regression; item 6 is folded into CH6a's existing scope. Item 1 (jump-in-air still silent live) is NOT reproduced and NOT speculatively fixed — round 1's press-edge logic is provably correct by construction from source, a live headless repro was attempted and blocked (no 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. 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. CH6b (floating chat windows 1-4) landed CODE-COMPLETE 2026-08-10 under this session's hard constraints (no subagents, no client launches) — see its ledger row and Slices bullet; CH6c (opacity) remains not started. Status stays CODE-COMPLETE pending the next user gate round (still needed for CH6a's own visual confirmation, CH6b's keybind/mirror/filter behavior, 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.

Scope

Four deliverables, one campaign:

  1. Exact chat colors. The 2026-06-16 cdb session pinned the retail RGBAColor constants (0x81c4a8+), but only 4 of ~13 kinds were mapped with confidence; the type→color lookup table (ChatInterface::BuildChatColorLookupTable @ 0x4f31c0) was never decompiled. This campaign pins the COMPLETE table and conforms ChatWindowController.RetailChatColor to it.
  2. Working side channels vs local ACE. Turbine rooms (General / Trade / LFG / Roleplay / Society) and the legacy family (/f /a /m /p /v /cv), inbound and outbound. The 26-day-old "ACE doesn't run a TurbineChat server" claim is under verification — the research lane reads current ACE source rather than trusting it.
  3. Retail's on-screen interface text. The transient yellow top-of-viewport messages (jump-while-airborne being the canonical example) that retail does NOT put in the chat window. Routing table (on-screen vs chat vs both), presentation (color/duration/fade as far as acclient-side data allows — the draw itself lives in keystone.dll), and the client-raised local errors.
  4. Complete / and @ command registry. Every command the retail client parses, audited against RetailClientCommandCatalog (the 2026-07-13 family port is the baseline, not a restart). Missing commands implemented; non-retail verbs remain ACE server-passthrough per the command ownership rule (2026-07-13).

Out of scope: retail's secondary/floating chat windows and per-window filter masks (post-alpha polish unless a slice lands it for free); chat color user-configurability (we ship retail defaults); TurbineChat server emulation (ACE-side).

Method

Per CLAUDE.md: grep docs/research/named-retail/ first; cdb only where static decomp is insufficient; cross-check ACE + holtburger; conformance tests pin every table (colors, routing, command catalog); divergence register rows land in the same commits; build + full Release suite green per commit.

Model split (user-directed 2026-08-09): research on Opus; planning by the main loop (Fable); implementation slices on Sonnet; every slice gets a dual-lens Opus review — retail faithfulness AND architecture — before its gate.

Research lanes (all Opus, parallel, read-only)

Lane Output doc Question
R1 command registry docs/research/2026-08-09-chat-retail-command-registry.md Complete retail verb/alias/handler table + acdream audit
R2 interface text docs/research/2026-08-09-chat-retail-interface-text.md What draws the yellow text, the routing table, client-raised errors
R3 color table docs/research/2026-08-09-chat-retail-color-table.md Full BuildChatColorLookupTable decompile + wire→type→color trace
R4 side channels docs/research/2026-08-09-chat-side-channels-vs-ace.md Per-family defect diagnosis vs current ACE source

Slices (provisional — finalized when research lands)

Ordering rationale: colors first (small, immediately visible, zero wire risk), then interface text (new presentation subsystem), then side channels (wire work, needs the connected gate), then command breadth, then closeout. Slices touching shared chat files run serially — one implementer per slice against a pinned contract (per feedback_dont_parallelize_coupled_plan_slices).

  • CH1 — exact color table. Conformance test pins every enum entry to its retail RGBA; RetailChatColor corrected; register rows for any kind we cannot yet receive on the wire.
  • CH2 — on-screen interface text. New presentation owner (App layer, retained-UI or TextRenderer HUD path per research recommendation); routing per retail's table; client-raised local errors ported at their retail raise sites; keystone-owned presentation unknowns get register rows.
  • CH3 — side channels. Fix list from R4; connected two-way gate against local ACE (send + receive per family); self-echo semantics per ChatChannelInfo.IsSelfEchoChannel.
  • CH4 — command registry completion. Catalog conformance test pins acdream's table against the retail registry enumeration; missing commands implemented family-by-family.
  • CH5 — closeout. Register sweep, ledger flip, ISSUES updates, in-client test script for the user gate.
  • CH6 — chat-window shell parity (filed 2026-08-09 at user gate round 1; research complete: docs/research/2026-08-09-chat-retail-window-shell.md). Three sub-slices:
    • CH6a — correct main-window import + resize. Swap the wrong LayoutDesc 0x21000006 for retail's 0x2100006F and delete the downstream compensations (hand-cropped 490px ContentWidth, dropped 800px resize bar, the 9px transcript patch, orphan-sibling pruning); teach LayoutImporter element type 9 (UIElement_Resizebar, 8 authored grips: 4 edges + 4 corners via bools 0x2A0x2D); fix the resize mask that excludes Top. Expected to also clear the round-1 artifact report. User gate round 2 (2026-08-09) added two more symptoms to fold into this same slice: resize has no diagonal (corner) cursor feedback — only edge cursors show, the 4 corner grips from the 8-grip authored set above have no matching cursor affordance yet; and the window cannot grow in the Y axis when dragging from the bottom-right corner (a corner-grip axis-composition bug, likely the same resize-mask gap already scoped for the Top edge above, now confirmed to also affect corner grips specifically). CODE-COMPLETE (this commit). The wrong layout swap, the 8-grip LayoutImporter/UiResizeGrip port, and both round-2 symptoms landed together: the real DAT ground-truth (0x2100006F lives in dats.Local, not dats.Portal) showed the top strip is authored as a Type-2 Dragbar, not an 8th Resizebar grip as the research doc's decomp-only reading assumed — UiRoot now gives a directly-hit grip's own edges priority over its generic proximity heuristic, and a directly-hit move handle the same priority over ambient proximity, so the plain top strip moves the window while its two corners (which ARE grips) resize it including the Y axis, matching retail exactly. The diagonal-cursor gap needed no new cursor plumbing — CursorFeedbackController/RetailCursorCatalog already had the exact DAT-matching Type-9 cursor ids pinned; they just never received a genuine diagonal edge combination before. The corner-grow gap dissolved with the RetailWindowChrome.Imported mount (0x2100006F's own border art IS the window chrome — no nine-slice wrapper, no content crop, frame==content) using the DAT's real minH=100/maxH=2000/minW=300/maxW=2000. Register row AP-185 files the one accepted simplification (the _Locked cosmetic border-art swap on UiLocked is not ported; the live grip skin shows unconditionally).
    • CH6b — floating windows 14. Mount 0x2100005B ×4 as always-resident children per gmGamePlayUI::SetupChildren @0x004E9EC0 (ids 0x10000505/0x1000050E/0x1000050F/0x10000510); keybind actions ToggleFloatingChatWindow1..4 (retail defaults at retail-default.keymap.txt:150-153) through the dispatcher; the main window's 1/2/3/4 buttons are one-directional state mirrors (gmMainChatUI::RecvNotice_SetPanelVisibility @0x004CCD80), not the toggle; per-window PostInit filter defaults (color research §4) with the windowId == m_eWindowID OR (windowId==0 && TypeIsActive) display rule. CODE-COMPLETE 2026-08-10 (this commit — hard constraint: no subagents, no client launches). ChatWindowState (Runtime, borrowed from RuntimeCommunicationState.ChatWindows) owns the exact PostInit-default filters + open flags for ids 0-4 and the full ShouldDisplay(windowId, targetWindowId, logTextType) predicate; FloatingChatWindowController (new sibling to ChatWindowController, sharing the wrap/color algorithm via the new ChatTranscriptRenderer) binds all four windows, each importing its own widget tree from one shared 0x2100005B ElementInfo parse. The keymap default confirmed Alt+1..4 (MetaKeys index 3 = 0x00000004, cross-checked against the file's own Alt+A/D strafe and Alt+Enter/Tab/F4 rows — KeyBindings already carried this binding since Phase K.1c). A direct decomp read of gmMainChatUI::ListenToElementMessage @0x004CDA80 (the only function in the binary that branches on a click message) settled the button-mirror-vs-toggle question the research doc had left as a hedge: the four indicator buttons carry NO click handler in retailChatWindowController.SetIndicatorOpen ports this as a pure one-directional mirror, no OnClick. RetailUiRuntime. OnWindowVisibilityChanged is the single chokepoint that both syncs ChatWindowState.SetOpen and calls the indicator mirror, regardless of what changed a window's visibility (keybind, close button, or a restored layout). Geometry + open/visible persist for free through the existing RetailWindowLayoutPersistence path once each window registers under its own WindowNames entry; the filter masks get a dedicated local ChatSettings round-trip (register row AP-187 — no retail 0x1000008C wire yet). One approximation, register row AP-188: a floaty window's entry field always sends on Say (no talk-focus menu is authored on 0x2100005B, and whether retail's ACTUAL send path reads a per-window or a shared globally-current channel is unconfirmed). Full Release suite 12,392 passed / 4 skipped / 0 failed.
    • CH6c — opacity. Implement UiRenderContext.AlphaMod consumption (whole-composited-window alpha per ChatInterface::SetOpacity @0x004F3120); the two GLOBAL retail options 0x10000080 (unfocused) / 0x10000081 (focused, active >= default); retail defaults now, the user-facing Settings slider in the settings track. Persistence: local settings first — the retail per-window option array 0x1000008C is stored by ACE as an opaque byte[] it never parses, so the wire format is its own deferred slice.

Gates

  • Per slice: dotnet build green, full Release suite green, conformance tests, register rows same-commit, Opus dual review resolved.
  • Campaign: user in-client gate — colors side-by-side vs the retail client, each side channel spoken + heard, on-screen text provoked live (jump in air), command spot-checks. Test script delivered at CH5.

Ledger

Slice Commit Suite Review User gate
R1R4 research see docs/research/2026-08-09-chat-retail-*
CH1 colors 172c6f9a 11,835 passed / 4 skipped / 0 failed APPROVE-WITH-FIXES; fixed 34d8a3c0 pending
CH2 interface text 77c8296e, reworked e0e78883 11,916 passed / 4 skipped / 0 failed REJECT → reworked e0e78883 → re-review APPROVE-WITH-FIXES → nits 233c30d1 pending
CH3 side channels 614a1e05 11,964 passed / 4 skipped / 0 failed APPROVE-WITH-FIXES; fixed e07fba57 pending (connected gate — see handoff below)
CH4 commands 090825e7 12,221 passed / 4 skipped / 0 failed REJECT; fixed 724ef2d3; re-review APPROVE-WITH-FIXES; closed 5d247d55 pending
CH5 closeout (this commit) — (docs/memory only, no build) pending (connected gate — see test script)
User gate round 1 (this commit) 12,221 passed / 4 skipped / 0 failed (baseline; items AG fixed this commit) items AG user-gate round 1 fixed; ten findings total, see "User gate — round 1" below
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 superseded — split below superseded
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, reworked (this commit) 12,420 passed / 4 skipped / 0 failed REJECT (docs/research/2026-08-10-ch6ab-review-findings.md, BLOCKER 1) → reworked (this commit) pending — needs the next in-client round to confirm the border/corner art now renders
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)
CH6b floating windows 14 22020ef2, reworked (this commit) 12,420 passed / 4 skipped / 0 failed REJECT (docs/research/2026-08-10-ch6ab-review-findings.md) → reworked (this commit) — SHOULD-FIXES 2/3/4/5 + NITs 1-5 applied pending — no client launches this session (hard constraint); needs the next connected round for keybind/mirror/filter visual confirmation, plus the new 0x2100005B fixture's resolved-type assumptions
CH6c opacity not started not started

CH4 closeout (2026-08-09)

Full parser-semantics + catalog-breadth pass against docs/research/2026-08-09-chat-retail-command-registry.md's complete 130-registered + 22-fallback = 152-verb enumeration.

A. Parser semantics (ChatInputParser.cs, RetailClientCommandCatalog.cs, ChatCommandRouter.cs):

  1. :/; emote-prefix rewrite (OnChatCommand cases 0x0B/0x0C) — both prefixes rewrite identically to @emote <rest>.
  2. Verb trailing-comma trim (DoCommand's right-trim) applied at every verb-lookup site in both the catalog and the parser — "@f, hi""@f hi".
  3. @tell/aliases now split the target on the FIRST COMMA, not the first whitespace token — "@tell Aunt Agatha, hello" addresses "Aunt Agatha" (previously truncated to "Aunt"). Falls back to the pre-CH4 whitespace+punctuation-strip split when no comma is present, so existing single-word-target muscle memory still works.
  4. The 22 unregistered ChannelSystem::GetChannelID fallback tags (av, admin, sentinel, celestialhand, …) now broadcast for real via a new RetailChannelTagTable + SendRawChannelCmd bypass path, reusing the existing BuildChatChannel wire builder — no new opcode needed.

B. Binding corrections:

  1. /g//group//party → Fellowship (0x800), not General — the live correctness bug the doc flagged as Tier-1 #1.
  2. /rp → reply alias (confirmed by retail's own help text, "You may also use @r or @rp"), not Roleplay. Roleplay keeps roleplay/crp; the non-retail /role invention is deleted.
  3. /allegiance//all are now RetailClientCommandCatalog's allegiance MANAGEMENT command (a new TryMatchAllegiance dispatcher), not a channel verb. The channel-send verbs stay a/ab/guild/gu. Corrected 2026-08-09 at the CH4 REJECT-review (Blocker 1): the original implementation above only claimed ownership for the 2 ported subcommands (info/hometown/ho) and let every OTHER subcommand fall through the unregistered-tag channel-fallback path, which broadcast the raw subcommand text to the Allegiance chat channel (0x02000000) — a real chat-visible bug (@allegiance boot Bob sent "boot Bob" to allegiance chat). Retail's own DoAllegiance claims the ENTIRE verb unconditionally: an unrecognized subcommand prints "Please see @help Allegiance for more information on how to use this command." locally and never reaches DoChannelCommand or the server. TryMatchAllegiance now matches this exactly — it always returns ownership for allegiance/all, showing retail's refusal text for any subcommand beyond the 2 ported ones.
  4. /house//hou no longer swallows unrecognized subcommands with a local usage error — TryMatchHouse returns no match for anything beyond recall/re/mansion_recall/alleg_recall/ma/abandon, letting it reach ACE.
  5. @mr/@pr pinned as permanently non-executable (MrPr_AreNeverExecutable test) — retail registers them with a NULL function pointer; they must never resolve in RetailClientCommandCatalog or ChatInputParser.

C. New verbs implemented (real local execution, not passthrough): endurance, speaker, title (silent — AP-182, no chrome yet), chat, notell, join, leave, permit, hslist, index, clist, on, off, alh/ah (+ @allegiance hometown/ho), @allegiance info, @house abandon; missing-alias sweep (pkl, hou, message_types, msgtypes, msg_types, rt, send, whisper, w, vassal, covassal, co-vassals, c, fellows, group, party, guild, gu, cg, ct, clfg, crp, soc, o, ab (already CH3)); the non-retail inventions gen, cv, lookingforgroup, tr, role, h are deleted. New Core.Net wire builders: IndexChannels/ListChannels/ AddChannel/RemoveChannel/RecallAllegianceHometown/ AllegianceInfoRequest/ListAvailableHouses/AddPlayerPermission/ RemovePlayerPermission/AbandonHouse — all parameterless or single-field payloads cross-checked against ACE's GameAction readers (references/ACE/Source/ACE.Server/Network/GameAction/Actions/*.cs), not guessed. Deferred, filed as issues #360/#361/#362 + register rows TS-68/TS-69/TS-70: the ~22 remaining allegiance/house subcommands + the standalone @motd, the three still-inert pure-local commands (day/log/render), and the four unparsed inbound GameEvent responses for the new outbound requests.

D. Conformance: RetailCommandRegistryConformanceTests (new, tests/AcDream.UI.Abstractions.Tests/Panels/Chat/) enumerates all 152 verbs from the registry doc, transcribed and cross-checked against the doc's own per-section counts (130 = 9+31+20+14+6+7+17+8+18 by section; 22 fallback tags; totals self-consistent). Per-verb theory test asserts Implemented verbs resolve through exactly one of RetailClientCommandCatalog/ChatInputParser/RetailChannelTagTable, and HelpOnly/ServerPassthrough verbs resolve through NONE of them (so they provably fall to ACE passthrough). Two reverse-direction tests enforce the ownership rule: nothing in RetailClientCommandCatalog. KnownVerbs or ChatInputParser.KnownVerbs may exist outside this registry — a future invented alias fails the build immediately. Final tally: 138 Implemented / 5 ServerPassthrough / 9 HelpOnly = 152.

Suite: 12,190 passed / 4 skipped / 0 failed (Release), up from CH3's 11,964/4/0 — net +226 tests. (Corrected 2026-08-09 at the CH4 REJECT-review, item 8: this paragraph originally read "12,026 ... net +62 (157 new conformance-family cases plus net test churn)"; the actual measured CH4-landing count was 12,190, matching CLAUDE.md's Current Suite baseline — only the raw counts are corrected here, the +62/157 breakdown was not re-derived.) One pre-existing, environment-specific Debug-only failure (LandblockBuildOriginTests.FarLoad_StripsEnvCellsAndPhysicsEvenWhenEntityListIsAlreadyEmpty) was confirmed present on the unmodified baseline via git stash before and after this slice's changes — passes in Release, unrelated to chat.

CH4 REJECT-review fixes (2026-08-09)

Two blockers, seven should-fixes, and six nits from the CH4 review landed:

Blockers: (1) @allegiance <sub> for an unrecognized subcommand was broadcasting the raw subcommand text to the Allegiance chat channel (SendRawChannelCmd(0x02000000, ...)) because TryDispatchChannelFallback only guarded on ChatInputParser.IsKnownVerb, and /allegiance had been deleted from that parser at CH4. Fixed at both ends: TryMatchAllegiance now claims ownership of allegiance/all unconditionally (matching retail's own DoAllegiance, which never falls through to DoChannelCommand) and shows retail's own "Please see @help Allegiance for more information on how to use this command." refusal client-side; TryDispatchChannelFallback also gained a blanket RetailClientCommandCatalog.KnownVerbs ownership guard as defense in depth for the rest of the catalog. (2) @house abandon sent 0x021F immediately with zero confirmation; retail's DoHouse abandon branch runs a real two-stage dialog ("Do you really want to abandon your house? ..." then "Are you absolutely certain you wish to abandon your house? Click yes only if you are sure!") before Event_AbandonHouse(). ClientCommandController's HouseAbandon case now chains two ShowConfirmation calls with retail's verbatim text; AbandonHouse only fires after both accepts.

Should-fixes: a bare unregistered tag with no text (@admin) now passes through to the server silently, matching retail's DoChannelCommand returning 0 on argc<=0, instead of showing "You must specify the text you wish to say!" (that string belongs to the registered-verb-only DoStupidChannelHack); @join/@leave now update RuntimeCharacterOptionsState locally (a new SetOptionBit method) before the wire push, so TurbineChatMembershipGate stops refusing a just-joined room without waiting on a fresh PlayerDescription; @permit add/remove now accepts a multi-word name (>= 2 tokens, joins the remainder, matching retail's JoinArgsAsName); @clist/@on/ @off now validate only argument SHAPE (exactly one token) at the catalog layer and raise WeenieError 0x422 ("That channel doesn't exist.") for an unresolved tag, instead of silently doing nothing; @mr/@pr's help text is now the verbatim retail strings from data_7daa08/data_7daa80 (previously fabricated acdream summaries), and the class doc no longer overclaims every table entry is verbatim (the ~35 channel one-liners are acknowledged as acdream summaries); issues #360 and register row TS-68 corrected — @house's unported subcommands still reach ACE, but @allegiance's now correctly stay client-side; the campaign doc's own B.7 note and RetailChannelTagTable's stale "IsKnownVerb intercepts them first" comment are corrected to describe the catalog-ownership interception path; the ledger suite counts above are corrected from a stale 12,026 to the actual 12,190. The error-typing debt (~10 new refusal sites at ClientLocal 0x00 where retail types several 0x1A, plus DoCommand's real HandleFailureEvent (0x26) bad-args response) was deliberately NOT re-plumbed — filed as issue #363 and register row AP-183, CH5-or-later.

Nits: TryMatchHouse's doc comment no longer describes a local-swallow path that doesn't exist in the code; AP-182 and the SetChatTitle/@title comments across three files no longer claim the value is "stored" (the binding is _ => { }, a pure no-op) and AP-182 now lists DoTitle's three omitted failure messages; RetailChannelTagTable .IsUnregisteredFallbackTag now excludes by TAG STRING instead of channel ID, fixing a false-positive on "olthoi" (which shares an id with the genuinely-unregistered "ol" but has its own registered Turbine verb); two binding-level conformance pins (/g→Fellowship 0x800, /rp→reply) were added to RetailCommandRegistryConformanceTests so a rebind regression fails there, not just a narrower parser test; @index foo is now accepted (retail's DoChannelIndex ignores argc); an ISSUES.md note records the six invented verbs (gen/cv/lookingforgroup/tr/role/h) removed at CH4 for registry parity.

Suite: 12,216 passed / 4 skipped / 0 failed (Release), up from CH4's 12,190/4/0 — net +26 tests (new/expanded theory cases across ChatCommandRouterTests, RetailClientCommandCatalogTests, RetailCommandRegistryConformanceTests, ClientCommandControllerTests, RuntimeCharacterStateTests, and TurbineChatMembershipGateTests; no tests removed, several renamed/retargeted in place).

CH4 re-review fixes — closed at 5d247d55 (2026-08-09)

A second, smaller review round found three more should-fixes after 724ef2d3 landed: (1) RetailDialogFactory.CloseDialog's queued branch ran DialogDone — whose callback can synchronously reopen a new dialog under the SAME queue key, exactly what the two-stage house-abandon confirmation does — then called OpenNextDialog, which did an unconditional Dictionary.Add on a key the reentrant dialog had already re-occupied; retail's HashTable::add tolerates the duplicate, Dictionary throws. OpenNextDialog now returns early when the queue key is already active. (2) @join/@leave wrote the local RuntimeCharacterOptionsState bit before sending, but the Settings Chat toggles reached a second binding (SendSingleCharacterOption) that only sent the wire message, leaving the Turbine membership gate stale until the next PlayerDescription; LiveSessionRuntimeFactory.CreateCommandBindings now shares one local function for both entrances. (3) TS-68/#360's wording was corrected again — retail's DoAllegiance dispatcher table EXECUTES the nine boot/ban/officer/title/motd/name/lock/house/chat/broadcast subcommands locally through their own handlers; acdream shows the unrecognized- subcommand refusal for all nine pending the #360 port. What matches retail is the ownership rule (the verb never reaches DoChannelCommand/the server), not the subcommand behavior itself — the inaccurate "now matches this" claims were removed from both the register row and the issue.

Suite: 12,221 passed / 4 skipped / 0 failed (Release), up from CH4's 12,216/4/0 — net +5 tests, no removals. This is the CH4 slice's final number; the ledger row above and CLAUDE.md's Current Suite baseline both carry it.

CH3 closeout handoff (2026-08-09)

All nine steps of the research doc's §6 fix list landed:

  1. The false "ACE doesn't run a TurbineChat server" claim retracted in docs/ISSUES.md (#19) and docs/plans/2026-04-11-roadmap.md (I.6, ×2).
  2. TurbineChatMembershipGate (new, AcDream.Runtime.Gameplay) ports retail's SendTurbineChat @0x0057db10 local pre-send gate — Turbine off/room-0 → "Turbine chat is not available."; Hear-option off → 0x0551 YouAreNotListeningTo_Channel — both raised through RuntimeCommunicationState.AddText. Wired into BOTH LiveSessionCommandRouter (graphical) and DirectGameRuntimeCommandAdapter (headless) so the two hosts can't diverge. RuntimeCharacterState.IsOlthoiPlayer added (heritage-gated, not an option) for the Olthoi room.
  3. SetSingleCharacterOption (0x0005) implemented end to end (codec, WorldSession.SendSetSingleCharacterOption, IRuntimeCharacterCommands. SetSingleOption, both adapters, LiveSessionCommandRouter registration) and wired to the 5 Settings Chat toggles via RuntimeSettingsController. SaveChat (publishes only the CHANGED bits) through a hoisted LiveSessionCommandSurface now shared with RuntimeSettingsTargets. No 6th (Allegiance) toggle was added — ChatSettings has never had one and retail's own Settings UI was not confirmed to have one either; flagged for the user rather than guessed.
  4. ChatSettings seeded from server truthRuntimeSettingsController. SyncChatFromServerOptions reseeds both the persisted snapshot and any live unsaved draft from CharacterOptions2 whenever a fresh PlayerDescription lands (LiveCharacterSessionBindings. OnCharacterOptionsChanged, new optional hook).
  5. Self-echo double-print fixedLiveSessionCommandRouter. RouteLegacyChannel now consults ChatChannelInfo.Legacy(...). IsSelfEchoChannel(); Fellow/Vassals/Patron/Monarch/CoVassals skip the local echo (server resends with "" sender), AllegianceBroadcast/Say/Tell keep it. Existing TellAndLegacyChannel_PreserveOutboundAndEchoPolicy test corrected to the fixed (single-print) expectation.
  6. TurbineChat ack HResult surfacedLiveSessionEventRouter. RouteTurbineChat now switches on Payload.Response{HResult}; nonzero surfaces as a system chat line, zero (the common case) stays silent matching retail.
  7. /a routes to Turbine unconditionallyChatChannelKind.Allegiance is exhaustively dispatched to the Turbine pipeline (never falls through to legacy); a new ChatChannelKind.AllegianceBroadcast + /ab verb owns the legacy 0x02000000 path retail's own @ab verb uses. /allegiancebroadcast was deliberately NOT added — the retail command registry (docs/research/2026-08-09-chat-retail-command-registry.md) only has ab, not that long form.
  8. Malformed builders resolvedSocialActions.BuildSetCharacterOptions (0x01A1, no caller), BuildAddChannel/BuildRemoveChannel (0x0145/0x0146, wrong payload type, no caller) DELETED along with their entire call chain (WorldSession.SendSetCharacterOptions, IRuntimeCharacterCommands. SetOptions1, SetCharacterOptionsRuntimeCmd) — replaced by SetSingleCharacterOption, the message step 3 actually needed.
  9. Register + memory — AP-181 (no client-side chat spam throttle) and UN-9 (an incidentally-discovered, unexplained one-byte CharacterOptions1.Default mismatch vs ACE's own literal — not investigated further, flagged for a future pass) filed in docs/architecture/retail-divergence-register.md. claude-memory/project_chat_pipeline.md line ~111 corrected.

Deviations from the literal ordered list: none structural; the two notes above (no 6th Allegiance toggle, no /allegiancebroadcast verb) are scope-narrowing decisions made against the retail command registry and the existing ChatSettings shape, not skipped work.

CH3 Opus review fixes (2026-08-09)

The review found the CH3 closeout above had three items that no longer match the fixed code — corrections, not a rewrite of the historical record:

  • Item 5 is now WRONG for AllegianceBroadcast. ACE's GameActionChatChannel handler iterates player.Allegiance.Members — the sender IS a member, so they get their own real-name line back through the same broadcast, same as Fellow/Vassals/Patron/Monarch/ CoVassals (a different mechanism, same self-echo consequence). ChatChannelInfo.Legacy.IsSelfEchoChannel() now returns true for 0x02000000 too; RouteLegacyChannel skips the local echo for it.
  • Item 7 is now WRONG. /a is NOT unconditionally Turbine. Retail's base binding keeps it on the legacy AllegianceBroadcast bitflag until StartupTurbineChatSystem successfully starts Turbine chat and rebinds it (research doc §4.3). LiveSessionCommandRouter.RouteChat and DirectGameRuntimeCommandAdapter.TrySendChannel now special-case TurbineChatState.Enabled == false to fall back to the legacy send; Enabled == true with AllegianceRoom == 0 still correctly refuses locally ("Turbine chat is not available.").
  • Item 9's UN-9 filing was a phantom. ACE's own CharacterOptions1.cs:47 OR-sum is 0x50C4A54A (confirmed by its own inline comment, // 1355064650), identical to acdream's PlayerDescriptionParser.cs:217 — there was never a divergence. The wrong literal 0x50C48D4A existed only in docs/research/2026-08-09-chat-side-channels-vs-ace.md. UN-9 is deleted from the register; AP-181 is rewritten to name BOTH of retail's omitted pre-send checks (IsMessageSafe silent-drop, THEN IsMessageSpam) and no longer misattributes either to RouteLegacyChannel.

Also fixed this review: ChatSettings.Default now matches ACE's real CharacterOptions2.Default (Roleplay/Society start OFF, not the previously-claimed "all on"); TurbineChatMembershipGate reuses TurbineChatDisplayNames.Resolve instead of a second name table; the gate-result-to-refusal-text mapping is shared via TurbineChatMembershipGate.ResolveRefusalText instead of being duplicated in both hosts; the CharacterOptionId enum in SocialActions.cs moved below the class so its doc comment re-attaches correctly; docs/plans/2026-04-11-roadmap.md line ~948 got the same TurbineChat-server retraction already applied at line ~429; and the register's §3 header count was corrected from a pre-existing off-by-one (129 claimed vs 128 actual | AP- rows).

What the connected gate must verify (not run this session — build+test only per the CH3 task's hard constraint): General/Trade/LFG round-trip send+receive; Roleplay is now silent-but-correctly-refused until the user turns it on via Settings (then works); /a with and without an allegiance; /ab; the legacy family no longer double-prints; the TurbineChat ack HResult line never appears on an ordinary successful send.

User gate — round 1 (2026-08-09)

The user tested CH5's CODE-COMPLETE build live and reported ten defects. Items AG are fixed in this same commit; the last three are out of this round's scope and filed as slice CH6.

# User finding (condensed) Disposition
A Jumping while already airborne never shows retail's "You can't jump while in the air" refusal — the jump block only ever evaluates input.Jump inside the grounded-charge or already-charging branches. FIXED this SHA. Rising-edge detection (PlayerMovementController._prevJumpHeld) reports WeenieError.NotGrounded once per press while airborne; holding the key or the grounded charge/fire path is unaffected.
B Local system text shows an invented "[System] " prefix; retail prints it bare. FIXED this SHA. ChatVM.FormatEntry's ChatKind.System case now returns entry.Text unprefixed. [Popup] is unchanged (AP-175, a deliberate divergent marker).
C The SpewBox's color doesn't match retail — the user recalls it as the same bright yellow as an incoming Tell. FIXED this SHA (color only). SpewBoxController.SpewBoxColor is now the exact pinned value (1, 1, 0.247, 1) (0x81C4C8, same as RetailChatColorTable.Yellow). Size/position/font remain OPEN under register row AP-178 — the user reports all three still differ from retail; user gate round 1: differs, iterating.
D The portal-space "In Portal Space - Please Wait..." text never shows, and when it does (via #329's 5-second delay) it's the wrong (white) color. FIXED this SHA, closes #329. PortalTunnelPresentation.TickRotation now emits the notice unconditionally on every rotation-segment boundary, matching gmSmartBoxUI::UseTime's decompiled else-arm exactly (no hold/threshold gate); PortalWaitNoticeController now renders it in the same pinned yellow as item C. Register row AP-150 retired.
E /hslist villas (and the other three CH4-added request commands) is accepted server-side but produces no visible response. FIXED this SHA, closes #362. New ClientCommandResponses.cs parses and renders ChannelIndex/ChannelList/AvailableHouses/AllegianceInfoResponse, each ported line-for-line from the named-retail decomp's inbound handlers. Register row TS-70 retired.
F Multi-line server text (e.g. /help's reply) doesn't split on embedded \n — "probably broken in many places." FIXED this SHA. ChatWindowController.WrapText now splits on \n/\r\n first, then word-wraps each segment independently; the confirmed-correct single-line early-out is unchanged for text with no embedded newline.
G The chat input line overflows the window's right edge when the window is resized. FIXED this SHA. The input field's right edge no longer holds a fixed absolute pixel position across a resize (retail edge-mode 0's "frozen at current" fallback, or the AnchorEdges default with no Right bit) — ChatWindowController.Bind now upgrades it to retail edge-mode 1 (UiLayoutPolicy) or the equivalent AnchorEdges.Right stretch, so the right edge tracks every resize instead of only the bind-time/channel-change recompute.
H Extra/duplicate chat windows appear on number keys 1/2/3/4. STILL CH6b (not this commit) — retail's real floating windows 14 (0x2100005B ×4) and their ToggleFloatingChatWindow1..4 keybinds are a separate slice; CH6a only fixed the shell (import/resize) of the main window.
I Resizing the chat window only works from one corner, not every corner. FIXED at CH6a (this commit). Root cause: the main window imported the WRONG LayoutDesc (0x21000006, an unrelated layout whose root/resize-bar appear nowhere in the EoR gameplay UI) instead of retail's real 0x2100006F; every symptom (crop hacks, the dropped resize bar, the one-corner-only resize) was downstream of that. The swap + a new LayoutImporter case for element type 9 (UIElement_Resizebar, UiResizeGrip) + UiRoot grip-priority hit-testing now resize from all 4 edges and all 4 corners, while the top strip (a Type-2 Dragbar, not a grip) correctly remains a move-only affordance.
J The chat window has transparency issues / visual artifacts, and the user wants a transparency setting eventually. Artifacts FIXED at CH6a (this commit) — the reported visual glitches were downstream of importing the wrong LayoutDesc (stray unparented siblings, the hand-cropped content width, the 9px patch); all retired with the correct import, and the two hard-coded translucent-black tints on the transcript/input are removed now that their parent panels draw their own authored background sprites. Real opacity (the future transparency SETTING) remains CH6cUiRenderContext.AlphaMod still has no draw-path consumer.

Findings AG's evidence: this commit's diff + the new/updated tests in tests/AcDream.Runtime.Tests/Gameplay/PlayerMovementControllerTests.cs, tests/AcDream.UI.Abstractions.Tests/ChatVMTests.cs, tests/AcDream.App.Tests/UI/SpewBoxControllerTests.cs (unchanged; verified by inspection — no test pinned the old color), tests/AcDream.App.Tests/UI/PortalWaitNoticeControllerTests.cs, tests/AcDream.Core.Net.Tests/Messages/ClientCommandResponsesTests.cs, and tests/AcDream.App.Tests/UI/Layout/ChatWindowControllerTests.cs. Full Release suite green (see the commit message for the exact count). Items H/I/J need the next visual round once CH6 lands; AG still want a final in-client eyes-on pass to confirm the fix reads correctly on screen (build

  • test green is necessary, not sufficient, for a presentation change).

User gate — round 2 (2026-08-09)

The user tested round 1's fixes live and reported six more findings.

# User finding (condensed) Disposition
1 Jumping while already airborne is STILL silent live — round 1's press-edge branch (PlayerMovementController._prevJumpHeld) has a passing unit test but never visibly fires in the running client. NOT REPRODUCED; NO SPECULATIVE FIX SHIPPED. Exhaustive static re-audit of the whole live path (branch logic, _body.OnWalkable's single writer — the quantum-loop resolve, driven only by real physics results, never anything else — the exact-once-per-frame Update()/Capture() call site, TakeControlFromServer's edge-history reset scope, mouse-look's extra Capture() calls) found no bug: the mechanism is provably correct BY CONSTRUCTION from source. Attempted a live headless repro (new jump-probe bot policy exercising the SAME typed commands.Movement.SetIntent surface, real ACE connect at 127.0.0.1:9000) — blocked: probeaccount2 connects but has NO CHARACTER ("no available characters on account"), and per the task's own constraint the fallback to testaccount is refused because the graphical client (PID confirmed running this session) owns that account. Left TWO temporary probes behind a PhysicsDiagnostics-family flag (ACDREAM_PROBE_JUMP=1, blocked entirely in Headless by the existing multi-session static-state guard, so this is a GRAPHICAL-client-only diagnostic for the next round): [jump] in ReportJumpRefusal (prints unconditionally, even when OnInterfaceText is null, to separate "branch never evaluated true" from "callback dropped it"), and [jump-tick] (per-tick trace bracketing every frame where Jump is/was held). Next round: launch with ACDREAM_PROBE_JUMP=1, reproduce, and read the console trace — it will show exactly which of the three jump branches fires and what OnWalkable/_prevJumpHeld were at that instant.
2 Retail shows "In Portal Space…" at the TOP of the screen, SMALL font, tell-yellow (the SpewBox) — acdream renders a big centered white/yellow overlay instead. FIXED this SHA. Verified in the decomp: gmSmartBoxUI::UseTime's notice emits via ECM_UI::SendNotice_DisplayStringInfo(0x1A, ...), which forwards to AddTextToScroll(str, 0x1A, 1, 0) — type 0x1A is HARDCODED to the SpewBox (docs/research/2026-08-09-chat-retail-interface-text.md §1.1/§4.2), the same surface every other ClientLocal refusal (jump-in-air, etc.) already uses. PortalWaitNoticeController (the dedicated centered-overlay presentation) and its lease are DELETED outright; PortalTunnelPresentation's round-1 per-rotation-segment emission cadence is unchanged, now writing straight into RuntimeCommunicationState.AddText(text, RetailLogTextType.ClientLocal) — the SpewBox's own dedupe-at-index-0 (SpewBoxState.Tick) collapses the per-segment repetition exactly as retail's gmSpewBoxUI::Update does. No conflict with the decomp — the fix and the user's report agree exactly.
3 (Round 1 item B, [System] prefix) CONFIRMED FIXED — no regression, no further action.
4+5 /help output differs from retail; /help death prints an acdream META-MESSAGE ("This is a retail help-topic group; acdream has not yet extracted its exact retail listing text…"); spacing/alignment is off. FIXED this SHA via verbatim extraction, not authorship. tools/pdb-extract/sweep_weenie_strings.py generalized to decode narrow PStringBase<char> literals (the Help* family's shape) alongside its original UTF-16LE support, then swept every ClientCommunicationSystem::HelpXxxGroup function's exact byte extent (read from the pseudo-C's own function-header addresses) against the PDB-paired C:\Users\erikn\Downloads\acclient.exe (verified MATCH). 4 of 7 groups (death/status/text/allegiances) are now COMPLETE verbatim listings — /help death now prints retail's real 8-line text, byte-exact including the retail-authentic trailing space on the @day line. The other 3 (channels/chatting/commands) delegate part or all of their detail text to ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290, which builds its output from BN-mislabeled data fragments around a live channel-name lookup — genuinely not decodable with confidence; each keeps its own verbatim summary line plus an explicit UNVERIFIED note citing the address, never a fabricated meta-message. 7 of the ~35 channel one-liners are also now verbatim (a/guild/gu, general/cg, trade/ct, lfg/clfg, roleplay/crp, society/soc, olthoi/o), each including retail's own "Also: @alias" text — this is also the alignment/spacing fix, since the fabricated summaries never matched retail's exact wording. Register row AP-184 filed; ISSUES.md #364 tracks the remaining 3-group gap. RetailCommandHelpTableTests.cs (new) pins every complete listing and the partial/UNVERIFIED shape byte-exact.
6 Resize is still buggy: missing diagonal (corner) cursor feedback, and the window cannot grow in the Y axis when dragging from the bottom-right corner. FIXED at CH6a (this commit). The 4 corner grips (0x1000069B/D/F, 0x100006A1) decode retail's exact per-grip BorderLocation bools (0x2A-0x2D) and UiRoot gives a directly-hit grip's own edges priority, so CursorFeedbackController's existing (already retail-pinned) KindForResize now actually receives a diagonal edge combination when hovering a corner — no new cursor ids were invented; RetailCursorCatalog's pinned Type-9 cursor ids already matched the DAT exactly. The corner-grow gap traced to the same wrong-LayoutDesc root cause as item I: mounting through the universal nine-slice wrapper + a 490px content crop against 0x21000006's own (irrelevant) constraints created an indirection prone to stale-baseline growth bugs; 0x2100006F's root IS the complete retail chrome, so CH6a mounts it with RetailWindowChrome.Imported (no wrapper, no crop, frame==content) using the DAT's real minH=100/maxH=2000/minW=300/maxW=2000, collapsing the whole class. ChatLayoutConformanceTests.MountedChatWindow_BottomRightGrip_GrowsBothAxes_NotOnlyShrinks exercises the exact reported gesture end-to-end.

Evidence for items 2, 4, and 5: this commit's diff + tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandHelpTableTests.cs (new) + the deletion of tests/AcDream.App.Tests/UI/PortalWaitNoticeControllerTests.cs (its subject class no longer exists) + PortalTunnelAssetTests.cs (unchanged; verified by inspection — its CreateRequired call never passed the removed displayNoticeLifetime parameter, so it needed no update). Item 1's evidence is negative: no code changed in the jump branch logic itself, only diagnostics; the two temporary probes and the jump-probe headless bot policy are the round's deliverable for item 1, pending either a successful next-round repro or a character created on probeaccount2. Full Release suite green (see the commit message for the exact count).

Item 6's evidence landed in a LATER commit, the CH6a slice itself (see the ledger row above): src/AcDream.App/UI/UiResizeGrip.cs (new), src/AcDream.App/UI/Layout/DatWidgetFactory.cs (Type-9 factory case), src/AcDream.App/UI/UiRoot.cs (grip-priority hit-testing in OnMouseDown / HoverResizeEdges), src/AcDream.App/UI/Layout/ChatWindowController.cs and src/AcDream.App/UI/RetailUiRuntime.cs (the 0x2100006F swap + Imported chrome mount), plus new/updated tests in tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs, 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): positionSpewBoxController.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 .\n" — retail's own literal; "" 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.

CH6a/b REJECT-review rework (2026-08-10)

Applied everything in docs/research/2026-08-10-ch6ab-review-findings.md under the session's hard constraints (no subagents, no client launches). The retail research itself held up under independent re-derivation (masks, Alt+1..4, pure-mirror indicators all CONFIRMED); the rework was entirely against the FIX list, not the research.

  • BLOCKER 1 (CH6a — invisible borders). UiResizeGrip (src/AcDream.App/UI/UiResizeGrip.cs) gained a second constructor carrying its ElementInfo/sprite-resolve pair and an OnDraw override that stamps the active DirectState sprite exactly like UiDatElement — a synthetic (parameterless) grip still draws nothing, matching every existing resize-drag unit test. DatWidgetFactory.BuildResizeGrip now threads resolve through. SpriteFile exposes the raw dat sprite id for conformance tests without needing a live GL context; ChatLayoutConformanceTests .MountedChatWindow_LiveGrip_ResolvesNonZeroSprite pins all seven live grips against the committed chat_2100006f.json fixture.
  • SHOULD-FIX 2 (CH6b — window-id model). ChatWindowState gained BroadcastTargetWindow (a sentinel distinct from every real window id 0-4) so ShouldDisplay's explicit-addressing branch can no longer coincide with the broadcast check for the main window (id 0). SetFilter's main-window no-op is dropped — the main window's filter is now genuinely settable, matching retail's options-page-driven main filter. ChatWindowController.Bind gained a ChatWindowState windowFilters parameter (the same canonical RuntimeCommunicationState.ChatWindows instance the floating windows already read) and GetTranscriptLines now builds a real accept predicate from it instead of accept: null — verified safe: 0x1A (ClientLocal) never reaches ChatLog in the first place (RuntimeCommunicationState.AddText routes it to the SpewBox and returns before Chat.OnSystemMessage), so excluding it from the main filter has zero effect on WeenieError/refusal text. Every production gameplay chat type (Speech/Tell/Social/Fellowship/Allegiance/Turbine rooms) stays within the default filter's covered bits. Register row AP-189 documents the resulting shallower shared-log scrollback depth (SHOULD-FIX 5, below) as the one behavioral residual.
  • SHOULD-FIX 3 (CH6b — indicator self-toggle). UiButton gained SuppressSelfToggle, checked alongside ToggleBehavior at the press-release toggle site; ChatWindowController.Bind sets it on all four chat-window indicator buttons (which carry DAT property 0x0B = true but have no retail click handler). New UiButtonTests .SuppressSelfToggle_PressReleaseDoesNotFlipSelected and an extended MountedChatWindow_IndicatorButtons_ImportVisibleAndInert (now presses and releases the real fixture's indicator, asserting Selected unchanged) replace the "wrong level" OnClick == null check.
  • SHOULD-FIX 4 (CH6b — 0x2100005B never dumped). The floaty layout DAT dir at %USERPROFILE%\Documents\Asheron's Call\ (the default ConformanceDats.ResolveDatDir()/RetailLayoutFixtureGenerator path) IS reachable in this environment — chat_floaty_2100005b.json (171 KB, 5,473 lines) was generated for real and committed; RetailLayoutFixtureGenerator.Layouts gained the permanent entry for future regenerations. All three flagged untested assumptions in FloatingChatWindowController turned out CORRECT against the real fixture — no controller code changed: 0x10000016 is Type 12 with property 0x16 (Editable) = true, resolving to UiField; 0x100004D9 is Type 12 with NO 0x16, resolving to UiText; 0x1000052A is Type 1, resolving to UiButton. One extra finding beyond the three named assumptions: unlike the main window (whose plain top strip is a Type-2 Dragbar move handle because the main window has no title bar), the floaty window's move handle is its own title bar group (0x10000529, Type 2) — so ALL EIGHT of the floaty's border/corner elements are live Type-9 grips, not seven; a floaty window resizes from every edge and corner. New FloatingChatLayoutConformanceTests.cs pins the resolved widget types and extends BLOCKER 1's sprite-non-zero guard to all eight floaty grips.
  • SHOULD-FIX 5 (register row). AP-189 files the shared-500-entry/ 200-line-tail vs. retail's per-window 10,000-line log scrollback-depth divergence — the accumulate-while-closed and independent-per-window- scroll BEHAVIORS both reproduce correctly; only the numeric DEPTH ceiling differs.
  • NITs 1-5. N1: documented the filter-persistence-only-on-/saveautoui asymmetry on RetailUiRuntime.SaveChatWindowFilters, deferred to CH6e. N2: documented why ChatWindows.ResetToDefaults() only runs in RuntimeCommunicationState.Dispose (full teardown), not on reconnect — deliberate, matches window-geometry persistence. N3: replaced the §1.3 UNVERIFIED modifier-mapping hedge and the "0x00000002 = shift" mislabel with retail-default.keymap.txt's own MetaKeys legend (Shift=0x1, Ctrl=0x2, Alt=0x4, Win=0x8) — no code change, KeyBindings already had ModifierMask.Alt right. N4: fixed the false "ONLY function that branches on idMessage == 1" superlative in §1.4 and ChatWindowController.SetIndicatorOpen's doc (gmFloatyChatUI::ListenToElementMessage @0x004CE330 also does); the substantive "no case for the indicator ids" claim stands. N5: moved WrapText/WrapSingleLine off ChatWindowController onto ChatTranscriptRenderer (closing the circular dependency where BuildLines called back into one of its own two consumers); updated the two unrelated external callers (IndicatorDetailText.cs, ItemAppraisalReport.cs) and moved the WrapText tests into the new ChatTranscriptRendererTests.cs.

Full Release suite: 12,420 passed / 4 skipped / 0 failed (baseline 12,392/4/0 at 22020ef2; net +28 tests, all new coverage from this rework, zero regressions).