docs: Campaign CT — chat text tags, researched and planned
Six parallel research lanes on retail's chat text and window behaviour, plus a plan. The headline: the green clickable speaker name is not a chat feature and not a colour, it is a missing capability in the TEXT stack. Retail's client sprintfs literal tag markup into the chat line, and the text element parses the brackets while appending, attaching a ref-counted tag PER GLYPH. A tagged run is emergent: adjacent glyphs whose tag pointers are equal. A glyph takes the tag colour (property 0x1D) only when a tag is open and its type is 0x10000001; otherwise the ordinary line colour (0x1B). The colour itself was the one thing the decomp could not settle — it is authored, not runtime-built — so it was MEASURED out of the installed dats rather than assumed from a screenshot: P0x1D = RGB(0,178,0). That also exposed a trap: the tag colour is per-ELEMENT and authored while the line colour on the same element comes from the runtime chat table, so filing "tag green" into the LogTextType table would put it in the wrong place. Our own audit found the gap is narrower than feared. UiText ALREADY draws multi-coloured runs (the character stat panel uses it); the path is just gated to single-line elements. The draw path needs no renderer work, and HitChar already resolves a click to line+column. The real blocker is that sender identity is destroyed before it reaches the renderer: ChatEntry carries Sender/SenderGuid the whole way, and ChatVM.RecentLinesDetailed drops both. Two findings beyond the original question. Retail BOUNDS its transcript (10,000 chars, trimmed to ~7,500 at a newline) and splits auto-scroll from an unread indicator by sampling "was at bottom" before the line lands — a naive port auto-scrolls forever and leaks for the life of a session. And the chat-UI audit turned up an untracked bug: Escape in the chat input does nothing at all, because UiField has no Escape case and a focused field also suppresses the input dispatcher's fallback. Every lane was instructed to write "UNKNOWN — needs X" rather than guess, and they did; the carried unknowns are listed in the plan rather than papered over. Seven slices proposed, nothing implemented yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
be324c003c
commit
663129c340
6 changed files with 2732 additions and 0 deletions
126
docs/plans/2026-08-21-chat-text-tag-campaign.md
Normal file
126
docs/plans/2026-08-21-chat-text-tag-campaign.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Campaign CT — chat text tags and chat-window parity
|
||||
|
||||
**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**.
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
5. **Measured** out of the installed dats (`LayoutDump --colors`), chat
|
||||
`0x2100006F`, transcript `0x10000011`:
|
||||
|
||||
P0x1B (line) [0x00] R=204 G=204 B=204
|
||||
P0x1D (tag) [0x00] R= 0 G=178 B= 0 <- the green
|
||||
|
||||
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.
|
||||
|
||||
Two facts that shape the port:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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).
|
||||
|
||||
## 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue