acdream/docs/plans/2026-08-21-chat-text-tag-campaign.md
Erik 2f97052e4f docs: CT-B3 dropped by user direction — no chat word filtering
"I do not want any censoring." Recording it as a KNOWING divergence (register
row CT-2) rather than leaving it as an implicit gap, because retail does filter
and someone reading the code later would otherwise read our silence as an
oversight.

The option itself stays: FilterLanguage still stores and ships its bit to the
server exactly as retail does, so anything the SERVER gates on it behaves
normally. What acdream does not do is substitute words in the transcript.

The attempt before the decision is kept in the plan, since it establishes two
things that would otherwise be rediscovered:

  - The taboo table's dat id is not readable from the decomp —
    TabooTableAdaptor::CheckCensorsW @0x00682A30 reaches it through
    DBObj::GetByEnum with the arguments elided by Binary Ninja. Measured the
    portal master enum map (0x25000000) instead: 22 categories, with category 3
    (0x0E010001 / 0x0E010002) and single-entry categories 8 and 11 as the
    plausible candidates.
  - Chorizite.DatReaderWriter declares a TabooTable DBObj type but does not
    decode it: only DBObjType and HeaderFlags. The format would need decoding
    here first.

Also sharpened CT-B4's status to research-blocked — the chat log file's path
and rotation are not in the decomp either.

Group B therefore ships B1 and B2 and is complete as scoped.

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

8.3 KiB

Campaign CT — complete chat parity (system + GUI)

Status: PROPOSED (2026-08-21). Not started.

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 — <Tell:IIDString:{iid}:{name}>{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 @0x004694F0DeterminePositionFromXY @0x004688F0GlyphList::InqGlyph @0x00473430 → virtual HandleClick at tag-vtable +0x14gmMainChatUI::RecvNotice_TextTag_IIDStringClick @0x004CCE10ChatInterface::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 filteringDROPPED 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 (ClientSystem::s_pLogFile). We write none. Research-blocked: path and rotation are UNKNOWN and are not in the decomp — needs a live check or a dat/filesystem probe.

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.

Group D — hygiene

  • CT-D1 Delete the dead ImGui-era ChatPanel (never constructed since Campaign V deleted AcDream.UI.ImGui), and its three test files, which currently make the real input surface look better covered than it is.
  • CT-D2 Reconcile the chat digest and docs/ISSUES.md: #358, #362, #363, #367, #372, #379, #380, #382 are DONE in code but still listed open. #359, #360, #361, #366 remain genuinely open.

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).
  • The chat log file's path and rotation (blocks CT-B4).
  • 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.