# Campaign CH — chat & interface-text retail parity **Status:** ACTIVE 2026-08-09 — 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 this commit) — 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`. **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. ## 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 | — | — | — | — | | CH5 closeout | — | — | — | — | ### 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.