acdream/docs/plans/2026-08-09-chat-parity-campaign.md
Erik 22020ef2c4 feat(chat): Campaign CH slice CH6b — floating chat windows 1-4
Mounts retail's four floating chat windows as always-resident, born-hidden
children per gmGamePlayUI::SetupChildren @0x004E9EC0, all sharing LayoutDesc
0x2100005B (window ids 0x10000505/0x1000050E/0x1000050F/0x10000510). New
FloatingChatWindowController (AcDream.App/UI/Layout) binds each window's own
widget tree — built fresh per instance from one shared imported ElementInfo
— reusing ChatWindowController's word-wrap + retail color-carry algorithm via
the extracted ChatTranscriptRenderer instead of duplicating it. A floaty
window has no talk-focus menu (research doc §2.2), so its entry field always
sends on Say; the mismatch against retail's possible shared-channel behavior
is UNVERIFIED and filed as #369/AP-188.

Runtime owns the per-window filter/open state: ChatWindowState (new,
AcDream.Core.Chat) seeds retail's exact PostInit defaults per window
(window 1 0x0000101C Speech/Tell/DirectSend/Emote, window 2 0x00040C00
Social/SocialSend/Allegiance, window 3 0x00080000 Fellowship, window 4
0x78000000 Turbine General/Trade/LFG/Roleplay) and implements the full
ShouldDisplay(windowId, targetWindowId, logTextType) display predicate from
ChatInterface::RecvNotice_DisplayFinalStringInfo @0x004F4640. It lives on
RuntimeCommunicationState.ChatWindows so every host borrows the same
instance. The main window's filter (0xFBFFFFFF, "no user filter") never
actually gates anything because its own explicit-address branch already
covers every broadcast line — that's why UpdateFromPlayerModule early-returns
for window 0 in retail, ported here by construction rather than a special
case.

Keybind wiring: InputAction.ToggleFloatingChatWindow1..4 and their
KeyBindings.RetailDefaults() chords already existed since Phase K.1c
(unwired until now). The MetaKeys table confirms retail's default is Alt+1
through Alt+4 (index 3 = bit 0x00000004, cross-checked against the same
file's Alt+A/D strafe and Alt+Enter/Tab/F4 rows). Routes through
GameplayInputCommandController -> RetainedGameplayWindowCommands ->
RetailUiRuntime.ToggleFloatingChatWindow -> the generic UiHost.ToggleWindow,
whose visibility-change event is the single chokepoint that syncs
ChatWindowState.SetOpen and mirrors the main window's 1-4 indicator button
regardless of what changed a window's visibility (keybind, close button, or
a restored layout).

A direct decomp read of gmMainChatUI::ListenToElementMessage @0x004CDA80 —
the only function in the whole binary that branches on a click message —
settles what the research doc had left as a hedge: it handles exactly
0x1000046f (max/min) and the talk-focus menu's selection message, with NO
case for 0x10000522-0x10000525. The four indicator buttons are PURE
one-directional mirrors in retail; clicking them does nothing.
ChatWindowController.SetIndicatorOpen ports this with no OnClick at all.
Corrected research doc §1.4 accordingly.

Persistence is local-only (register row AP-187; the retail 0x1000008C
GameplayOptions wire remains deferred to CH6f): window geometry and
open/visible state ride the existing generic RetailWindowLayoutPersistence
path for free once each window registers under its own WindowNames entry;
the four filter masks get a dedicated ChatSettings round-trip
(ChatWindow1Filter..ChatWindow4Filter, defaulting to the retail PostInit
constants) loaded at mount and saved alongside SaveLayout().

Tests: ChatWindowStateTests (defaults, TypeIsActive, the full display-rule
matrix, toggle/reset, revision counter), FloatingChatWindowControllerTests
(bind smoke tests against a synthetic 0x2100005B tree, per-window filter
routing, filter-change cache invalidation, fixed-Say submit), new
ChatWindowController.SetIndicatorOpen tests (Highlight/Normal state,
cross-window isolation, range validation), GameplayInputCommandController
routing for the four toggle actions, and a SettingsStore filter round-trip.
Full Release suite: 12,392 passed / 4 skipped / 0 failed.

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

724 lines
59 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
retail** — `ChatWindowController.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` | 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) |
| CH6b floating windows 14 | (this commit) | 12,392 passed / 4 skipped / 0 failed | pending (no subagent review pass this session — implementer-only, per this session's HARD CONSTRAINT of no subagents) | pending — no client launches this session (hard constraint); needs the next connected round for keybind/mirror/filter visual confirmation |
| 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:**
5. `/g`/`/group`/`/party` → Fellowship (0x800), not General — the live
correctness bug the doc flagged as Tier-1 #1.
6. `/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.
7. `/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.**
8. `/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.
9. `@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 truth** — `RuntimeSettingsController.
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 fixed** — `LiveSessionCommandRouter.
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 surfaced** — `LiveSessionEventRouter.
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 unconditionally** — `ChatChannelKind.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 resolved** — `SocialActions.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 CH6c** — `UiRenderContext.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): **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.