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>
40 KiB
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:
- Exact chat colors. The 2026-06-16 cdb session pinned the retail
RGBAColorconstants (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 conformsChatWindowController.RetailChatColorto it. - 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.
- 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.
- Complete
/and@command registry. Every command the retail client parses, audited againstRetailClientCommandCatalog(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;
RetailChatColorcorrected; 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
0x21000006for retail's0x2100006Fand delete the downstream compensations (hand-cropped 490px ContentWidth, dropped 800px resize bar, the 9px transcript patch, orphan-sibling pruning); teachLayoutImporterelement 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 pergmGamePlayUI::SetupChildren @0x004E9EC0(ids 0x10000505/0x1000050E/0x1000050F/0x10000510); keybind actionsToggleFloatingChatWindow1..4(retail defaults atretail-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 thewindowId == m_eWindowID OR (windowId==0 && TypeIsActive)display rule. - CH6c — opacity. Implement
UiRenderContext.AlphaModconsumption (whole-composited-window alpha perChatInterface::SetOpacity @0x004F3120); the two GLOBAL retail options0x10000080(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 array0x1000008Cis stored by ACE as an opaque byte[] it never parses, so the wire format is its own deferred slice.
- CH6a — correct main-window import + resize. Swap the wrong
LayoutDesc
Gates
- Per slice:
dotnet buildgreen, 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):
:/;emote-prefix rewrite (OnChatCommandcases 0x0B/0x0C) — both prefixes rewrite identically to@emote <rest>.- 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". @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.- The 22 unregistered
ChannelSystem::GetChannelIDfallback tags (av,admin,sentinel,celestialhand, …) now broadcast for real via a newRetailChannelTagTable+SendRawChannelCmdbypass path, reusing the existingBuildChatChannelwire builder — no new opcode needed.
B. Binding corrections:
/g//group//party→ Fellowship (0x800), not General — the live correctness bug the doc flagged as Tier-1 #1./rp→ reply alias (confirmed by retail's own help text, "You may also use @r or @rp"), not Roleplay. Roleplay keepsroleplay/crp; the non-retail/roleinvention is deleted./allegiance//allare nowRetailClientCommandCatalog's allegiance MANAGEMENT command (a newTryMatchAllegiancedispatcher), not a channel verb. The channel-send verbs staya/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 Bobsent "boot Bob" to allegiance chat). Retail's ownDoAllegianceclaims the ENTIRE verb unconditionally: an unrecognized subcommand prints "Please see @help Allegiance for more information on how to use this command." locally and never reachesDoChannelCommandor the server.TryMatchAllegiancenow matches this exactly — it always returns ownership forallegiance/all, showing retail's refusal text for any subcommand beyond the 2 ported ones./house//houno longer swallows unrecognized subcommands with a local usage error —TryMatchHousereturns no match for anything beyondrecall/re/mansion_recall/alleg_recall/ma/abandon, letting it reach ACE.@mr/@prpinned as permanently non-executable (MrPr_AreNeverExecutabletest) — retail registers them with a NULL function pointer; they must never resolve inRetailClientCommandCatalogorChatInputParser.
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:
- The false "ACE doesn't run a TurbineChat server" claim retracted in
docs/ISSUES.md(#19) anddocs/plans/2026-04-11-roadmap.md(I.6, ×2). TurbineChatMembershipGate(new,AcDream.Runtime.Gameplay) ports retail'sSendTurbineChat @0x0057db10local pre-send gate — Turbine off/room-0 →"Turbine chat is not available."; Hear-option off →0x0551 YouAreNotListeningTo_Channel— both raised throughRuntimeCommunicationState.AddText. Wired into BOTHLiveSessionCommandRouter(graphical) andDirectGameRuntimeCommandAdapter(headless) so the two hosts can't diverge.RuntimeCharacterState.IsOlthoiPlayeradded (heritage-gated, not an option) for the Olthoi room.SetSingleCharacterOption (0x0005)implemented end to end (codec,WorldSession.SendSetSingleCharacterOption,IRuntimeCharacterCommands. SetSingleOption, both adapters,LiveSessionCommandRouterregistration) and wired to the 5 Settings Chat toggles viaRuntimeSettingsController. SaveChat(publishes only the CHANGED bits) through a hoistedLiveSessionCommandSurfacenow shared withRuntimeSettingsTargets. No 6th (Allegiance) toggle was added —ChatSettingshas never had one and retail's own Settings UI was not confirmed to have one either; flagged for the user rather than guessed.ChatSettingsseeded from server truth —RuntimeSettingsController. SyncChatFromServerOptionsreseeds both the persisted snapshot and any live unsaved draft fromCharacterOptions2whenever a fresh PlayerDescription lands (LiveCharacterSessionBindings. OnCharacterOptionsChanged, new optional hook).- Self-echo double-print fixed —
LiveSessionCommandRouter. RouteLegacyChannelnow consultsChatChannelInfo.Legacy(...). IsSelfEchoChannel(); Fellow/Vassals/Patron/Monarch/CoVassals skip the local echo (server resends with""sender), AllegianceBroadcast/Say/Tell keep it. ExistingTellAndLegacyChannel_PreserveOutboundAndEchoPolicytest corrected to the fixed (single-print) expectation. - TurbineChat ack HResult surfaced —
LiveSessionEventRouter. RouteTurbineChatnow switches onPayload.Response{HResult}; nonzero surfaces as a system chat line, zero (the common case) stays silent matching retail. /aroutes to Turbine unconditionally —ChatChannelKind.Allegianceis exhaustively dispatched to the Turbine pipeline (never falls through to legacy); a newChatChannelKind.AllegianceBroadcast+/abverb owns the legacy0x02000000path retail's own@abverb uses./allegiancebroadcastwas deliberately NOT added — the retail command registry (docs/research/2026-08-09-chat-retail-command-registry.md) only hasab, not that long form.- 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 bySetSingleCharacterOption, the message step 3 actually needed. - Register + memory — AP-181 (no client-side chat spam throttle) and
UN-9 (an incidentally-discovered, unexplained one-byte
CharacterOptions1.Defaultmismatch vs ACE's own literal — not investigated further, flagged for a future pass) filed indocs/architecture/retail-divergence-register.md.claude-memory/project_chat_pipeline.mdline ~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
GameActionChatChannelhandler iteratesplayer.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 returnstruefor0x02000000too;RouteLegacyChannelskips the local echo for it. - Item 7 is now WRONG.
/ais NOT unconditionally Turbine. Retail's base binding keeps it on the legacyAllegianceBroadcastbitflag untilStartupTurbineChatSystemsuccessfully starts Turbine chat and rebinds it (research doc §4.3).LiveSessionCommandRouter.RouteChatandDirectGameRuntimeCommandAdapter.TrySendChannelnow special-caseTurbineChatState.Enabled == falseto fall back to the legacy send;Enabled == truewithAllegianceRoom == 0still correctly refuses locally ("Turbine chat is not available."). - Item 9's UN-9 filing was a phantom. ACE's own
CharacterOptions1.cs:47OR-sum is0x50C4A54A(confirmed by its own inline comment,// 1355064650), identical to acdream'sPlayerDescriptionParser.cs:217— there was never a divergence. The wrong literal0x50C48D4Aexisted only indocs/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 (IsMessageSafesilent-drop, THENIsMessageSpam) and no longer misattributes either toRouteLegacyChannel.
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).