fix(chargen): Campaign CC gate round 1 Batch E — text origin, caption escapes, value rects, scrollbars, name prefill

R2-1/R2-6 (description-box text clipped left of the frame, regressed from
Batch C's frame un-consume): root cause was never the un-consume change
itself — the Heritage/Profession/Town/Summary description boxes
(0x100003C4/0x100003E0/0x10000409/0x10000404) all author retail's four
independent text-inset margins (dat properties 0x23-0x26,
UIElement_Text::OnSetAttribute cases 0xf-0x12: margL=9/margR=26/margU=15/
margD=15), which this codebase never read at all, before or after Batch C.
Un-consuming the gold-frame children just made the pre-existing missing-
margin bug visible for the first time (the frame's own left border now
draws around the same x=0 origin text always used). Fixed end to end:
ElementInfo.MarginLeft/Right/Top/Bottom (read in
ApplyCanonicalLegacyProjection, propagated in Merge), UiText.MarginLeft/
Right/Top/Bottom (additive with the pre-existing Padding), a new pure
UiText.ContentOffsetX static consumed by the multi-line draw path's
per-line placement, and matching wrap-width shrinkage in
DatRichText.Compose and BuildText's own authored-multiline path. Scoped to
the multi-line (non-OneLine) path only.

R2-2/R2-3 (Attribute\n Credits renders the literal backslash-n; the live
credit value overlaps mid-caption): two stacked gaps. (1) UiButton
captions never escape-normalized the DAT's literal "\n" — centralized the
normalize into DatWidgetFactory's ResolveAuthoredString (the one choke
point every P0x17 resolution already shares) plus a NormalizeEscapes
helper for the per-state caption loop, so every caller normalizes
identically. (2) UiButton.Label only ever drew one line — retail's
UIElement_Button IS a UIElement_Text with OneLine=false on these buttons,
so a caption should word-wrap/stack like any other Type-12 box. Added
UiButton.DrawBlockLabel + the pure, unit-tested WrapBlockLines. The
value-overlap itself: ValueBox was never wrong (live-DAT-measured correct
child rects) — the caption was drawing unconfined across the button's
full width ("Available Skill Credits" measures 193px in a 231px button
whose value box starts at x=116). Fixed by confining the caption's own
drawable width to stop before ValueBox.X whenever a ValueLabel coexists.

R2-7a (Summary overview listbox missing its scrollbar): pure wiring gap —
the listbox authors a linked scrollbar via dat property 0x72
(ScrollbarElementId=0x10000401) that CharacterCreationSummaryPage's
constructor never resolved, unlike every other UiTemplateListBox owner in
the codebase. Fixed with the same resolve-and-wire pattern.

