acdream/docs/plans/2026-08-09-chat-parity-campaign.md
Erik 724ef2d389 fix(chat): CH4 review fixes — allegiance ownership guard, house-abandon confirmation
Blocker 1: an unrecognized "@allegiance <sub>" subcommand escaped
TryMatchAllegiance (which only claimed "info"/"hometown") and fell through
the unregistered-tag channel fallback, broadcasting the raw subcommand
text to the Allegiance chat channel (0x02000000). Retail's own
DoAllegiance never reaches DoChannelCommand for an unrecognized
subcommand — it claims the whole verb and prints its own client-local
refusal. TryMatchAllegiance now claims "allegiance"/"all" unconditionally
and shows retail's "Please see @help Allegiance..." text; ChatCommandRouter
also gained a blanket RetailClientCommandCatalog.KnownVerbs ownership
guard in TryDispatchChannelFallback as defense in depth.

Blocker 2: "@house abandon" sent 0x021F immediately with no confirmation.
Retail runs a real two-stage dialog before Event_AbandonHouse(); ported
both verbatim strings and chained two ShowConfirmation calls.

Should-fixes: a bare unregistered tag with no text now passes through
silently instead of showing a refusal that belongs to a different retail
function; @join/@leave update RuntimeCharacterOptionsState locally (new
SetOptionBit) before the wire push so the Turbine membership gate stops
refusing a just-joined room; @permit accepts multi-word names; @clist/
@on/@off validate shape only and raise WeenieError 0x422 for an unknown
tag; @mr/@pr help text is now the verbatim retail strings; corrected
issue #360, register row TS-68, the campaign doc's B.7 note, and a stale
RetailChannelTagTable comment; filed issue #363 + register row AP-183 for
the deferred error-typing debt.

Nits: fixed TryMatchHouse's stale doc comment, the AP-182/@title "stores
the value" comments (the binding is a no-op), IsUnregisteredFallbackTag's
olthoi false-positive, added /g and /rp binding-level conformance pins,
made @index ignore extra arguments, and noted the six removed invented
verbs in ISSUES.md.

Suite: 12,216 passed / 4 skipped / 0 failed (Release), up from CH4's
12,190/4/0 — net +26 tests, no removals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:59:35 +02:00

410 lines
24 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:** 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`. CH4 (command
registry completion) CODE-COMPLETE — 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. Pending the in-client user
gate (command spot-checks per the campaign's overall gate list).
**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 |
|---|---|---|---|---|
| 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,190 passed / 4 skipped / 0 failed | REJECT; fixed `<sha>` | pending |
| CH5 closeout | — | — | — | — |
### 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).
### 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.