docs: Campaign CT rescoped to complete chat parity, system + GUI

The user set the goal: complete retail parity for the chat system AND the chat
GUI, not just the green clickable name that started the review. Plan restated
around that bar, with a definition of done — every retail behaviour either
implemented or carrying a divergence-register row, every user-visible surface
covered by a test, and the digest/ISSUES describing reality.

Slices regrouped into A (the tagged-text capability, a strict chain where
nothing is visible until A4), B (system behaviours), C (GUI), D (hygiene), plus
the research still owed before specific slices and what is deliberately out of
scope.

Four items joined the plan that the original six lanes did not own, because
they fell between lanes:
  - the /r, /t, /tell text-replacement macro (the commands work; retail's
    VISIBLE expansion to "@tell {LastTeller}, " does not exist)
  - FilterLanguage, which is a decorative toggle: we store it, ship the bit and
    show it in Options, and never actually filter anything
  - the plain-text session chat log retail writes and we do not
  - the option-gated timestamp prefix

Also corrects the CH3 command-registry research note. Its "acdream status"
columns are from before slice CH4 and list 13 verbs as MISSING that have all
since been added — cg, soc, o, co-vassals, fellows, group, party, vassal, ab,
guild, ct, clfg, crp — and its DIVERGENT row for /g is likewise stale: acdream
maps /g to Fellowship, matching retail, confirmed against the live client
today. The retail side of that document is still the authority; only the
columns describing us were wrong. They misled this session's investigation,
which is exactly why the banner says to verify against ChatInputParser.cs.

Nothing implemented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-21 07:13:55 +02:00
parent 663129c340
commit ba6c82af93
2 changed files with 125 additions and 93 deletions

View file

