Commit graph

3 commits

Author SHA1 Message Date
Erik
9f6d79b7e0 feat(chat): CT-C2 Escape leaves the entry; CT-C3 the timestamp is grey
Campaign CT slices C2 and C3.

**C2 — Escape in the chat input did nothing at all.** Not "did the wrong
thing": nothing. Two independent facts had to hold for that. UiField has no
Escape case, AND a focused field reports IsEditControl, which makes UiRoot skip
its own fallback and the input dispatcher withhold game actions — so the player
had no way out of the bar except the mouse.

Retail maps Escape to input action 0x0B, which runs
ChatInterface::DeactivateChatEntry @0x004F2FC0: RelinquishFocus, then
Deactivate. It does NOT clear the field. That is worth stating because the
obvious guess — "Escape clears the input" — is wrong and would have looked
perfectly reasonable; a half-written message survives stepping away from the
bar, and the test pins that rather than just pinning "handled".

**C3 — the timestamp took the message's colour.** Retail appends it as its own
run at a FIXED colour index (0x0C, which BuildChatColorLookupTable @0x004F31C0
fills with colorGrey) rather than the line's, so it stays grey whether the
message is red combat text or white speech.

Most of C3 was already done and stayed untouched: the DisplayTimeStamps option
is polled, and FormatTimestampPrefix already matches retail's "%#H:%M:%S ".
Only the colour was wrong, and it was only fixable now because A1/A4 made a
line able to carry more than one colour.

The stamp is a span ROLE rather than a second tag type: it is not clickable and
carries no payload, so modelling it as a tag would have made it hit-testable
for no reason. Its colour comes from the same runtime table every message
colour comes from, unlike the tagged-name colour, which is authored per element
(0x1D) and deliberately lives elsewhere.

One consequence worth naming: a timestamped line now needs runs even when its
sender is not tagged, because the stamp alone is reason enough. Before this,
only tagged lines got runs.

Also verified and NOT changed, having checked rather than assumed: C1's
auto-scroll half is already retail-faithful — UiScrollable.SetExtents samples
"was at the end" BEFORE applying new extents and only re-sticks if so, which is
exactly retail's IsAtVerticalEnd rule, and chat gets it by default. C1 reduces
to the unread indicator, which does not exist yet.

Solution builds clean; full hermetic gate green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:28:45 +02:00
Erik
0f1660d6ea feat(chat): CT-B1 — bound the transcript by retail's character budget
Campaign CT slice B1.

CORRECTION TO THE PLAN: this slice was written as "the transcript grows for the
life of the session — a slow leak". That was wrong, and the plan said it
because I read the retail-side finding and inferred our side without checking.
ChatLog has always been bounded (ConcurrentQueue, maxEntries default 500, with
a dequeue loop in Append). There was no leak.

The real gap is the UNIT. Retail bounds the rendered transcript by CHARACTERS —
0x2710, beheaded toward 0x1D4C at a newline boundary — while we bounded the
model by messages. Two different things: a window of 500 messages is far more
scrollback than 10,000 characters, and the message cap is a safety limit on the
log rather than a display rule.

So the budget is applied where retail applies it: on the rendered window, not
the model. ChatLog's entry cap stays as the model-level bound.

Two deliberate simplifications, both registered as CT-1 rather than left
implicit:

  - ONE threshold, not retail's two. The hysteresis exists to stop retail
    re-trimming an accumulating buffer on every append; we rebuild the visible
    list each time, so there is nothing to damp, and a second threshold would
    only make the oldest visible line jump around as messages arrive.
  - Whole-line cutting rather than a newline search near an offset — our unit
    already IS the line, which is what retail's newline preference is for.

Filtered-out lines deliberately do not consume budget: a line this window
filters out is not in retail's buffer at all, so counting it would mean turning
a filter OFF silently shortened the visible history.

Solution builds clean; full hermetic gate green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 08:06:18 +02:00
Erik
53395e4de4 feat(chat): CT-A4 — speaker names render in retail's tag colour
Campaign CT slice A4, and the first slice of Group A that shows on screen: a
player's name in a chat line now draws green while the rest of the line keeps
its message colour.

The colour is AUTHORED, not computed. Retail keeps two parallel index-selected
colour arrays on the text element and refreshes both from the same caller index
on every append (UIElement_Text::AppendStringInfoWithFont @0x00469DE0):
property 0x1B for ordinary glyphs, 0x1D for glyphs under an open tag. Property
0x1D is read exactly the way 0x1B already was, carried on ElementInfo, and
seeded onto UiText beside DefaultColor. Measured on the chat transcript
(0x2100006F / 0x10000011) as RGB(0,178,0).

It deliberately does NOT go into RetailChatColorTable. That table is the
runtime-built per-LogTextType mapping; the tag colour is per-element authored
data, and filing it there would put it somewhere it would look right in tests
and be wrong in principle.

RunsForFragment is the load-bearing piece and is pure. Wrapping can drop the
space it broke on, so a fragment is NOT simply the next N characters of the
line — BuildLines locates each fragment in the source text to keep the span
offsets honest, and the mapper clips spans to the fragment window. A tag
straddling a wrap break is therefore split across both fragments and stays
green on both, instead of changing colour mid-word.

Two guards worth naming. A fragment containing no tag returns NULL rather than
a single-run list, so the overwhelming majority of lines keep the existing flat
draw path untouched. And an element authoring no 0x1D falls back to the line
colour, so a name never renders in a colour nobody chose.

The run/fragment contract is property-tested across every substring of a tell
line, because CT-A1's RunsMatchLine refuses mismatched runs by silently falling
back to flat text — a mapping bug here would degrade quietly rather than fail.

Solution builds clean; full hermetic gate green.

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