acdream/docs/plans/2026-08-09-chat-parity-campaign.md
Erik 813cc51f9a docs: CH4 closed at 5d247d55; Campaign CH user test script
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 22:26:15 +02:00

24 KiB
Raw Blame History

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 724ef2d3; re-review APPROVE-WITH-FIXES; closed 5d247d55 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:

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

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

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

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

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

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

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

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

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

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

CH3 closeout handoff (2026-08-09)

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

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

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

CH3 Opus review fixes (2026-08-09)

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

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

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

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