@ -1,126 +1,143 @@
# Campaign CT — chat text tags and chat-window parity
# Campaign CT — complete chat parity (system + GUI)
**Status:** PROPOSED (2026-08-21). Not started.
Retail renders a speaker's name inside a chat line in green, and clicking it
opens a tell to that person. acdream renders flat, uniformly coloured, inert
lines. Six parallel research lanes established why, and the answer is not a
chat bug — it is a **missing capability in the text stack**.
**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`.
## The mechanism, proven
## Definition of done
1. The CLIENT composes the markup. `Handle_Communication__HearSpeech
@0x005712A0` / `HearDirectSpeech @0x005715A0` sprintf a literal tag into the
plain chat line, of the shape
`<Tell:IIDString:{iid}:{name}>{name}<\Tell> says, "{text}"` — the closing
marker is a literal backslash. Only senders whose GUID is in AC1's player
range `0x50000001..0x6FFFFFFF` are tagged at all.
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).
2. `UIElement_Text::InqGlyphs @0x00468EA0` recognises the brackets while
appending and calls `TextTagFactory::MakeTag @0x00478480`. Any bracketed
text that fails to parse closes the open tag — `MakeTag` requires a `:` to
succeed, which is exactly what makes the bare closer a closer.
## What the review established
3. Tags attach **per glyph**. There is no run or span object anywhere: a
"tagged run" is emergent, re-derived by walking neighbouring glyphs whose
`m_tag` pointers are equal.
### The green clickable name is a TEXT-STACK gap, not a chat gap
4. Colour: a glyph takes the TAG colour (property `0x1D`) only when a tag is
open AND its `m_type == 0x10000001`; otherwise the ordinary line colour
(`0x1B`). Both are DAT-authored arrays on the element.
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.
5. **Measured** out of the installed dats (`LayoutDump --colors`), chat
`0x2100006F`, transcript `0x10000011`:
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.
P0x1B (line) [0x00] R=204 G=204 B=204
P0x1D (tag) [0x00] R= 0 G=178 B= 0 <- the green
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.
6. Click: `UIElement_Text::MouseUp @0x004694F0` → `DeterminePositionFromXY
@0x004688F0` (xy → glyph index) → `GlyphList::InqGlyph @0x00473430` → a
virtual `HandleClick` at tag-vtable `+0x14``SendNotice_TextTag_*Click`
`gmMainChatUI::RecvNotice_TextTag_IIDStringClick @0x004CCE10`
`ChatInterface::StartTell @0x004F41F0`, which writes `"@tell {Name}, "` into
the entry, takes keyboard focus, and shows the entry bar.
### Our side is closer than feared
Two facts that shape the port:
`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 tag colour is per-ELEMENT and authored**, while the line colour on the
same element comes from the runtime chat table. Filing "tag green" into the
LogTextType colour table would put it in the wrong place.
- **Clicking a name always opens a TELL**, everywhere. Fellowship, allegiance,
patron/vassal and named-channel lines all embed the same markup. Dispatch is
generic over four tag shapes, but only `IIDString` has a listener in this
build.
### The command registry is already at parity
Retail applies no hover effect to a tag, and the colour is a static per-glyph
bake at append time — not a render-time lookup.
## What we already have
The audit found more than expected. `UiText` **already** has a
`TextRun`/`RunsProvider` path that draws several differently-coloured runs on
one line (used today by the character stat panel) — it is simply gated to
`OneLine == true`, and the chat transcript is multi-line. The draw path needs
no renderer work at all: it already accepts an arbitrary pen X and can measure
substrings. `UiText.HitChar` already resolves a click to (line, column).
So the gap is narrower than "build a text tag system":
- the multi-line path cannot carry runs, and
- sender identity is **destroyed before it reaches the renderer**: `ChatEntry`
keeps `Sender`/`SenderGuid` all the way through `ChatLog`, and
`ChatVM.RecentLinesDetailed()` builds a `FormattedLine` that drops both.
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
**CT1 — runs on the multi-line text path.** Extend the existing `TextRun`
model to multi-line elements; per-line run lists; additive, so `Line` keeps
working and the ~50 files using it are untouched. No behaviour change.
### Group A — the tagged-text capability (strict chain, A1→A5)
**CT2 — markup parse.** Parse the tag markup into runs carrying a tag payload,
including retail's rule that an unparseable bracket closes the open tag. Pure
and unit-testable, no UI.
Nothing is user-visible until A4.
**CT3 — stop flattening, and emit the markup.** Carry sender name + guid
through `ChatVM` into spans, and compose retail's markup in the speech handlers
behind the player-GUID-range gate. This is the slice that makes the name a
distinct run at all.
- **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.**
**CT4 — tag colour.** Read the authored `0x1D` array per element and apply it
when a tag is open and its type matches. Uses CT1's runs.
### Group B — chat SYSTEM behaviours
**CT5 — click to tell.** Sub-line hit-testing (`HitChar` → run → tag) and
`StartTell` behaviour: write `"@tell {Name}, "`, focus the entry, show the
entry bar. Dispatch keyed generically by tag type, with only `IIDString` wired.
- **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`: retail runs chat text through a taboo table and
substitutes (`PlayerModule::FilterLanguage` + `TabooTableAdaptor::
CheckCensorsW` inside `AddTextToScroll`). We store the option, ship the bit,
show it in Options — and never filter. A decorative toggle.
- **CT-B4** The plain-text session chat log (`ClientSystem::s_pLogFile`). We
write none. Path and rotation are UNKNOWN — needs research or a live check.
**CT6 — chat window behaviours.** Bound the transcript (10,000 chars, trim to
~7,500 preferring a newline boundary); split auto-scroll from the unread
indicator (`0x1000048C` — retail samples "was at bottom" BEFORE the line
lands); the option-gated timestamp prefix. Unbounded scrollback is also a slow
leak for the life of a session, not only a fidelity gap.
### Group C — chat GUI
**CT7 — tail.** The Escape-in-chat-input no-op the audit found (no `Escape`
case in `UiField`, and a focused field also suppresses the input dispatcher's
fallback, so nothing happens at all); delete the dead ImGui-era `ChatPanel`;
reconcile the stale digest/ISSUES rows (#358, #362, #363, #367, #372, #379,
#380, #382 are DONE in code but still listed open).
- **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. CT5's dispatch is generic, so they cost nothing to add later.
## Known-unknowns carried
- The symbolic name behind tag type `0x10000001` (only "Tell" is confirmed);
the full roster lives in the DAT `EnumMapper` category `0x18`.
- Whether the retail transcript supports text selection distinctly from the
entry field.
- The chat log file's path and rotation (`ClientSystem::s_pLogFile`) — retail
writes a plain-text session log a port would miss entirely.
behaviour. CT-A5's dispatch is generic, so they cost nothing to add later.

View file

@ -1,5 +1,20 @@
# Retail client slash-command registry — complete enumeration + acdream audit
> **CORRECTION 2026-08-21 — the "acdream status" columns below are STALE.**
> They record the state on 2026-08-09, BEFORE slice CH4 landed. Every verb this
> document calls MISSING has since been added and verified present in
> `ChatInputParser.ChannelVerbs`: `cg`, `soc`, `o`, `co-vassals`, `covassal`,
> `c`, `fellows`, `group`, `party`, `vassal`, `ab`, `guild`, `gu`, `ct`,
> `clfg`, `crp`. The DIVERGENT row for `g` was also corrected — acdream now
> maps `/g` to Fellowship, matching retail, confirmed against the live retail
> client on 2026-08-21.
>
> The RETAIL side of this document (the registered verbs, handler addresses and
> channel ids) remains accurate and is still the authority. Only the columns
> describing what acdream does are out of date; verify against
> `ChatInputParser.cs` before trusting them.
Date: 2026-08-09
Status: RESEARCH ONLY. No production code was changed.