# Campaign CT — complete chat parity (system + GUI) **Status:** Groups A, B, C and D COMPLETE 2026-08-21, each user-gated. CT-B4 landed 2026-08-21 after the research block turned out to rest on a wrong premise (see the slice). One item deliberately not shipped: CT-B3 (word filtering — dropped by user direction, register row CT-2). **Carried forward:** ~~multi-frame state media~~ **DONE 2026-08-21.** The importer now keeps the whole authored sequence and `UiMediaSequence` plays it. Measuring the real data (`LayoutDump --media 0x1000048C`) corrected the behaviour as well as enabling it: the indicator blinks three times over three seconds and then hands off to `Ghosted`, hiding itself. Retail's is a transient attention-flash, not a badge that stays lit until you scroll down. Register rows CT-3, CT-4. **Goal, set by the user 2026-08-21: complete retail parity for the chat system AND the chat GUI.** Not "fix the green name" — that was the symptom that started the review. The bar is that a retail player sitting down in front of acdream's chat window finds nothing missing and nothing behaving differently. Campaign CH (2026-08-09, closed user-accepted) landed colours, side channels, the 152-verb command registry, the window shell and verbatim `/help`. CT is the pass that closes what CH did not reach. Research notes (all 2026-08-21): `chat-texttag-model.md`, `chat-tagged-name-composition.md`, `chat-tag-click-dispatch.md`, `retail-chat-window-ui.md`, `acdream-text-stack-audit.md`, `acdream-chat-ui-audit.md`. ## Definition of done 1. Every retail chat behaviour is either implemented, or has a divergence- register row saying why not. 2. Every user-visible chat surface has a test that would catch its regression. 3. The chat digest and `docs/ISSUES.md` describe reality (both are stale today). ## What the review established ### The green clickable name is a TEXT-STACK gap, not a chat gap The client sprintfs literal markup into the line — `{name}<\Tell> says, "{text}"`, closing marker a literal backslash — and `UIElement_Text::InqGlyphs @0x00468EA0` parses the brackets while appending, calling `TextTagFactory::MakeTag @0x00478480`. Tags attach **per glyph**; a "run" is emergent (adjacent glyphs with equal tag pointers). A glyph takes the tag colour (property `0x1D`) only when a tag is open AND its `m_type == 0x10000001`, else the line colour (`0x1B`). Only senders with a GUID in `0x50000001..0x6FFFFFFF` are tagged. Colour **measured** from the installed dats (`LayoutDump --colors`), chat `0x2100006F` / transcript `0x10000011`: `P0x1B` = RGB(204,204,204), `P0x1D` = **RGB(0,178,0)**. The tag colour is per-ELEMENT and authored, while the line colour on that same element comes from the runtime chat table — filing "tag green" into the LogTextType table would put it in the wrong place. Click: `UIElement_Text::MouseUp @0x004694F0` → `DeterminePositionFromXY @0x004688F0` → `GlyphList::InqGlyph @0x00473430` → virtual `HandleClick` at tag-vtable `+0x14` → `gmMainChatUI::RecvNotice_TextTag_IIDStringClick @0x004CCE10` → `ChatInterface::StartTell @0x004F41F0`, which writes `"@tell {Name}, "`, takes focus, and shows the entry bar. Clicking a name always opens a TELL — fellowship, allegiance, patron/vassal and named-channel lines all embed the same markup. No hover effect. ### Our side is closer than feared `UiText` **already** draws multi-coloured runs (`TextRun`/`RunsProvider`, used by the character stat panel); it is gated to `OneLine == true`. The draw path needs no renderer work — arbitrary pen X, substring measurement — and `UiText.HitChar` already resolves a click to (line, column). The blocker is that `ChatVM.RecentLinesDetailed()` drops `Sender`/`SenderGuid` one step before the renderer, though `ChatEntry` carries them the whole way. ### The command registry is already at parity All 13 verbs the CH3 research note lists as MISSING were closed by CH4 and verified present 2026-08-21 (`cg`, `soc`, `o`, `co-vassals`, `fellows`, `group`, `party`, `vassal`, `ab`, `guild`, `ct`, `clfg`, `crp`). `/g` correctly resolves to Fellowship, confirmed against the live retail client. That note's "acdream status" columns are stale and now carry a correction banner. ## Slices ### Group A — the tagged-text capability (strict chain, A1→A5) Nothing is user-visible until A4. - **CT-A1** Multi-line text elements carry coloured runs. Additive; the ~50 files using `Line` are untouched. No behaviour change. - **CT-A2** Parse the tag markup into runs with a tag payload, including retail's rule that an unparseable bracket closes the open tag. Pure, unit- testable, no UI. - **CT-A3** Stop flattening: carry sender name + guid through `ChatVM` into spans, and compose retail's markup in the speech handlers behind the player-GUID-range gate. - **CT-A4** Apply the authored `0x1D` tag colour when a tag is open and its type matches. **Names turn green.** - **CT-A5** Sub-line hit-testing and `StartTell`. **Names become clickable.** ### Group B — chat SYSTEM behaviours - **CT-B1** Bound the transcript: 10,000 chars, trim to ~7,500 preferring a newline boundary (`TruncateChatLog @0x004F4290`). Today it grows for the life of the session — a slow leak, not only a fidelity gap. - **CT-B2** Text-replacement macros: typing `/r `, `/t `, `/tell ` rewrites the input to `@tell {LastTeller}, ` on the space keypress (`HandleTextReplacements @0x004F50D0`). The commands already work; the visible expansion does not exist. - **CT-B3** ~~`FilterLanguage` word filtering~~ — **DROPPED by user direction (2026-08-21): "I do not want any censoring."** acdream keeps the option itself, which still stores and ships its bit to the server exactly as retail does, but performs no client-side substitution. Registered as CT-2 rather than left as an implicit gap, since it IS a knowing departure from retail (`PlayerModule::FilterLanguage` + `TabooTableAdaptor::CheckCensorsW @0x00682A30` inside `AddTextToScroll`). Worth keeping on record, because the attempt established two things that would otherwise be rediscovered if this is ever revisited: 1. The table's dat id is not readable from the decomp — `CheckCensorsW` reaches it through `DBObj::GetByEnum` with the arguments elided by Binary Ninja. The portal master enum map (`0x25000000`) has 22 categories, with category 3 (`0x0E010001`, `0x0E010002`) and the single-entry categories 8 and 11 the plausible candidates. 2. Chorizite.DatReaderWriter declares a `TabooTable` type but does NOT decode it — only `DBObjType` and `HeaderFlags`. The format would have to be decoded here first. And the matching is an algorithm, not a word list: `TabooTable::CreateCheckString @0x00681570` normalises a candidate before `StringMatchesFilter @0x00681600` compares it, which is how retail catches obfuscated spellings. - **CT-B4** ~~The plain-text session chat log~~ **DONE — and the premise was wrong.** There is no automatic session log to have a path for. Retail's `@log` is a COMMAND: `ClientCommunicationSystem::DoSetOutput @0x0057E4F0` takes a filename, `StartCopyOutputToFile @0x0057C8A0` does the `fopen(name, "a+")`, and running it again with no argument closes it. So "path and rotation UNKNOWN" was asking a question the design does not have: the player names the file, and there is no rotation because it appends forever. The path question that DOES exist — where a bare name lands — is answered by retail's own help text, which the CH4 help table already carried verbatim without anyone reading it: "a log file named Aclog.txt **in your Asheron's Call directory**". acdream cannot use the install directory (the launcher replaces it atomically on update), so a bare name lands in the client's own log directory. Rooted paths are honoured verbatim. Register row CT-5. Landed with it: the verb registered in the catalog (it had a help entry since CH4 but no catalog entry, so `/log` printed help and did nothing), retail's `.txt`-for-extensionless rule, all five reply strings byte-decoded from the paired binary, and the writer attached at OPEN so only text after the command is copied. The line logged is the composed display line with the shared timestamp, because retail's `fprintf` sits inside `AddTextToScroll` — downstream of composition, upstream of glyph layout. ### Group C — chat GUI - **CT-C1** Auto-scroll vs unread: retail samples "was at bottom" BEFORE the line lands; if you had scrolled up it leaves you there and lights the unread indicator (`0x1000048C`), which scrolls to bottom and clears on click. - **CT-C2** Escape in the chat input is a complete no-op — `UiField` has no `Escape` case, and a focused field also suppresses the input dispatcher's fallback, so there is no clear, no defocus and no hotkey passthrough. - **CT-C3** Option-gated timestamp prefix (`%#H:%M:%S `, colour index `0x0C`, grey), gated on `PlayerModule::DisplayTimeStamps()`. - **CT-C4** Input-bar editing parity: clipboard and selection paths (Ctrl+C/X/V, shift-selection) work but are untested; `ToggleMaximize` and the floating-window Close button have zero coverage. **DEFERRED** — pure test coverage over behaviour the audit confirmed already works, so it changes nothing a user can see. Worth doing; not worth blocking the campaign on. ### Group D — hygiene - **CT-D1** ~~Delete the dead ImGui-era `ChatPanel`~~ **DONE.** Verified never constructed in `src/`, then removed with its three panel-only test files. `ChatVMCombatTests` was KEPT — three of its four tests are real `ChatVM` coverage; only the one `ChatPanel` render test went. - **CT-D2** ~~Reconcile the chat digest and `docs/ISSUES.md`~~ **DONE.** `docs/ISSUES.md` turned out to be ACCURATE already — #358 and #363 are recorded CLOSED there. Only the chat digest's "Open" section was stale, and it is corrected: genuinely open are #359, #360, #361, #366. The digest also gained a Campaign CT section and three new DO-NOT-RETRY rows. ## Research still owed before the affected slices - The tag-type roster behind `m_type == 0x10000001` — only "Tell" is confirmed; the full set lives in the DAT `EnumMapper` category `0x18`. Blocks nothing in Group A, but decides whether other tag shapes exist. - Whether retail's transcript supports text selection distinctly from the entry field (blocks CT-C4's scope). - Whether a chat-specific sound cue exists — a grep came back empty, which is weak evidence, not proof of absence. ## Deliberately NOT in scope Item links and the other three tag shapes (`DID`, `IID`, `IIDEnum`). They have no listener in the retail build we target, so porting them would be inventing behaviour. CT-A5's dispatch is generic, so they cost nothing to add later.