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>
376 lines
20 KiB
Markdown
376 lines
20 KiB
Markdown
# acdream text-stack audit: seam for retail glyph-level tagged text
|
|
|
|
**Scope.** Research only — OUR codebase (`src/AcDream.App/UI/**`,
|
|
`src/AcDream.UI.Abstractions/Panels/Chat/**`, `src/AcDream.Core/Chat/**`).
|
|
Goal: determine what it takes to support retail's glyph-level tagged
|
|
text (a differently-colored, clickable name inside an otherwise
|
|
uniformly-colored chat line). No code changes made.
|
|
|
|
## 1. Current model — what is a rendered text line?
|
|
|
|
`UiText` (`src/AcDream.App/UI/UiText.cs`) is the one retained-UI text
|
|
widget (`RegisterElementClass(0xc)`, class doc at `UiText.cs:10-22`).
|
|
It has **two** display-line shapes, both single-color:
|
|
|
|
- **`Line`** — `UiText.cs:50`:
|
|
`public readonly record struct Line(string Text, Vector4 Color);`
|
|
One string, one `Vector4` color for the WHOLE string. This is what
|
|
`LinesProvider` (`UiText.cs:62`, `Func<IReadOnlyList<Line>>`) returns
|
|
and what the scrollable multi-line transcript path renders
|
|
(`DrawClippedText`, `UiText.cs:636-691`). Color is **per-line**, not
|
|
per-run: `lines[i].Color` (`UiText.cs:673`/`677`) is one value passed
|
|
whole to `ctx.DrawStringDatPass`/`ctx.DrawString`.
|
|
- **`TextRun`** — `UiText.cs:55`:
|
|
`public readonly record struct TextRun(string Text, Vector4 Color);`
|
|
Multiple colored fragments concatenated onto **one** authored line,
|
|
fed by `RunsProvider` (`UiText.cs:69`,
|
|
`Func<IReadOnlyList<TextRun>>?`) and drawn by `DrawSingleLineRuns`
|
|
(`UiText.cs:693-754`). This is real per-run coloring — each run gets
|
|
its own `ctx.DrawStringDatPass` call at its own pen X
|
|
(`UiText.cs:725-743`) — but it is **only reachable when
|
|
`OneLine == true`** (`UiText.cs:506-510`: `if (OneLine &&
|
|
RunsProvider is { } runsProvider)`), i.e. the static single-line
|
|
label path. The chat transcript is NOT `OneLine` (`ChatWindowController.cs:320`:
|
|
`c.Transcript.OneLine = false;`), so it can never reach
|
|
`DrawSingleLineRuns` — the scrollable multi-line path only ever
|
|
reads `LinesProvider`/`Line`.
|
|
|
|
**Answer to Q1:** color today is per-`Line` in the transcript
|
|
(scrollable, multi-line, bottom-pinned/word-wrapped) path, and
|
|
per-`TextRun` only in the unrelated static single-line label path
|
|
(currently used by exactly one controller — see §5/§6). Chat uses the
|
|
former exclusively.
|
|
|
|
## 2. Where the flattening happens (the key finding)
|
|
|
|
`ChatEntry` (`src/AcDream.Core/Chat/ChatLog.cs:499-536`) is a
|
|
structured record: `Sender` (string), `SenderGuid` (uint), `Text`,
|
|
`ChannelId`/`ChannelName`, `Kind`, `LogTextType`. The sender's identity
|
|
survives as a distinct field all the way through `ChatLog`.
|
|
|
|
It is destroyed in **two** steps inside `ChatVM`
|
|
(`src/AcDream.UI.Abstractions/Panels/Chat/ChatVM.cs`), and the second
|
|
step is the point of no return:
|
|
|
|
**Step A — string composition.** `ChatVM.FormatEntry`
|
|
(`ChatVM.cs:262-313`) string-interpolates `entry.Sender` directly into
|
|
the message prose, e.g. for `ChatKind.LocalSpeech`
|
|
(`ChatVM.cs:269-271`):
|
|
```
|
|
ChatKind.LocalSpeech => IsOwnSpeaker(entry.Sender)
|
|
? $"You say, \"{entry.Text}\""
|
|
: $"{entry.Sender} says, \"{entry.Text}\"",
|
|
```
|
|
After this call the sender name is prose inside one `string`; there is
|
|
no longer a machine-readable boundary marking where "Name" ends and
|
|
"says, ..." begins.
|
|
|
|
**Step B — metadata drop (the actual point of no return).**
|
|
`ChatVM.RecentLinesDetailed()` (`ChatVM.cs:345-371`) builds the
|
|
`FormattedLine` record (`ChatVM.cs:384-388`):
|
|
```
|
|
public readonly record struct FormattedLine(
|
|
string Text,
|
|
ChatKind Kind,
|
|
CombatLineKind? CombatKind,
|
|
uint LogTextType);
|
|
```
|
|
`FormattedLine` does **not** carry `Sender` or `SenderGuid` at all —
|
|
only the composed `Text`, `Kind`, `CombatKind`, and the retail color
|
|
key `LogTextType`. Every downstream consumer
|
|
(`ChatWindowController.GetTranscriptLines`, `ChatWindowController.cs:736-781`,
|
|
which calls `vm.RecentLinesDetailed()` at `ChatWindowController.cs:753`,
|
|
then `ChatTranscriptRenderer.BuildLines`,
|
|
`src/AcDream.App/UI/Layout/ChatTranscriptRenderer.cs:67-95`) only ever
|
|
sees the flat `Text` string plus one `LogTextType` per entry.
|
|
`ChatTranscriptRenderer.BuildLines` then assigns exactly **one**
|
|
`Vector4 currentColor` per entry (resolved once from `LogTextType` at
|
|
`ChatTranscriptRenderer.cs:89`) and stamps every word-wrapped fragment
|
|
of that entry with that single color (`ChatTranscriptRenderer.cs:90-93`):
|
|
```
|
|
if (RetailChatColorTable.TryGetColor(d.LogTextType, out Vector4 resolved))
|
|
currentColor = resolved;
|
|
foreach (string frag in WrapText(d.Text, maxW, measure))
|
|
result.Add(new UiText.Line(frag, currentColor));
|
|
```
|
|
|
|
**So: the sender's identity (name + guid) is available up through
|
|
`ChatLog`/`ChatEntry`, is baked into prose by `ChatVM.FormatEntry`, and
|
|
is then dropped entirely — not merely flattened, but discarded — by
|
|
`ChatVM.RecentLinesDetailed`'s `FormattedLine` shape.** By the time a
|
|
`UiText.Line` exists, there is no span boundary, no guid, and (because
|
|
word-wrap has already run) not even a guarantee that "the sender name"
|
|
is wholly contained within a single rendered `Line` if it happened to
|
|
sit at a wrap boundary. Any attempt to recover "where is the name"
|
|
downstream of this point would have to regex/string-match the composed
|
|
prose back apart — fragile (a message body containing the speaker's
|
|
own name, or a name that is a prefix of a common word, breaks it) and
|
|
still has no guid to attach for the click action.
|
|
|
|
## 3. Draw path
|
|
|
|
**One call per line/run, never per-glyph-color-batch beyond that.**
|
|
The scrollable path (`DrawClippedText`, `UiText.cs:636-691`) issues one
|
|
`ctx.DrawStringDatPass(datFont, text, x, y, color, isOutlinePass)`
|
|
(`UiText.cs:687-689`) per visible `Line` (whole wrapped fragment, one
|
|
color). The DAT-font backend (`UiRenderContext.DrawStringDatPass`,
|
|
`src/AcDream.App/UI/UiRenderContext.cs:320-367`) walks every glyph in
|
|
that ONE string with ONE `tint` (`UiRenderContext.cs:321`, `tint`
|
|
passed once, applied per-glyph at `UiRenderContext.cs:359-362`) — there
|
|
is no per-glyph or per-substring color inside a single
|
|
`DrawStringDatPass` call.
|
|
|
|
**Drawing N differently-colored runs on one visual line requires N
|
|
separate `DrawStringDatPass` calls, each starting at its own pen X** —
|
|
this is exactly the mechanism `DrawSingleLineRuns`
|
|
(`UiText.cs:693-754`) already uses: it measures each run's width
|
|
(`datFont.MeasureWidth(run.Text)`, `UiText.cs:709`/`736`), accumulates
|
|
a `penX` (`UiText.cs:731-737`), and issues one
|
|
`ctx.DrawStringDatPass(datFont, run.Text, run.X, y, run.Color,
|
|
isOutlinePass: false)` per run (`UiText.cs:742-743`). The outline pass
|
|
is batched block-wide first (all runs' outlines, then all runs' fills —
|
|
`UiText.cs:739-743`, same reasoning as the multi-line block-batching
|
|
documented at `UiRenderContext.cs:306-318`) so a per-run outline
|
|
doesn't notch an adjacent run's descender.
|
|
|
|
**Yes, the DAT font path supports starting a draw at an arbitrary X
|
|
offset and measuring a substring's width.** `UiDatFont.MeasureWidth(string
|
|
text)` (`src/AcDream.App/UI/UiDatFont.cs:160-172`) sums per-glyph
|
|
advances for any string/substring — already used for substring
|
|
measurement in the selection-highlight code
|
|
(`UiText.cs:656-657`/`661-662`, `datFont.MeasureWidth(text.Substring(0,
|
|
c0))`). `DrawStringDatPass`/`DrawStringDat` take an arbitrary `float x`
|
|
(`UiRenderContext.cs:277-278`, `320-321`) with no assumption it starts
|
|
at the element's left edge. The bitmap-font fallback (`BitmapFont.cs`,
|
|
`MeasureWidth` at `BitmapFont.cs:167`) and `UiRenderContext.DrawString`
|
|
(`UiRenderContext.cs:188-203`, also takes an arbitrary `float x`) mirror
|
|
the same capability. **Conclusion: the low-level draw primitives
|
|
already support everything a run-based transcript line needs — no
|
|
renderer/font work is required, only a widget-level model change to
|
|
call them N times instead of once.**
|
|
|
|
## 4. Hit-testing
|
|
|
|
**No sub-line hit-testing exists today; click/hover route to whole
|
|
elements, never to a text span.** `UiRoot.HitTestTopDown`
|
|
(`src/AcDream.App/UI/UiRoot.cs:1410-1430`) walks the retained tree via
|
|
`UiElement.HitTest` (`src/AcDream.App/UI/UiElement.cs:705-731`), which
|
|
recurses into children and, failing that, calls the virtual
|
|
`OnHitTest(localX, localY)` (`UiElement.cs:563-564`, default is a
|
|
rectangle containment check) — the granularity is always "some
|
|
`UiElement`", never "some substring of a `UiElement`'s text." A
|
|
resolved hit becomes a `Click` `UiEvent` at `UiRoot.OnMouseUp`
|
|
(around `UiRoot.cs:994-997`) and bubbles via
|
|
`UiRoot.BubbleEvent`/`UiElement.OnEvent` (`UiRoot.cs:1516-1525`,
|
|
`UiElement.cs:571`). `UiText.OnEvent`'s `Click` case
|
|
(`UiText.cs:809-813`) fires the single `OnClick` delegate for the
|
|
WHOLE element — there is no notion of "which run was clicked."
|
|
|
|
**The pieces needed already exist, just not wired to `Click`.**
|
|
`UiText.HitChar(float localX, float localY)` (`UiText.cs:1031-1050`)
|
|
already converts a local point into a `Pos(line, col)` caret position
|
|
using the cached draw geometry (`_lastLines`/`_lastBaseY`/
|
|
`_lastLineHeight`, `UiText.cs:268-273`) and a per-character advance
|
|
lookup (`UiText.cs:1042-1048`, works for both `UiDatFont` and
|
|
`BitmapFont`) — but it is currently invoked only from the
|
|
selection-drag path (`MouseDown`/`MouseMove` cases, `UiText.cs:826-847`),
|
|
gated behind `Selectable` (`UiText.cs:828`, `839`). The chat transcript
|
|
IS `Selectable = true` (`ChatWindowController.cs:321`), so `HitChar`
|
|
already runs on every mouse-down inside the transcript — it is simply
|
|
never asked "which run (if any) covers this `(line, col)`", because
|
|
`Line` carries no runs to check against.
|
|
|
|
**What mapping a click to a run would need:**
|
|
1. A per-line list of run boundaries (start col, end col, and a
|
|
payload — e.g. sender guid) reaching `UiText` alongside the text,
|
|
which does not exist today (`Line` has no such field, see §1/§2).
|
|
2. `HitChar`'s existing `(line, col)` result checked against that
|
|
list — this is a small, local addition to `UiText`, not a new
|
|
hit-test mechanism.
|
|
3. A dispatch from "run payload resolved" to an actual action (e.g.
|
|
pre-filling a `/tell <name>` in the chat input) — analogous to the
|
|
existing `OnClick` delegate, but keyed by run rather than by
|
|
element.
|
|
|
|
No retail-side click semantics (what a name-click does) were
|
|
researched here — that is a different agent's lane per the task brief.
|
|
|
|
## 5. Seam proposal
|
|
|
|
**Given Code Structure Rules (CLAUDE.md "Code Structure Rules" §1-3):**
|
|
- `AcDream.Core` must not depend on window/GL/backend projects (rule 2)
|
|
— `ChatEntry`/`ChatLog` (Core) can carry the STRUCTURED data a tagged
|
|
run needs (sender name + guid + explicit text-span boundaries) but
|
|
must not know about `Vector4`/GL/rendering.
|
|
- UI panels target `AcDream.UI.Abstractions` only (rule 3) — the
|
|
composition of "structured entry -> ordered list of colored,
|
|
optionally-actionable spans" is exactly the kind of pure formatting
|
|
logic `ChatVM` already owns (`ChatVM.FormatEntry`/
|
|
`RecentLinesDetailed`, `ChatVM.cs:262-371`) and should keep owning;
|
|
it must not reach into `AcDream.App` (GL/rendering) types.
|
|
- The retained-widget rendering (`AcDream.App/UI/UiText.cs`) is where
|
|
GL-adjacent draw calls (`DrawStringDatPass`) and Silk.NET-adjacent
|
|
hit-testing (`HitChar`, `UiRoot`) live, and must stay there.
|
|
|
|
**Proposed layering (three seams, one per project boundary):**
|
|
|
|
1. **`AcDream.UI.Abstractions` (data shape)** — introduce a
|
|
run-carrying line shape parallel to (not replacing) `FormattedLine`.
|
|
Sketch: `FormattedLine` gains an optional ordered list of spans, or
|
|
a new `RichFormattedLine(IReadOnlyList<FormattedRun> Runs, ...)` is
|
|
added, where `FormattedRun` is something like
|
|
`(string Text, uint? ActorGuid, bool IsSpeakerName)` — deliberately
|
|
NOT carrying a color yet (`AcDream.UI.Abstractions` has no
|
|
`System.Numerics`/GL dependency requirement today, but keeping
|
|
color resolution in `AcDream.App` mirrors the existing
|
|
`RetailChatColorTable`/`ChatTranscriptRenderer` split, where
|
|
`ChatVM` supplies `LogTextType`/structured data and
|
|
`ChatTranscriptRenderer` in `AcDream.App` resolves it to `Vector4`).
|
|
`ChatVM.FormatEntry` (`ChatVM.cs:262-313`) would need a sibling that
|
|
returns spans instead of one interpolated string — e.g. split each
|
|
`case` into "prefix run" / "sender run" / "suffix run" instead of a
|
|
single `$"..."` — and `RecentLinesDetailed` would carry
|
|
`entry.Sender`/`entry.SenderGuid` through instead of discarding them
|
|
(the §2 fix).
|
|
2. **`AcDream.App/UI/Layout` (composition)** — `ChatTranscriptRenderer.BuildLines`
|
|
(`ChatTranscriptRenderer.cs:67-95`) is the existing per-controller-
|
|
shared seam that already resolves `LogTextType -> Vector4` and
|
|
word-wraps. It would gain a variant that word-wraps a RUN LIST
|
|
instead of a flat string per entry, producing a new "rendered line
|
|
with runs" shape (see below) instead of `UiText.Line`. Both
|
|
`ChatWindowController` (`ChatWindowController.cs:778`) and
|
|
`FloatingChatWindowController` (same shared function, per the class
|
|
doc at `ChatTranscriptRenderer.cs:9-15`) would switch to the new
|
|
builder — this is the one place both chat surfaces already share,
|
|
so it is the natural single point of change for chat specifically.
|
|
3. **`AcDream.App/UI/UiText.cs` (widget)** — this is where the actual
|
|
gap is. `Line` needs an additive `Runs` concept for the
|
|
MULTI-LINE (`OneLine == false`) path, not just the existing
|
|
`OneLine`+`TextRun` path (§1). Minimal shape: extend `Line` (or add
|
|
a parallel `RichLine`) to carry `IReadOnlyList<TextRun>` alongside
|
|
or instead of a flat `string Text` + single `Vector4 Color`; change
|
|
`DrawClippedText`'s multi-line loop (`UiText.cs:636-691`) to, for a
|
|
line with runs, do what `DrawSingleLineRuns` already does per-run
|
|
(walk runs, accumulate `penX`, call `DrawStringDatPass` per run,
|
|
batch all outlines-then-all-fills at the BLOCK level exactly as
|
|
`UiText.cs:681-690` already batches across LINES today — extending
|
|
that batching one level deeper, across runs within lines, is
|
|
mechanical). `HitChar` (`UiText.cs:1031-1050`) needs to additionally
|
|
resolve which run (if any) contains the hit `col`, and `OnEvent`'s
|
|
`Click` case (`UiText.cs:809-813`) needs a second dispatch path
|
|
(run-click, distinct from whole-element `OnClick`) that a controller
|
|
(e.g. `ChatWindowController`) can bind to "prefill a tell to this
|
|
guid," mirroring how `OnClick` is bound today.
|
|
|
|
**Existing abstraction that already almost does this:**
|
|
`TextRun`/`RunsProvider`/`DrawSingleLineRuns` (§1, `UiText.cs:55,69,693-754`)
|
|
is the closest precedent — it proves the draw-side mechanics (measure
|
|
run, accumulate pen, per-run `DrawStringDatPass`, block-batched
|
|
outline) already work and are exercised in production by
|
|
`CharacterStatController.BuildSelectedTitleRuns`
|
|
(`src/AcDream.App/UI/Layout/CharacterStatController.cs:1425-1454`,
|
|
wired at `CharacterStatController.cs:1680`) for a skill/attribute title
|
|
with a colored numeric delta suffix. It is currently scoped to
|
|
`OneLine` only and carries no click/actor payload — extending it to
|
|
the multi-line/wrapped path and adding a payload field is smaller than
|
|
building a new mechanism from scratch. `DatRichText`
|
|
(`src/AcDream.App/UI/Layout/DatRichText.cs`, `Segment(string? Text,
|
|
Vector4 Color)` at `DatRichText.cs:40`, `Compose` at
|
|
`DatRichText.cs:52-88`) is a second, partially-overlapping precedent:
|
|
it already composes multiple colored segments for a multi-line box,
|
|
but it word-wraps EACH segment independently and concatenates the
|
|
results as separate `Line`s (`DatRichText.cs:83-84`) — so two segments
|
|
that would visually share one wrapped row are NOT joined onto that row
|
|
today; it solves "multiple colors across a paragraph's several lines,"
|
|
not "multiple colors sharing one rendered row." A tagged-name-in-chat
|
|
feature needs the latter (the name and the rest of the sentence share
|
|
row 0 of a possibly-multi-row wrapped message), so neither existing
|
|
mechanism is a drop-in — both inform the shape of the fix.
|
|
|
|
## 6. Blast radius
|
|
|
|
`UiText.Line`/`LinesProvider` is used extremely broadly — 52 files
|
|
reference `LinesProvider = ` and 53 reference `UiText.Line(`/`new
|
|
UiText()` (full grep list retained below). **If the change is additive**
|
|
(new optional `Runs` field/type alongside the existing `Line`, default
|
|
behavior unchanged for every caller that keeps returning plain
|
|
`Line`s), the blast radius for BEHAVIOR is limited to whichever
|
|
controllers opt in (initially: chat only). The blast radius for
|
|
BUILD/COMPILE risk (anything that touches `UiText.cs`, `UiElement.cs`
|
|
recompiles the whole `AcDream.App` UI layer) and for REVIEW is the full
|
|
list below, grouped by category — every one of these should be
|
|
smoke-tested after a `UiText`-internal change even if it doesn't touch
|
|
their own code:
|
|
|
|
- **Chat (the actual feature target):**
|
|
`ChatWindowController.cs`, `FloatingChatWindowController.cs`,
|
|
`ChatTranscriptRenderer.cs`, `SpewBoxController.cs`
|
|
(`src/AcDream.App/UI/SpewBoxController.cs` — retail's other
|
|
colored-text-scroll surface, `RetailLogTextType.ClientLocal` per
|
|
`ChatVM.cs:159-183`; likely wants the SAME run model eventually since
|
|
it renders `LogTextType`-colored lines too).
|
|
- **Tooltips:** `RetailTooltipPresenter.cs` — world/UI hover tooltips;
|
|
currently plain `Line`s.
|
|
- **Appraisal / item & creature reports:** `AppraisalUiController.cs`,
|
|
`CreatureAppraisalRows.cs`, `ItemAppraisalReport.cs` — these already
|
|
render multi-colored informational text (spell names, damage types)
|
|
as SEPARATE `Line`s per colored fragment (one color per whole line,
|
|
not per run) — a run model could simplify these, or they could stay
|
|
as-is if row-granularity coloring already meets retail fidelity
|
|
there (not assessed here — out of this audit's scope).
|
|
- **Social panels:** `SocialSquelchPageController.cs`,
|
|
`SocialAllegiancePageController.cs`,
|
|
`SocialFellowshipPageController.cs`, `SocialFriendsPageController.cs`
|
|
— friends/allegiance/fellowship rows; per CLAUDE.md's Campaign FA
|
|
notes these already do per-state color swaps on `UiText`, a
|
|
different (not run-based) mechanism.
|
|
- **Vendor / trade:** `VendorUiController.cs`,
|
|
`SecureTradeUiController.cs`.
|
|
- **Combat / spellcasting:** `CombatUiController.cs`,
|
|
`SpellcastingUiController.cs`, `EffectsUiController.cs`.
|
|
- **Dialogs:** `RetailWaitDialogView.cs`, `RetailMessageDialogView.cs`,
|
|
`RetailConfirmationDialogView.cs`,
|
|
`RetailConfirmationTextInputDialogView.cs`.
|
|
- **Character sheet / creation:** `CharacterStatController.cs` (the
|
|
existing `TextRun` consumer, §5), `CharacterCreationSkillsPage.cs`,
|
|
`CharacterCreationSummaryPage.cs`, `CharacterCreationTownPage.cs`,
|
|
`CharacterCreationProfessionPage.cs`,
|
|
`CharacterCreationHeritagePage.cs`,
|
|
`CharacterManagementUiController.cs`.
|
|
- **Options / config:** `ConfigOptionsPageController.cs`,
|
|
`ChatOptionsPageController.cs`, `CharacterOptionsPageController.cs`,
|
|
`KeyboardConfigController.cs`.
|
|
- **Misc panels:** `InventoryController.cs`, `RadarController.cs`,
|
|
`MapPageController.cs`, `HousePageController.cs`,
|
|
`LinkStatusUiController.cs`, `VitaeUiController.cs`,
|
|
`VitalsController.cs`, `RetailFpsController.cs`,
|
|
`SelectedObjectController.cs`, `IndicatorDetailText.cs`,
|
|
`ComponentBookTemplateFactory.cs`, `EffectRowTemplateFactory.cs`,
|
|
`CharacterController.cs`, `DatWidgetFactory.cs` (the factory that
|
|
builds every `UiText` from LayoutDesc — touches all of the above by
|
|
construction).
|
|
- **Tests:** `UiTextTests.cs`,
|
|
`SocialFellowshipPageControllerTests.cs`,
|
|
`SocialPanelControllerTests.cs`, `RowTemplateResolverTests.cs`,
|
|
`DatWidgetFactoryTests.cs`, `CharacterStatControllerTests.cs`,
|
|
`VitalsBindingTests.cs`, `AppraisalUiControllerTests.cs` — any of
|
|
these that assert on `UiText.Line` shape/count would need review if
|
|
`Line`'s shape changes (not if a new type is added additively).
|
|
|
|
**Net:** an ADDITIVE seam (new run-carrying line type, existing `Line`
|
|
untouched) keeps the functional blast radius to chat (and optionally
|
|
SpewBox) while still requiring the whole `AcDream.App/UI` tree to
|
|
rebuild/retest since it all depends on `UiText.cs`/`UiElement.cs`. A
|
|
seam that changes `Line`'s existing shape would force a review pass
|
|
across every file in the list above.
|
|
|
|
## What this audit did NOT do
|
|
|
|
- Did not research retail's own tagged-glyph-run mechanism (separate
|
|
agent's lane per the task brief).
|
|
- Did not propose or write any code change — `Line`/`TextRun`/`Segment`
|
|
shapes above are illustrative sketches for sizing, not a spec.
|
|
- Did not assess whether `AppraisalUiController`/`CreatureAppraisalRows`'s
|
|
existing one-color-per-`Line` approach is already retail-faithful for
|
|
their own content (out of scope; flagged only as a blast-radius
|
|
member).
|