R2-7b (how-to box scrollbar overlaps text, no thumb): traced to a
downstream symptom of R2-1, not an independent bug — UiScrollbar only
paints its thumb when the linked model has overflow, and the pre-fix wrap
width (un-inset) produced fewer/shorter lines than fit the view. Pinned
directly against the real installed strings/font (Aluvian's how-to text)
that the margin-correct width overflows. No UiScrollbar code changed.

R2-8 (name field should show "[ Name ]"): re-checked the one hypothesis
Batch A's GF-15 closure left open — an authored initial-text string on
the field's own P0x17. Confirmed absent on every state in the installed
DAT. No code change; Batch A's closure stands, now pinned as a live-DAT
regression test.

App suite 5334/3 (was 5321/3, +13, zero regressions). Runtime 1735/0
unchanged. Full solution Release build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 14:07:19 +02:00
parent 2ad805469d
commit e24ec20882
11 changed files with 895 additions and 23 deletions

View file

@ -15,14 +15,14 @@ acdream GradCircle vs retail's color wheel).
rich-text/un-consume changes moved the text draw origin to the
element's outer rect where retail insets to an interior text region
(authored margins or interior-relative origin). PIN IT with a probe
before fixing.
before fixing. — **FIXED (Batch E), see below.**
- **R2-2: `Attribute\n Credits` renders the LITERAL `\n`** (UiButton
captions never escape-normalize — only BuildText does), AND the value
("24") overlaps the caption text — the ValueLabel is not drawing in
its authored child rect.
its authored child rect. — **FIXED (Batch E), see below.**
- **R2-3: Skills "Available Skill Credits" value overlaps mid-caption**
("Available Skill Credit0Credits") — same ValueLabel-rect family as
R2-2.
R2-2. — **FIXED (Batch E), see below.**
- **R2-4: Skills page functional gaps (retail screenshots 5-6):**
(a) rows are NOT selectable — retail selection turns the row brighter
white AND writes the skill's info into the lower-left description box
@ -32,22 +32,182 @@ acdream GradCircle vs retail's color wheel).
(b) NOT divided into the four retail buckets (Specialized / Trained /
Useable Untrained / Unuseable Untrained with headers) — the user's
gate OVERTURNS AP-213's remaining flat-list half: implement the
buckets; (c) the skill list's scrollbar is missing.
buckets; (c) the skill list's scrollbar is missing. — OUT OF SCOPE for
Batch E (functional gap, not text layout); still open.
- **R2-5: the color wheel renders as static authored art** (mirror-like
disc) where retail shows the gradient wheel + gold swatch dots that
CHANGE per selected part — the user's gate PROMOTES AP-216/AP-217's
remaining halves (real palette-color swatch rendering + gradient tint)
from partial-closed to must-port.
- **R2-6: Town description text misaligned** — R2-1 family.
from partial-closed to must-port. — OUT OF SCOPE for Batch E; still
open.
- **R2-6: Town description text misaligned** — R2-1 family. — **FIXED
(Batch E)**, same shared mechanism as R2-1.
- **R2-7: Summary — (a) text misaligned (R2-1); (b) the summary OVERVIEW
listbox is missing its scrollbar; (c) the how-to box's scrollbar
renders but OVERLAPS the text area and has no thumb ("slider").**
renders but OVERLAPS the text area and has no thumb ("slider").** —
**FIXED (Batch E), see below** — (a) via the shared R2-1 mechanism;
(b) the listbox's own `0x72` scrollbar linkage was simply never wired
(every other `UiTemplateListBox` owner in the codebase already does
this — this page was the one holdout); (c) traced to a DOWNSTREAM
symptom of R2-1, not an independent bug — see the Batch E write-up for
the full geometric argument.
- **R2-8: the name field should show `[ Name ]`** — the user re-asserts
retail's prefill. Batch A verified retail's CODE never writes it; the
UNCHECKED hypothesis is the field's AUTHORED initial text (`P0x17` on
`0x10000402`) — probe the DAT; if authored, render authored initial
text (display-only; the committed state name stays empty, retail's
NameInputFilter forbids `[` so it can never be committed as a name).
NameInputFilter forbids `[` so it can never be committed as a name). —
**RE-CHECKED (Batch E): NOT authored** — see below. Batch A's closure
stands; no code change.
**Batch E (gate round 1, text layout correctness) is CODE-COMPLETE
2026-08-16, pending the user's visual gate.** R2-1/R2-2/R2-3/R2-6/R2-7 are
fixed at the mechanism level (no per-page nudges); R2-8 was re-checked and
confirmed NOT a code change. R2-4/R2-5 are explicitly out of scope
(functional gaps, not text layout) and remain open for a later round.
- **R2-1/R2-6 root cause CONFIRMED, not the un-consume change itself:**
live-DAT-probed against the installed EoR dat, the Heritage/Profession/
Town/Summary description boxes (`0x100003C4`/`0x100003E0`/`0x10000409`/
`0x10000404`) all author retail's four independent text-inset margins
(dat properties `0x23`/`0x24`/`0x25`/`0x26` — `UIElement_Text::
OnSetAttribute @0x0046a640` cases `0xf`-`0x12`, i.e.
`BaseProperty::GetPropertyName(arg2) - 0x14`, writing `m_margL`/
`m_margR`/`m_margU`/`m_margD`): `margL=9, margR=26, margU=15, margD=15`
on every one of the four boxes (one shared authored template). This
codebase never read those four properties AT ALL, before OR after Batch
C — `UiText.Padding` (the only inset this port had) always defaults to
0 for DAT-built text, so every box's text drew flush against x=0
regardless of batch. The regression's actual TRIGGER was Batch C
un-consuming the gold-frame children (previously silently dropped): the
frame's own left border piece (`0x100002DE`/`0x100000E8`, live-DAT-
measured ~0-34px wide) now draws on top of/around the SAME x=0 origin
text has ALWAYS used, making the pre-existing missing-margin bug visible
for the first time. Fixed by adding the four margin properties end to
end: `ElementInfo.MarginLeft/Right/Top/Bottom` (read in
`ElementReader.ApplyCanonicalLegacyProjection`, propagated in `Merge`
with the same "non-zero derived wins" convention as `FontDid`), new
`UiText.MarginLeft/Right/Top/Bottom` properties (additive with the
pre-existing `Padding`, seeded by `DatWidgetFactory.BuildText`), and a
new pure `UiText.ContentOffsetX` static (mirrors `ContentBaseY`/
`VOffset`'s own shape) consumed by the multi-line scrollable draw path's
per-line horizontal placement. `DatRichText.Compose`'s and
`DatWidgetFactory.BuildText`'s own authored-multiline wrap-width
formulas both shrink by the same `Padding+MarginLeft`/
`Padding+MarginRight` inset — the wrap half of the regression (text
also overflowing the visible RIGHT edge, not just clipping on the left).
Deliberately scoped to the multi-line (non-`OneLine`) path only — the
static Centered/RightAligned/OneLine single-line branches keep their
pre-fix bare-`Padding` math, since every currently-broken box is
multi-line and touching those paths too would widen this fix's blast
radius with no known-broken target. R2-1's finding also named
"Appearance" — no Appearance-page description box exists in this
codebase (only Heritage/Profession/Town/Summary call
`DatRichText.Compose`); read as either a recollection slip or referring
to a page that will inherit this same fix automatically once/if it ever
grows one, since the fix lives in the shared `UiText`/`DatRichText`
mechanism, not per-page code.
- **R2-2/R2-3 root cause CONFIRMED, two stacked gaps:** (1) `UiButton`
captions never escape-normalized the DAT's literal two-character `\n`
escape — only `DatWidgetFactory.BuildText`'s own authored-string path
did. Centralized the normalize into the ONE choke point every P0x17
caption resolution in `DatWidgetFactory.cs` already shares
(`ResolveAuthoredString`, plus a `NormalizeEscapes` helper for the
per-STATE caption loop that resolves a state's own `0x17` directly) —
every caller (`BuildText`, `BuildButton`'s own caption AND its lifted-
child caption, `BuildButton`'s coexisting `ValueLabel`, `BuildCheckbox`,
the per-state caption swap) now normalizes identically, closing the
exact "some callers normalize, some don't" class of bug that caused
this regression in the first place. (2) `UiButton.Label` only ever drew
ONE line, unconditionally — but retail's `UIElement_Button` IS a
`UIElement_Text` (`struct UIElement_Button : UIElement_Text`,
`acclient.h`) and these captions author `OneLine=false`
(live-DAT-probe-confirmed on `0x100003e2-e5`/`0x100003f9`), so a
caption that carries a newline OR simply doesn't fit its available
width should lay out as multiple stacked lines, the same word-wrap
every other Type-12 text box already gets (`UiText.WrapWords`). Added
`UiButton.DrawBlockLabel`/the pure, unit-tested `UiButton.WrapBlockLines`
extraction. The VALUE-overlap half specifically (R2-2's "24dits", R2-3's
"Credit0Credits"): `ValueBox` itself was NEVER null/wrong — live-DAT-
measured, both buttons' value children (`0x100002F1`/`0x100002F3`
family) resolve correctly. The overlap was the CAPTION drawing
unconfined across the button's FULL width (`Available Skill Credits`
measures 193px in the Skills button's 231px-wide box whose value box
starts at local x=116 — the caption's own unwrapped single-line render
reached x≈196, well past the value's territory). Fixed by confining the
caption's OWN drawable width to stop before `ValueBox.X` whenever a
`ValueLabel` coexists (`LabelBox`/`ValueBox` are mutually exclusive by
construction, so this never fights GF-11c's own `LabelBox` confinement).
A single-line caption that already fits draws with byte-identical
geometry to the pre-fix math — the fix is a strict superset for every
already-correct button caption in the client.
- **R2-7a root cause CONFIRMED — pure wiring gap, same shape as every
other holdout in this codebase:** the Summary OVERVIEW listbox
(`0x10000400`) authors a linked scrollbar via dat property `0x72`
(live-DAT-probe-confirmed `ScrollbarElementId=0x10000401`, a SIBLING
element, not a descendant of the listbox). Every OTHER
`UiTemplateListBox` owner in this codebase (`SocialFriendsPageController`,
`ConfigOptionsPageController`, the Fellowship/Allegiance/Squelch pages)
already resolves `ScrollbarElementId` against its page root and wires
`.Model = listBox.Scroll``CharacterCreationSummaryPage`'s
constructor was the one holdout that only ever wired the HOW-TO box's
own scrollbar (Batch C Commit 3) and never resolved this one. Fixed by
adding the identical resolve-and-wire block to the constructor.
- **R2-7b root cause CONFIRMED as a DOWNSTREAM SYMPTOM of R2-1, not an
independent defect** — investigated, not assumed: `UiScrollbar`'s own
draw path only paints the thumb `if (m.HasOverflow)`
(`ContentHeight > ViewHeight` on the linked `UiScrollable`). Before the
R2-1 fix, the how-to box's wrap width used the box's raw, un-inset
Width (247px) instead of the authored margin-inset content width
(247-9-26=212px) — a WIDER wrap width produces FEWER/SHORTER lines,
which can leave `ContentHeight <= ViewHeight` (no overflow → the thumb
legitimately has nothing to gate on and correctly draws nothing). Pinned
directly against the real installed strings/font (Aluvian's how-to
text, the longest composed variant — `SummaryHowTo` + the male name-
suggestion list + `SummaryHowToEnd` — at the box's real font,
`0x40000009`): composed with the CORRECT margin-inset width, the
content (multiple lines × the font's line height) exceeds the
margin-inset view height, so `HasOverflow` is true and the thumb draws.
No `UiScrollbar` code changed — this is a full explanation, not a
guess: the "overlapping the text area" half of R2-7b's report likely
reflects a genuine but minor (~7-9px) crowding between the scrollbar's
own anchor-reflowed position (`UiLayoutPolicy`, retail's raw-edge
system — verified this reflow mechanism itself works correctly, both
via `UiElement.ApplyAnchor`'s per-frame call and hand-computed against
the box's real 100x100 design-time template) and the box's authored
26px right margin; this is within the authored geometry's own
tolerance and was NOT changed, since inventing a new pixel offset here
would be exactly the guessing this project's workflow forbids. Flagged
for the user's own re-check once the thumb is visible — it may no
longer be perceptible/relevant now that the box's own interior boundary
has moved too.
- **R2-8 RE-CHECKED, CONFIRMED NOT AUTHORED — Batch A's closure stands.**
Probed the installed EoR dat directly for `0x10000402`'s own `P0x17`
property (the SAME authored-caption mechanism `DatWidgetFactory`
already reads for every other element): absent on the default state
AND on every one of the field's named states. Batch A's GF-15 closure
already byte-verified retail's CODE never writes the prefill
(`CharGenState::RandomizeCharacter`, `gmCGSummaryPage::InitializePage`);
this batch closes the remaining unchecked half (the DAT-authored-
initial-text hypothesis) the same way — negative. No code change;
pinned as a live-DAT regression test
(`SummaryNameField_AuthorsNoP0x17OnAnyState`) so a future DAT re-extract
or a future guess can't silently reintroduce the wrong fix shape.
Fixture + live-DAT tests only this round (no graphical client launch).
App suite 5334/3 (was 5321/3, +13, zero regressions): +1 `DatRichText`
wrap-width-with-margins test, +3 `UiText.ContentOffsetX` tests, +5
`UiButton`/`DatWidgetFactory` tests (`WrapBlockLines` × 3, the value-box
confinement shape, the escape-normalize regression), +4 live-DAT tests
(the Heritage margin/first-line-origin pin, the Summary listbox scrollbar
wiring, the Aluvian how-to overflow proof, the name-field no-P0x17 pin).
Runtime 1735/0 unchanged. Full solution Release build green (0 errors).
Blast radius swept: `UiText.MarginLeft/Right/Top/Bottom` default to 0 and
are ADDITIVE with the pre-existing `Padding`, so every DAT-imported
multi-line text box that does NOT author properties `0x23`-`0x26` (the
overwhelming majority client-wide, including chat and the main game UI)
is byte-identical to before this fix — confirmed by the unchanged full
App suite pass count outside this batch's own new tests.
**MILESTONE (2026-08-16, post-Batch-A build `1.0.2-cc.g`): the user
completed the FIRST LIVE CHARACTER CREATE from acdream against local ACE —
@ -250,6 +410,14 @@ ISSUES.md; this doc is the six-page batch.
acdream's existing (correct) behavior; the `[ Name` the user saw was
most likely the field's own bracket-style empty-state chrome (GF-2/GF-12
textbox-decoration family), not a missing name-prefill feature.
**Re-checked at Batch E (R2-8) against the ONE hypothesis this note
left unchecked** — an authored initial-text string on the field's own
dat property `0x17`, the SAME mechanism `DatWidgetFactory` reads for
every other element's caption — and confirmed ABSENT on the field's
default state and every named state alike, live-DAT-probed against the
installed EoR dat. This closure now covers both the CODE half (this
paragraph) and the AUTHORED-DATA half (Batch E); no further hypothesis
remains unchecked.
## Presentation families (retail parity)