Item 2: retail's portal-space "In Portal Space..." notice is the SpewBox (ECM_UI::SendNotice_DisplayStringInfo(0x1A,...) -> AddTextToScroll(str, 0x1A, 1, 0), hardcoded to the SpewBox per the decomp), not a dedicated centered overlay. PortalWaitNoticeController and its lease are deleted; PortalTunnelPresentation's per-rotation-segment cadence now writes straight into RuntimeCommunicationState.AddText(ClientLocal) -- the SpewBox's own dedupe-at-index-0 handles the repetition exactly as retail's does. Register row AP-184 records the surface fix and the AP-178 scope extension. Items 4+5: /help text was partially fabricated -- the user caught the "/help death" meta-message. Generalized tools/pdb-extract/sweep_weenie_strings.py to decode narrow PStringBase<char> literals (the ClientCommunicationSystem::Help* family's shape) alongside its original UTF-16LE support, then swept every HelpXxxGroup function's exact byte extent against the PDB-paired acclient.exe. 4 of 7 group topics (death/status/text/allegiances) are now complete verbatim listings; the other 3 (channels/chatting/commands) keep an honest UNVERIFIED note citing HelpStupidChannelHack @0x0056f290 (a genuinely undecodable BN-mislabeled-fragment mechanism) instead of the old fabricated sentinel. 7 of ~35 channel one-liners are also now verbatim. ISSUES.md #364 tracks the remainder; RetailCommandHelpTableTests.cs pins every result byte-exact. Item 1: jump-in-air refusal still silent live is NOT reproduced and NOT speculatively fixed. Exhaustive static re-audit found the mechanism correct by construction (single-writer OnWalkable, exactly-once-per-frame Update()/Capture(), no interfering edge-history resets). A live headless repro (new jump-probe bot policy, real ACE connect) was blocked -- probeaccount2 has no character, and the graphical client already owned testaccount this session so the task's own fallback rule forbade using it. Two temporary probes are left behind ACDREAM_PROBE_JUMP=1 (blocked entirely in Headless by the existing multi-session static-state guard -- graphical-only for the next round). Item 3 confirmed fixed, no regression. Item 6 (resize: no diagonal cursors, cannot grow Y from bottom-right) folded into CH6a's existing scope. Full Release suite: 12,267 passed / 4 skipped / 0 failed (up from 12,221/4/0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
555 lines
40 KiB
Markdown
555 lines
40 KiB
Markdown
# 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 A–G 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. Status stays CODE-COMPLETE pending
|
||
the next user gate round (still needed for item 1, CH6, 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 0x2A–0x2D); 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).
|
||
- **CH6b — floating windows 1–4.** 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.
|
||
- **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 |
|
||
|---|---|---|---|---|
|
||
| R1–R4 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 A–G fixed this commit) | — | items A–G user-gate round 1 fixed; ten findings total, see "User gate — round 1" below |
|
||
| CH6 chat-window shell parity | not started | — | — | filed 2026-08-09 at user gate round 1; research first |
|
||
| 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) |
|
||
|
||
### 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 A–G 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. | **NEW SLICE CH6** — see the slice list above. Retail has real multi-window chat with per-window type filters; this needs research before a fix, not a quick patch. |
|
||
| I | Resizing the chat window only works from one corner, not every corner. | **NEW SLICE CH6** — bundled with H and J as "all-corner resize." |
|
||
| J | The chat window has transparency issues / visual artifacts, and the user wants a transparency setting eventually. | **NEW SLICE CH6** — bundled as "opacity"; the future user-facing transparency setting is noted in the slice's own scope, not promised for CH6's first cut. |
|
||
|
||
Findings A–G'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; A–G 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. | **FOLDED into CH6a's spec** (not fixed this SHA — CH6a itself has not started; see the slice list above, now carrying these two symptoms alongside the pre-existing resize-mask/LayoutDesc-import scope). |
|
||
|
||
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).
|