acdream/docs/research/2026-08-16-campaign-cc-gate-round1-findings.md
Erik ff8b1ebc89 docs: Campaign CC connected gate PASSED — campaign CLOSED user-accepted 2026-08-16
The extended gate round (GF-1..16, R2/R3/R4 re-tests, fix batches A-G +
closeout + two re-test rounds) closed with the user's pass on build
1.0.2-cc.o. Plan status and ledger flipped; findings doc carries the
full round history.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 19:13:36 +02:00

1166 lines
76 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Campaign CC connected gate — round 1 findings (2026-08-16)
## RE-TEST 2 (build `1.0.2-cc.k`, post-E/F/G/closeout) — findings R3-1..R3-8
Heritage PASSES. Remaining, with retail side-by-side screenshots:
- **R3-1 Profession: "Coordination" wraps to two lines** on the attribute
label — retail fits one line. Likely the Batch E block-label wrap (or a
wrong/too-large font on the runtime-written `0x100002ED` labels).
Probe the authored font+rect: if the authored font fits, our font
resolution is wrong; if not, retail doesn't word-wrap captions and the
auto-wrap is the bug.
- **R3-2 Skills: "Available Skill Credits" wraps after "Available"**,
hiding behind adjacent graphics — retail is ONE line. Same family as
R3-1 (the Batch E caption-width confinement + wrap).
- **R3-3 Skills info box: the title line ("Item Enchantment (10)")
overlaps the description text** — description must start on the line
below the title.
- **R3-4 SCROLLBAR THUMB MISSING EVERYWHERE (shared mechanism):** skills
list scrollbar and summary scrollbars show track+arrows but no thumb
(retail: the red/gold diamond); the shade slider on the color disc
works but shows no indicator (retail: the small handle across the
disc). One probe: what authors the thumb (a child? state media on the
scrollbar?) and why our UiScrollbar never draws it on these chargen
scrollbars while (per OP-era gates) other scrollbars show thumbs.
- **R3-5 Appearance color wheel render targets:** (a) we tint the RING
art — retail fills the small circle INSIDE the ring (the spot); (b)
beyond-count swatches: retail SHOWS them as blocked/dark circles — we
hide them; (c) the shade indicator on the disc (see R3-4).
- **R3-6 Eyes: retail shows a graphic icon in the disc center** (the
authored eye plug art) and its swatches still show colored rings;
acdream shows an empty ring and dark swatches. Re-derive DoGradDisk's
Eyes branch + the eye swatch rendering.
- **R3-7 Summary scrollbar thumbs** — R3-4 family.
- **R3-8 `[ Name ]` (user re-asserts, third time):** P0x17 and state
text are probed-absent; retail code writes none. UNCHECKED: the
field-widget-specific properties — dump EVERY authored property id +
value on `0x10000402` raw, and check `UIElement_TextInput::OnSetAttribute`'s
full case list for prompt/default-text properties beyond 0x17. If that
is also empty, STOP and request a live retail screenshot of the field
before any further work.
**RE-TEST 2 fix batch (2026-08-16, R3-1..R3-8) is CODE-COMPLETE, pending the
user's visual gate.** All eight findings investigated and fixed except R3-8
(genuinely no dat-authored placeholder mechanism exists — see its own
disposition below). App suite live-DAT env 5358/3 → 5372/3 (+14, zero
regressions); Runtime 1735/0 unchanged (untouched this round); full
solution 14578 tests / 4 skips / 0 failures (0 Core/Content changes this
round, so those suites are unaffected by construction, not merely by
measurement). No client launches.
- **R3-1/R3-2 FIXED.** Root cause: Batch E's `UiButton.DrawBlockLabel`/
`WrapBlockLines` auto-wrapped ANY caption that didn't fit its box width —
live-DAT-probed, this is the WRONG mechanism. `UIElement_Text::
CalcJustification @0x00467260` (shared by the horizontal/vertical
branches) shows retail's real per-glyph break decision — BOTH the
width-triggered wrap AND the explicit-newline break — sits behind ONE
gate keyed on the `OneLine` flag passed into `GlyphList::Recalculate`;
nothing in the decomp confines a caption's wrap width to a SIBLING
element's rect (the ValueBox confinement Batch E added for the
coexisting-value-label shape). Live-DAT evidence: the Coordination
attribute-slider label (`0x100002ed`) authors `OneLine=true` (so it
should never wrap, regardless of width — decomp-confirmed, not just
measurement); the Skills credits button's "Available Skill Credits"
caption measures 193px against its OWN full 231px button width (fits
comfortably) — the 113px confined width Batch E fed into the wrap
decision was never a real retail quantity. Fixed by making
`UiButton.WrapBlockLines` split ONLY on the explicit (already-
normalized) `\n` — never width-based — a strict superset of the
pre-Batch-E single-line draw for every already-correct caption, and the
exact "Attribute\n Credits" authored-break shape still works unchanged.
The `ValueBox` confinement computation itself STAYS in
`UiButton.OnDraw` (still feeds the Center-alignment tx formula and the
now-unreachable-for-wrap clip rect for the rare multi-line+ValueBox
case) — deliberately not deleted, since it's harmless now that nothing
reads it for the wrap decision, and removing it would be unrelated
scope. **Known residual risk, NOT re-solved here:** the original
Batch E root-cause diagnosis for R2-2/R2-3 ("24dits"/"Credit0Credits")
was that the caption's own unconfined single-line render visually
overlapped the coexisting value label's rect (live-DAT-measured: the
Skills credits caption's rendered span reaches x≈196, the value box
starts at local x=116). Removing the WRAP does not reintroduce a
confinement CLIP either (deliberately — see `DrawBlockLabel`'s own doc
for why inventing a new clip boundary here would be exactly the
guessing this project forbids), so this specific button's caption and
value MAY visually overlap again in the live client. This was NOT
something the current re-test (R3-2) flagged as broken — it only
reported the wrap — so no action was taken beyond documenting the risk
for the user's own re-check.
Files: `src/AcDream.App/UI/UiButton.cs` (`DrawBlockLabel`,
`WrapBlockLines`, `OnDraw`'s Label block). Tests: `UiButtonTests.cs`
(`WrapBlockLines_LongSingleParagraph_NeverWordWraps` replaces the retired
Batch E word-wrap expectation; new
`WrapBlockLines_SingleWordNarrowerThanBoxButWiderThanOffsetAdjustedWidth_StaysOneLine`);
2 new live-DAT pins in `CharacterCreationLiveDatTests.cs`
(`CoordinationAttributeLabel_AuthorsOneLineTrue`,
`SkillsCreditsButton_CaptionFitsFullWidth_ValueChildStartsAtMidpoint`).
- **R3-3 FIXED.** Root cause: the title (`0x100003fb`, Y=435 H=100) and
description (`0x100003fc`, Y=460 H=100) panes' own AUTHORED boxes
overlap by 75px, live-DAT-measured — retail relies on VERTICAL
JUSTIFICATION, not disjoint rects, to keep them visually separate.
Neither pane authors dat property `0x15` (live-DAT-confirmed absent on
both), so both fall to this port's shared unauthored-VJustify default —
currently `Center`. Byte-tracing retail's real ctor default
(`UIElement_Text::UIElement_Text @0x004685ff`,
`m_eVerticalJustification = 4`) against `UIElement_Text::
CalcJustification @0x00467260`'s ACTUAL enum semantics (`ecx_5==1`
Center; `ecx_5==3||5` → the far edge/Bottom; anything else, INCLUDING
the ctor's own default of 4 → the near edge/Top) shows the correct
unauthored default is **Top, not Center** — a genuine, client-wide
enum-mapping bug in this port (`ElementReader.cs`'s import-time switch,
`DatWidgetFactory.cs`'s build-time switch, and `ElementInfo.VJustify`'s
field default all currently resolve an absent `0x15` to Center). Under
the CORRECT Top default both panes render near their own box's TOP edge
(25px apart — no collision); under the current Center default both
cluster toward the middle of their overlapping boxes (collision).
**Scoped fix, not the systemic one:** `CharacterCreationSkillsPage`'s
constructor now force-sets `VerticalJustify = VJustify.Top` on both
panes directly, rather than fixing the shared mapping/default. The
shared bug is CLIENT-WIDE (every DAT-imported `UiText` reaching the
`Centered`/`RightAligned`/`OneLine` static paths or the multi-line
honored-justification path) and could regress already-shipped, visually
-verified, FROZEN surfaces (vitals numbers, chat, main game UI, Options
panel) that may rely on the CURRENT Center default — fixing it properly
needs its own dedicated investigation + full regression sweep, filed as
**ISSUES.md #410** and register **AD-104**.
Files: `src/AcDream.App/UI/Layout/CharacterCreationSkillsPage.cs`
(constructor). Tests: new
`CharacterCreationUiControllerTests.SkillsPage_InfoBoxPanes_ForceTopVerticalJustify_ToAvoidTitleDescriptionOverlap`
(fixture); new live-DAT
`CharacterCreationLiveDatTests.SkillsInfoBoxTitleAndDescription_AuthoredBoxesOverlap`
(pins the overlap premise itself, so a future DAT re-extract that makes
the boxes genuinely disjoint is visible).
- **R3-4/R3-7 FIXED (single shared mechanism, confirmed).** Root cause:
retail authors TWO DISTINCT `UIElement_Scrollbar` thumb shapes.
`DatWidgetFactory.BuildScrollbar`'s existing vertical-thumb detection was
built against chat's own scrollbar (`0x10000012`) — a 3-slice composite
where the thumb CHILD carries no media of its own and three Type-3
grandchildren supply the top-cap/middle/bottom-cap sprites. The chargen
Skills listbox scrollbar (`0x100003f8`), Summary's OVERVIEW listbox
scrollbar (`0x10000401`), the Summary how-to box's scrollbar
(`0x100002e7`), AND the shade slider (`0x10000321`) all instead author a
SIMPLE single-sprite thumb: the same structural child (Type 1, id 1, not
the inc/dec button) carries its OWN direct Normal/Normal_rollover/
Normal_pressed (or, for the shade slider, a single DirectState) media
and has ZERO children — the 3-slice-only search found nothing for this
shape, so every `Thumb*Sprite` stayed 0 regardless of overflow. Fixed by
falling back to the thumb's own `DefaultImage` when the slice search
finds nothing — additive; a thumb WITH real slice children (chat) is
unaffected. This ONE fix covers R3-4's three listbox thumbs AND R3-7
AND, as a natural consequence (same code path, same structural shape),
the shade-slider indicator half of R3-5(c) — no separate fix was needed
for the shade slider.
**Process note:** the ORIGINAL live-DAT probe for this investigation
mis-reported the shade slider's own thumb child (and, separately, the
swatch/gradCircle elements investigated for R3-5/R3-6) as authoring
"zero media" — a `string.Join(",", StateMedia.Keys)` display artifact
(a single `""` DirectState key joins to an EMPTY STRING, indistinguishable
from zero entries in a printed diagnostic — not a code defect, a
diagnostic-only mistake caught and corrected mid-investigation by
re-probing with an exact `.Count`/dictionary-content check instead of a
joined string).
Files: `src/AcDream.App/UI/Layout/DatWidgetFactory.cs`
(`BuildScrollbar`). Tests: new `DatWidgetFactoryTests.cs`
(`Type11_VerticalScrollbar_SingleSpriteThumbWithNoSliceChildren_SetsThumbSprite`,
`Type11_VerticalScrollbar_ThumbWithSliceChildren_StillUsesSliceMedia`
negative companion); 3 new live-DAT pins in
`CharacterCreationLiveDatTests.cs`
(`SkillsListboxScrollbar_SingleSpriteThumbShape_BuildsWithNonZeroThumbSprite`,
`ShadeSlider_ThumbAuthorsItsOwnDirectStateSprite_BuildsWithNonZeroThumbSprite`);
existing `ChatFixture_ScrollbarImportsInheritedMediaRoles` (3-slice shape)
re-verified unaffected.
- **R3-5/R3-6 CODE-COMPLETE.** Root cause, re-derived from
`gmCGAppearancePage::DoColorSpots @0x0047d850` and `DoGradDisk
@0x0047da90`: retail does NOT multiply-tint the swatch/grad-circle's
authored sprite. It builds a FRESH composited surface once
(`CreateLocalSurface` + `Blit`), then calls `SurfaceWindow::ReplaceColor`
against old-color `RGBAColor(0,0,0,1)` (opaque black — the spot
template's own placeholder fill, live-DAT-PIXEL-confirmed: the
37x44 "spot" resource has a genuine solid-black CENTER region and a
genuine non-black RING/border region) — swapping every EXACT opaque-black
pixel for the swatch's real color while leaving the ring untouched. A
multiply-tint (Batch G's mechanism) is architecturally wrong here: black
multiplied by ANY color stays black (never recolors the center at all),
and multiplying the ring's own non-black pixels shifts their hue,
corrupting them — exactly the reported "we tint the ring" symptom.
Beyond-count swatches (R3-5b) use a COMPLETELY DIFFERENT authored
resource (enum `0x1000000f`, "blank" — live-DAT-pixel-confirmed almost
no black pixels at all, i.e. genuinely different art, not "the spot with
its center left un-recolored") shown UNTINTED — and retail's own
`pColor->SetVisible(1)` is UNCONDITIONAL for all 9 swatches (never
hidden, only the CONTENT differs). For Eyes (R3-6), `DoGradDisk`'s Eyes
branch (`arg2=1`) blits the "grad plug" icon (enum `0x10000010`)
UNTINTED (`Blit_Normal`, no color argument at all) — and, re-reading
`SetSelection @0x0047e260`'s own Eyes/non-Eyes tail
(`@0x0047e859-0047e878`), NEITHER branch ever calls
`m_pGradCircle->SetVisible` — only `DoGradDisk` (swaps the source image)
and `m_pShadeScroll->SetVisible` (a DIFFERENT element) are touched, so
the disc itself is NEVER hidden for Eyes — a correction to this port's
prior `_gradCircle.Visible = !isEyes` line (and its own now-renamed
test).
**Mechanism ported faithfully** via a new `ChargenColorSpotComposer`
(CPU-side decode-once + per-color bake-and-cache-once, uploaded through
the existing `TextureCache.UploadRgba8` seam — the SAME "decode,
recolor by exact-match, upload, cache" shape `AcDream.App.UI.
IconComposer.GetSpellComponentIcon` already established for item icons,
just matching black instead of white) and a new opt-in
`UiButton.ColorKeyFaceResolver`/reuse of the EXISTING
`UiDatElement.RuntimeImageTexture` seam — both additive, zero behavior
change for any element that doesn't set them. `Tint` itself is
UNCHANGED in meaning (still the "this swatch's color is X" signal every
existing test reads) for the swatches; the grad circle's own Tint STAYS
a genuine multiply for the non-Eyes case, matching retail's OWN
`Blit_Multiply` there (the ONE place a multiply tint is actually
correct). Wired as a fourth late-bound composition seam
(`SwatchTextureSource`), same pattern and same site as the existing
three color-computation seams (`PalSetSource`/`ClothingTableSource`/
`PaletteColorSource`), constructed in `LivePresentationComposition.cs`
once `TextureCache` exists.
**STOPPED item, same shape as Batch G's own two STOPPED items:** this
is CODE-COMPLETE and unit/live-DAT-tested (including a real pixel-level
proof that the spot template genuinely has a black center + non-black
ring, and that the blank template genuinely doesn't), but has NOT been
visually verified in the live client this round (no client launches per
this batch's own constraint) — the user's connected gate is owed.
Files: `src/AcDream.App/UI/Layout/ChargenColorSpotComposer.cs` (new),
`src/AcDream.App/UI/UiButton.cs` (`ColorKeyFaceResolver`, `OnDraw`),
`src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs`
(`SwatchTextureSource`, `BuildSwatchTextureResolver`, the swatch/
gradCircle refresh block), `src/AcDream.App/UI/Layout/
CharacterCreationUiController.cs` + `src/AcDream.App/UI/
RetailUiRuntime.cs` (pass-through seam), `src/AcDream.App/Composition/
LivePresentationComposition.cs` (composition-root wiring). Tests: new
`ChargenColorSpotComposerTests.cs` (3 pure byte-level tests for the
exact-match recolor), 4 live-DAT tests in `CharacterCreationLiveDatTests.cs`
(`ColorSpotAndGradDiskResources_ResolveToExpectedDimensions`,
`SpotTemplate_HasBlackCenterAndNonBlackRing_BlankTemplateHasNeitherBlack`),
8 `CharacterCreationAppearancePageSwatchColorTests.cs` assertions updated
(`Visible` now always true; `EyesPart_GradientDiscStaysHiddenAndUntinted`
renamed+rewritten to `EyesPart_GradientDiscStaysVisibleButUntinted_ShowsPlugIconInstead`).
- **R3-8 DISPOSITION: genuinely nothing authored — no code change, per
this batch's own contract ("DO NOT invent a placeholder").** Dumped
EVERY property present on `0x10000402` (not just P0x17) across every
state, cross-referenced against `UIElement_Text::OnSetAttribute`'s
COMPLETE case list (there is no `UIElement_TextInput` class in retail —
the name field is a plain `UIElement_Text`/`m_filter`-bearing field,
`DynamicCast(0xc)`-confirmed in `gmCGSummaryPage::InitializePage`; the
task's own reference to that class name doesn't exist in the named
decomp). The full recognized-property space (every id `0x14`-`0x29`
plus the sparse high ids `0xC7`/`0xCB`/`0xCC` for
TruncateTextToFit/LoseFocusOnEscape/LoseFocusOnAcceptInput) has NO
mechanism for a placeholder/prompt string independent of the committed
P0x17 caption. The BaseElement/prototype-inheritance hypothesis is ALSO
ruled out — not by assumption, but because the EXISTING regression test
(`SummaryNameField_AuthorsNoP0x17OnAnyState`) already probes the FULLY
MERGED `ElementInfo` (post `LayoutImporter`'s BaseElement resolution,
confirmed by reading `ElementReader.Merge`/`UiStateInfo.Merge`'s own
"derived overrides, else inherit base" property-bag semantics) and finds
no P0x17 anywhere. The live dump found exactly ONE StringInfo-kind
property on the whole element: **`0x49`, resolving to "Your name can be
32 characters long and cannot contain numbers or symbols."** — but
`0x49` is part of the SAME five-property tooltip family ISSUES.md
#409/GF-16 already documents client-wide (`0x47` tooltip behavior
enum = `0x10000487`, `0x48` the tooltip popup LayoutDesc DID =
`0x21000041` — the EXACT DID #409 cites, `0x49` the tooltip TEXT,
`0x4B` tooltip-enabled = true) — a HOVER TOOLTIP describing naming
rules, not an in-field placeholder, and #409's tooltip system is
unshipped so this text is authored but never shown anywhere yet. The
field's 8 children are the SAME gold-frame family (`0x100002DE-E3`/
`0x100000E8`/`0xEA`) GF-12 already renders — reinforcing GF-15's
existing hypothesis that the `[ Name` the user perceives is this frame's
own bracket-style chrome around an empty box, not text content.
**Batch A's closure and Batch E's re-check both stand; this is the
THIRD independent negative result on the same question via three
different mechanisms (retail code, DAT P0x17, now the complete property
space + inheritance chain).** No further code-side avenue remains — the
lead should request a live retail screenshot of the field per the
batch's own contract before any further investigation.
Test: `CharacterCreationLiveDatTests.SummaryNameField_AuthorsNoP0x17OnAnyState`
extended with the exhaustive `0x49`-is-the-tooltip assertion (a
regression pin, not just a probe finding).
## ROUND 1 RE-TEST (build `1.0.2-cc.i`, post-Batches B/C/D) — findings R2-1..R2-8
User's second visual pass with retail side-by-side screenshots (heritage
description, profession template text, "Attribute\n Credits" overlap,
skills credits overlap, retail skill-info box, retail Skills page, the
acdream GradCircle vs retail's color wheel).
- **R2-1 (COMMON, regression from Batch C): description-box text
misaligned LEFT, clipping outside the frame** on Heritage, Profession,
Appearance, Town, Summary — first characters cut ("rained Starting
Skills", "OW HUNTERS", "ump, Loyalty"). Pre-Batch-C (cc.e screenshots)
the text started INSIDE the box. One shared cause suspected: the
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. — **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. — **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. — **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
("Loyalty (5) / Affects experience given to your patron... / Training
Bonus +5 / Formula :") — the info panes `0x100003FB/FC` are still
never written (investigation root-1c, missed by Batch C's scope);
(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. — 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. **CODE-COMPLETE at Batch G (2026-08-16),
register AP-216/AP-217 rewritten (not retired — see their own rows):**
the retail mechanism (`gmCGAppearancePage::DoColorSpots @0x0047d850` /
`SetSelection @0x0047e260` / `DoGradDisk @0x0047da90`) is fully re-derived
and ported — a new pure Core resolver
(`AcDream.Core.CharGen.ChargenSwatchColorResolver`) computes each of the
nine swatches' representative RGB (PalSet-averaged for Hair/Nose+Mouth+
Skin/Headgear/Shirt/Trousers/Footwear at retail's own fixed sample
indices `0xd0`/`0xb0`/`0x520`, direct-Palette for Eyes at `0x103`) backed
by a new `ChargenAppearanceCatalog.TryGetColor` reading real Palette dat
objects, pinned against the installed EoR dat
(`ChargenAppearanceCatalogColorTests` — e.g. Aluvian male's shared skin
PalSet measures a plausible flesh-tone RGB(182,148,118)).
`CharacterCreationAppearancePage` recomputes all nine swatches + the
gradient disc's tint on every refresh (part change / color change /
heritage change, `CharacterCreationAppearancePageSwatchColorTests`), and
paints them through a new `ChargenSwatchColorTile` overlay element.
**Two STOPPED items block this from being visually live**, both outside
Batch G's file contract: (1) the new `PalSetSource`/`ClothingTableSource`/
`PaletteColorSource` late-bound seams (mirroring the existing
`PreviewControl` pattern) are never assigned by the composition root
(`CharacterCreationUiController.cs`) — until wired, the mechanism stays
fully inert, matching PRE-Batch-G behavior exactly; (2) the rendering
primitive is a flat-color-fill approximation of retail's actual
recolored-sprite blit — neither `UiButton` (sealed) nor `UiDatElement`
exposes a per-instance sprite `Tint`, though the retained-UI sprite
pipeline's `DrawSprite` already carries the `Vector4 tint` multiply
retail's own `Blit_Multiply` needs; adding that property is a small,
precisely-specified addition to those two shared widget files for the
lead to sequence. Nose/Mouth/Skin (retail's own non-interactive single
representative swatch, `SetSelection`'s hard-coded `var_1e0 = 1`) is ALSO
ported, beyond AP-216/AP-217's original six-part scope. Tests: 11 new
Core (`ChargenSwatchColorResolverTests`), 6 new Content live-DAT
(`ChargenAppearanceCatalogColorTests`), 8 new App-layer fixture
(`CharacterCreationAppearancePageSwatchColorTests`) — App suite
5321/3 -> 5329/3, Runtime 1735/0 unchanged, zero regressions.
- **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").** —
**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). —
**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 —
launcher → character select → Create → six pages → name → Finish → real
character created. USER-CONFIRMED: "Yes i could now create a char." The
create flow's core path is live; the round continues for visual parity
(Batches B-D) and the remaining script checks (rejection dialogs,
log-straight-in confirmation, credit/randomize/exit warnings).**
**Batch B (selection state media + label state) is CODE-COMPLETE
2026-08-16, pending the user's visual gate.** GF-1, GF-8, GF-9, GF-10,
GF-11b, and GF-11c are fixed — see each entry's own FIXED note below.
Fixture + live-DAT tests only this round (no graphical client launch);
App suite 5282/3 (was 5266/3), Runtime 1735/0 unchanged. Register:
AP-222 RETIRED, AP-215 NARROWED (item 1 retired, item 2 stays open).
**Batch C (text/frame/label fidelity, the largest visual batch) is
CODE-COMPLETE 2026-08-16, pending the user's visual gate.** GF-2, GF-3,
GF-4, GF-6, GF-11a, GF-12, and GF-14's text half are fixed — see each
entry's own FIXED note below. Three commits: (1) chargen-scoped rich
text + labels + backdrops (new shared `DatRichText` composer; `UiButton`
gains a coexisting `ValueLabel` slot; Heritage/Profession backdrop
`SetState` cascades; AP-216/AP-217 partially closed — the "beyond count"/
"Eyes-blank" halves ship, the palette-to-RGB "actual color"/"gradient
tint" halves stay open, judged disproportionate to add alongside this
batch's ~10 other fixes), (2) a CLIENT-WIDE `LayoutImporter` fix
un-consuming media-bearing dat children on `UiText`/`UiField` (37 distinct
(layout, element) pairs across 15 layouts, independently re-derived —
includes MAIN GAME UI and the chat transcript, closing the build half of pre-filed
issue #366), (3) the Summary how-to text
(`gmCGSummaryPage::SetHowToText`) plus the scrollbar-to-text-scroll
linkage Commit 2 left unbound. Fixture + live-DAT tests only (no
graphical client launch); App suite 5307/3 (was 5282/3, +25 tests, one
pre-existing full-suite-only allocation flake unrelated to this batch —
passes in isolation and in the full Release run), Runtime 1735/0
unchanged. Register: AP-215/AP-216/AP-217 rewritten, AP-218 retired,
AD-103 retired. **FLAG FOR THE LEAD: chat and the main game UI both
render new dat children (gold frames, an unseen-text indicator) for the
first time this batch — the user's own visual check of both is owed
before considering Commit 2 closed; automated coverage cannot catch a
purely visual placement regression.**
**Batch D (chargen 3D preview backdrop) is CODE-COMPLETE 2026-08-16, pending
the user's visual gate.** GF-7 and GF-14 are fixed — see each entry's own
FIXED note below. GF-16 (client-wide tooltips) was investigated in the same
root-cause pass but is explicitly out of this batch's scope — deferred as
`docs/ISSUES.md` #409 with its own decomp anchors. Fixture + live-DAT tests
only this round (no graphical client launch); App suite went from 5307/3 to
5321/3 (+14, zero regressions), Runtime 1735/0 unchanged. Both Launcher test
projects (this being the first build of the merged tree carrying the #406
launcher merge) pass at their own baselines: Launcher.Core.Tests 337/0,
Launcher.Tests 67/0. Blast radius: `PrivateEntityViewportRenderer` (shared
with paperdoll and creature-appraisal) gained an OPTIONAL second entity
slot reserved via a `backdropRenderId` constructor parameter — paperdoll
and creature-appraisal never pass one, so their draw stays single-entity
by construction (`SetBackdrop` throws if called without a reserved slot,
and the entity-list-assembly helper `BuildDrawEntities` degrades to
exactly the main entity whenever no backdrop is configured/set).
**Batch F (Skills page completion — R2-4 + review F1/F2) is CODE-COMPLETE
2026-08-16, pending the user's visual gate.** Four of R2-4's five
sub-items are fixed; R2-4b (the four-bucket sorted model) is NOT — see the
AP-213 register row for the exact missing data channel this batch's
investigation pinned down (`SkillBase.MinLevel`, confirmed present in the
installed dat but not threaded through `ChargenOptions`/
`CharacterCreationRuntimeBindings`). **Fixed:** R2-4a (row selection — a
row click, or an arrow click matching retail's own post-Increase/
DecreaseSkillLevel re-select, highlights the row and writes the info
panes' TITLE — name + score — and a level-gated bonus line; the
description/formula halves stay unported for the SAME missing-data reason
as R2-4b, documented on `CharacterCreationSkillsPage.RefreshInfoBox`'s own
doc rather than a new register row since no file outside the page's own
scope was needed to identify it); R2-4c (the listbox's own authored
scrollbar link, live-DAT-CONFIRMED at `0x100003F8` — exactly this batch's
own "+1 from the listbox" hypothesis — wired to the listbox's `Scroll`
model, the ordinary page-level linkage every other `UiTemplateListBox`
owner uses); review F1 (the Untrained-down/Specialized-up literal `"0"`
cost text the prior port rendered blank, and the exact per-branch 999-blank
gate — up-cost only, never down-cost); review F2 (the
`pSkillUpButton`/`pSkillDownButton` Ghosted/Enabled state pair, gated on
credits and a re-derived `bUntrainable`/`bUnspecializable` — the row's own
effective cost being nonzero — using cost data the page already resolves,
no new channel needed). Fixture + one live-DAT test this round (no
graphical client launch); App suite live-DAT env went from 5321/3 to
5328/3 (+7, zero regressions — one pre-existing baseline flake, the
streaming "injected dungeon enqueue failure" test, is a known standalone-
pass-only flake unrelated to this batch and did not reproduce on the full
post-fix run), Runtime 1735/0 unchanged.
User ran the six-page chargen flow live (build `1.0.2-cc.e`, RDP session,
windowed). Screenshots: retail Heritage, acdream Heritage, retail
Profession. The user's side-by-side retail reports are AXIOMS
(`feedback_retail_oracle_no_whack_a_mole`). Pre-page findings #405 (fixed
`344d88bf`), #406 (open), #407 (fixed `e601a496`) are recorded in
ISSUES.md; this doc is the six-page batch.
## Functional (blocking or behavior-dead)
- **GF-1 Heritage selection dead/unmarked — FIXED (Campaign CC gate round
1, Batch B).** Root cause: retail authors a custom radio-selection state
pair (`RetailUiStateIds.Unselected`/`Selected`, `0x10000016`/
`0x10000017`) on the heritage row (property-only state descriptors, no
media) with the actual art on a single stateful CHILD (the dot,
`0x100003C0`, media `0x06006E35`/`0x06006E21`, live-DAT-probe-confirmed).
`UiButton.AddAvailableStates` only recognized the standard Normal/
Highlight/Ghosted name space, so `_availableStates` never admitted the
custom pair and `.Selected` committed nothing (probe-verified before the
fix: `Selected=true` left `ActiveState=="Unselected"`, while the raw
`TrySetRetailState(0x10000017)` already worked). Fixed by teaching
`UiButton` to detect the authored pair (`HasStateMedia("Unselected") &&
HasStateMedia("Selected")`) at construction and bypass the standard
state machine for it — `.Selected` now routes directly to
`RetailUiStateIds.Selected`/`Unselected`, additive and gated on the
pair's presence, so every OTHER button's Normal/Highlight path is
byte-identical. The SAME fix also lights the Profession template icon
(`0x100003D9`), the Appearance Face/Clothes sub-tabs (GF-8, below), and
the gender buttons (whose media lives directly on the button, not a
child — the OTHER shape this fix covers). The open-roll's own
no-lit-dot-on-entry symptom shares this same root: `CharacterCreationHeritagePage.Refresh`
already sets `button.Selected = heritageId == snapshot.HeritageId` for
every row on every refresh (including the first one after open), so the
same `.Selected`-was-a-no-op bug silently ate the initial roll's own dot
too — this fix closes both halves of GF-1 with the same change.
- **GF-5 Skills page empty — FIXED (Campaign CC gate round
1, Batch A).** Root cause was `CharacterCreationSkillsPage.RebuildRows`
resolving `Templates[0]` (retail's own 3-child bucket-HEADER row,
`0x100002F4`) instead of `Templates[1]` (the REAL skill row,
`0x100002FF`, live-DAT-probe-confirmed 7 children) and requiring the
resolved root to be a `UiButton` (it's a plain container). Byte-traced
against `gmCGSkillsPage::DoSkillRecords @0x004817e0` +
`tagSkillRecord`'s copy-constructor field order to map every child id:
name (`0x10000301`), `pSkillLevelText` (`0x10000302`), `pUpCostText`
(`0x10000303`), `pSkillUpButton` (`0x10000304`), `pSkillDownButton`
(`0x10000305`), `pDownCostText` (`0x10000306`). Fixed to resolve
`Templates[1]`, wire the real per-row up/down arrow buttons to
`ListenToElementMessage @0x004814c0`'s own plain-click dispatch
(`IncreaseSkillLevel`/`DecreaseSkillLevel`), retiring AP-213's click-to-
advance/double-click-retreat single-button substitution (narrowed, not
fully retired — the flat-list-vs-four-bucket half stays). The credits-
caption clobber (`SkillsPage.cs:81-82`, now different line numbers) is
UNCHANGED — Batch C's scope.
- **GF-9 Appearance color swatches do nothing observable — FIXED (Campaign
CC gate round 1, Batch B).** Root cause confirmed as working-but-
invisible, not a dead dispatch: the `SelectColor`/`SetAppearanceIndex`
click path was already intact end to end (unchanged by this fix). The
swatch buttons themselves author ONLY an unnamed DirectState sprite —
live-DAT-probe-confirmed NO Normal/Highlight media at all — so the
existing `swatch.Selected = ...` highlight assignment in
`RefreshColorAndShadeControls` was a permanent no-op; nothing could ever
have shown a click's effect. Retail's REAL feedback mechanism is nine
separate companion overlay elements (`0x10000318`-`0x10000320`,
`CharacterCreationAppearancePage.SwatchOverlayIds`, live-DAT-confirmed
siblings of the swatches under the color-wheel container `0x100003B9`,
index-paired 1:1 with `SwatchIds`) that retail's `SetColor @0x0047DD50`
shows/hides via `m_tColorWheel[...][0x10][iCurColor*7]->SetVisible`
cross-confirmed against `gmCGAppearancePage::InitializePage`'s own
swatch/overlay id-pair table (`@0x004800ff-00480164`). Fixed by wiring
exactly one overlay visible per part, tracking the current part's
selected color index; retires AP-215's swatch-selection substitution
(item 1 — the icon-vs-ordinal item 2 stays open).
- **GF-11a Town description text does not change when switching towns —
FIXED (Batch C, Commit 1).** The composed string
(`gmCGTownPage::SetTownString @0x0047c1f0`'s `howTo + "\n\n" + townText +
"\n"` — this ONE composition site is where retail's OWN code, not the
authored DAT string content, inserts the separator, confirmed via the
compiled format literal's raw bytes `u"\n\n%s\n"`) was already
byte-correct; the real gap was rendering it as a single un-wrapped line,
so the town-specific SUFFIX rendered past the clipped viewport —
switching towns changed the underlying string but not what was visibly
on screen. Fixed by routing through `DatRichText.Compose` (same fix
family as GF-2).
- **GF-13 Summary shows "-Non-admin or Non-envoy" below the name — FIXED
(Campaign CC gate round 1, Batch A) — this commit.** Root cause: dat
property `0x3B` (Invisible — `UIElement::OnSetAttribute @0x00462d80`
case 8) was never read by the importer at all; elements `0x10000403`
("Non-Admin") and `0x10000494` ("Non-Envoy") both author it `true`
(live-DAT-probe-confirmed, path `0x100003CC > 0x100003D0 > 0x100003D6 >
{0x10000403,0x10000494}`). Blast-radius sweep found **1,083 elements
client-wide** author the same flag — a blanket importer-wide honor is
its own visual gate, filed as ISSUES.md #408. This fix is CHARGEN-SCOPED
ONLY: `ElementInfo.Invisible`/`UiElement.AuthoredInvisible` are pure
data additions (read/stored everywhere, acted on nowhere by the shared
importer path), and `CharacterCreationUiController.HideAuthoredInvisibleElements`
walks its own mounted subtree once at construction and hides whatever
the dat itself marked hidden — by the authored flag, not a hardcoded id
list. Register AP-230 records the scoped-vs-general split.
- **GF-15 Summary name entry DEAD + Finish unpressable — FIXED
(Campaign CC gate round 1, Batch A) — this commit, LIVE-VERIFIED end to
end.** The live-repro investigation (offline `ACDREAM_OPEN_CHARGEN=1`
alone does NOT open the chargen screen — `RuntimeCharacterCreationState`
only activates via `LiveSessionController.StartAsync`'s authenticated-
connect path, `LiveSessionController.cs:791`; the repro required a real
connect to the project's own local ACE test server) showed the FIRST
click into the name field correctly focuses it and typing correctly
lands characters — the modal/pick/focus mechanics the earlier static
investigation examined were never broken. The REAL cause only surfaces
after the FIRST dialog opens: pressing Finish with an empty name
successfully creates the NoName `RetailMessageDialogView`
(`visible=true`, live-DAT-probe-confirmed nonzero popup/message/button
geometry — 400x95 popup, correctly centered) but it renders NOTHING and
silently absorbs every subsequent click across the WHOLE canvas,
including clicks aimed at the name field or Finish button underneath.
Root cause: `CharacterCreationUiController.Tick()` (and
`CharacterManagementUiController.Tick()`) call `UiRoot.BringToFront(Root)`
UNCONDITIONALLY every frame while their screen is open (needed so
chargen stays above the occluded character-management screen
underneath, register AP-229); a dialog's root is a direct sibling of
those screen roots under the same `UiRoot`, and
`RetailWindowManager.BringToFront` is a simple "highest ZOrder among
siblings + 1" — whichever sibling's own `BringToFront` call runs LAST in
a frame wins. `RetailDialogFactory.Tick()` never re-asserted its own
open dialogs' z-order, so the VERY NEXT frame's screen `Tick()` (which
always runs before the dialog factory's own `Tick()` in
`RetailUiRuntime.Tick(double)`'s per-frame sequence) silently buried the
dialog behind the screen's opaque backdrop — while the dialog remained
the registered `UiRoot.Modal` and kept EXCLUSIVE input priority
(`OnMouseDown`'s Modal-vs-bounds gate is independent of render/z-order).
Fixed by having `RetailDialogFactory.Tick()` re-raise every open dialog
(in `_openOrder`, so the most recently opened stays topmost) every tick,
matching retail's real always-on-top dialog behavior. Live-verified the
COMPLETE user sequence after the fix: click name field (focuses), type
(lands), press Finish empty (NoName dialog now VISIBLY renders: "You
must enter a name for this character!"), click OK (dismisses cleanly,
`Modal` clears), click the field again (still focusable/typable). The
`[ Name` prefill question is CLOSED, not a bug: byte-verified neither
`CharGenState::RandomizeCharacter @0x005c6d80` nor
`gmCGSummaryPage::InitializePage @0x0047bbf0` ever write text into the
name field (`InitializePage` only sets the input filter) — retail's
field is genuinely code-empty on a freshly-rolled character, matching
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)
- **GF-2 Description textboxes broken everywhere — FIXED (Campaign CC gate
round 1, Batch C, Commit 1 + Commit 2).** Root cause was TWO stacked
gaps, both closed: (1) the Heritage/Town/Profession description pages
bypassed escape-normalize + word-wrap entirely, assigning a raw
unwrapped single-`Line` `LinesProvider` — fixed by routing every
description box through the new shared `DatRichText.Compose` helper
(ports `UIElement_Text::SetStringInfoWithFont`/`AppendStringInfoWithFont`'s
composition model: escape-normalize, per-segment word-wrap, per-segment
palette color — Heritage's own header/body segments now render in
retail's own green/white, matching `AppendStringInfoWithFont`'s font-index
argument). (2) The authored gold frame (8 pieces) and linked scrollbar
were silently dropped by `UiText.ConsumesDatChildren` — fixed by
Commit 2's client-wide `LayoutImporter` carve-out (see GF-12). Both
halves are pinned by live-DAT tests (`CharacterCreationLiveDatTests`,
`LayoutImporterMediaBearingChildSweepTests`) and unit tests
(`DatRichTextTests`).
- **GF-3 Profession template description textbox missing — FIXED (Batch C,
Commit 1).** `gmCGProfessionPage::InitializePage @0x00483068`'s
`m_pTextBox` (`0x100003e0`) was never bound. Fixed:
`CharacterCreationProfessionPage` now binds it and composes the
per-template string (`ID_CharGen_CustomText`/`BowText`/`SwashText`/
`LifeText`/`WarText`/`WayText`/`SoldierText`, `UpdateProfession
@0x004821b0`'s per-case literal, plain `SetStringInfo` — one color, no
palette) through the same `DatRichText` helper.
- **GF-4 Profession labels missing — FIXED (Batch C, Commit 1).** Two
distinct mechanisms, both closed: (a) the four display buttons
(avail/health/stamina/mana credits) author their caption directly as
their own P0x17 AND carry a separate media-less Type-12 value child that
`UiButton.ConsumesDatChildren` used to drop entirely — pages substituted
the button's own `.Label`, destroying the caption. Fixed by giving
`UiButton` a coexisting `ValueLabel`/`ValueBox`/`ValueFont`/`ValueColor`
slot (`DatWidgetFactory.BuildButton`, gated on the button's own P0x17
caption existing), so the caption and the live value now render
independently — the same fix also closes the Skills page's credits
badge. (b) The six slider name labels (`0x100002ed`, live-DAT-measured
as `UIElement_Button` — retail's `UIElement_Button` is DynamicCast(0xc)-
compatible with `UIElement_Text`) are now written once at construction
with `CharGenState::GetAttributeName @0x005C3A20`'s six hardcoded
literals, matching retail's own single `InitializePage`-time write
(never re-written on refresh, same as retail).
- **GF-6 Appearance spin captions are numbers — FIXED (Batch C, Commit 1),
retiring AP-218.** `gmCGAppearancePage::Update @0x0047e8f0` writes a
heritage-flavored STATIC caption to the Hair/Eyes/Skin spins only
(normal / `GearText_*` / `OlthoiText_*` variants) — never an index, and
never touches the other six spins' own DAT-authored caption at all.
Removed the prior 1-based-ordinal/gear-name substitution outright.
AP-215's own icon-thumbnail item (the four icon-only spins still show no
per-choice icon art — a DIFFERENT, still-open gap) is rewritten, not
retired — see that row.
- **GF-7 Preview backdrop black on Appearance (and Summary, GF-14) — FIXED
(Campaign CC gate round 1, Batch D).** Root cause: retail's
`gmCG3DView::Update @0x004EE9D0` (~0x004eecd3-0x004eed44) constructs a
SECOND `CPhysicsObj` from the current heritage's own
`HeritageGroup_CG.environmentSetupID` field (verbatim struct layout,
`acclient.h`) and adds it to the SAME viewport's `creature_mode_objects`
the player object lives in — this codebase already parsed that id as
`ChargenHeritageOptions.EnvironmentSetupId` (`ChargenTableReader.cs`)
but never consumed it anywhere. The decompiler elides the actual field
read (`var_b8`/`eax_32`, an unresolved-call artifact — see
`claude-memory/feedback_bn_decomp_field_names.md`); cross-referencing
`acclient.h`'s `HeritageGroup_CG` struct (environmentSetupID sits right
after setupID) confirmed what the elided value is. The backdrop object
gets NO explicit position/orientation/scale — `CPhysicsObj::makeObject`
(0x004eed2f) leaves it at the scene origin with identity orientation,
same as the player object's own default placement, and retail's own
`AddObject` insertion order puts the backdrop BEFORE the player (the
player's own re-`AddObject` happens later, at ~0x004ef199, after the
full clothing ObjDesc composes). Fixed by extending
`ChargenPreviewEntityBuilder` with `TryBuildBackdrop` (builds a plain,
unposed Setup mesh from the heritage's `EnvironmentSetupId`, returning
null for id 0/unset or an unresolvable Setup — matching retail's own
`if (eax_32 != INVALID_DID.id)` gate at 0x004eed29), giving
`PrivateEntityViewportRenderer` an optional second entity slot
(`SetBackdrop`, reserved via a `backdropRenderId` constructor param so
paperdoll/creature-appraisal — which never pass one — cannot acquire a
second entity even by accident), and wiring `ChargenPreviewController`
to rebuild the backdrop whenever the HERITAGE changes (narrower than the
existing camera-eye-reset gate, since `environmentSetupID` is a pure
function of heritage, never gender or appearance selection). Both
Appearance and Summary get the fix from the SAME `ChargenPreviewRenderer`
facade — no page-specific code needed, confirmed both pages call the
identical `gmCG3DView::Update` on their own separate `gmCG3DView`
instance. Lighting was independently re-verified against the same
function's `SetLight` call (`DISTANT_LIGHT, 2.0, (0.3, 1.9, 0.65)`
direction, default white color) and found to ALREADY match byte-for-byte
what CC6a shipped — no lighting change was needed.
- **GF-8 Appearance Face/Clothes sub-tab selection unmarked — FIXED
(Campaign CC gate round 1, Batch B).** Same root and same fix as GF-1:
the Face (`0x100003A9`)/Clothes (`0x100003AA`) sub-tab buttons author
the identical custom Unselected/Selected radio-pair shape (media on a
stateful icon child, `0x100002E9`, live-DAT-probe-confirmed) — not the
AP-222 family as originally suspected (AP-222 turned out to be a
DIFFERENT mechanism, the per-state label color/outline gap fixed
alongside GF-11b below). `UiButton`'s custom-selection-pair bypass
fixes both in one change.
- **GF-10 Zoom buttons show identical art — FIXED (Campaign CC gate round
1, Batch B).** Pure wiring gap, not a widget mechanism problem — both
zoom buttons already author a standard Normal/Highlight(/rollover) pair
(live-DAT-probe-confirmed). `gmCGAppearancePage::ZoomIn @0x0047CF00`
(`@0x0047d005/0x0047d00f`) ends `ZoomInButton->SetState(6)` (Highlight),
`ZoomOutButton->SetState(1)` (Normal); `ZoomOut @0x0047D050` mirrors.
`CharacterCreationAppearancePage`'s click handlers only ever called
`PreviewControl.ZoomIn()/ZoomOut()`, never touching either button's
state — fixed to set the mutual-exclusive pair on every click. Re-
derived the INITIAL state from `InitializePage @0x0047fdd0-0048032e`:
`m_bZoomedIn = 0` is set at construction, but NO explicit initial
`SetState` call exists for either zoom button anywhere in
`InitializePage` — both start at their DAT-authored "Normal" default
until the first real zoom click; this port does not force an initial
Highlight either.
- **GF-11b Town selected marker does not turn white — FIXED (Campaign CC
gate round 1, Batch B).** The marker PIN art itself already swapped
correctly (the town button's own Normal/Highlight state machine was
never broken — its marker child, `0x1000040C`, authors real Highlight
media). What was missing: retail ALSO recolors the town NAME caption
(a lifted Type-12 child, id collides with the page-level description
panel's own id `0x10000409` in the installed dat — two distinct
elements in two distinct subtrees, harmless for the per-button lift)
from gold (218,167,85) to white (255,255,255) on selection, live-DAT-
measured. `DatWidgetFactory.BuildButton` lifted the caption's font
COLOR once at build time with no per-state override. Same root and fix
as AP-222 (below): per-state label color/outline, applied off the
REQUESTED retail state id.
- **GF-11c Town names misaligned on the map — FIXED (Campaign CC gate
round 1, Batch B).** The per-button caption's own authored rect
(`(0,4,100,37)`, Center-justified, live-DAT-measured) was being
discarded in favor of a Left-aligned offset computed from the marker
FACE's rect (`face.X + face.Width + 4`) — correct for the heritage/
template/Face-Clothes row family (label authored DIRECTLY on the
button, beside a single-purpose face segment) but wrong here, where a
DISTINCT Type-12 caption child was lifted with its own independent
geometry. Fixed by adding `UiButton.LabelBox`: when a distinct lifted
caption carries its own rect, the label draws within THAT box using
its own authored justify instead of the face-relative offset; every
other button (`LabelBox` null) keeps the EXACT prior draw math.
- **GF-12 Missing authored gold frames around boxes — FIXED (Batch C,
Commit 2).** Root cause: `UiText.ConsumesDatChildren` (true unless a
state authors PassToChildren) and `UiField.ConsumesDatChildren`
(unconditionally true) dropped EVERY dat child at import time,
including the eight gold-frame pieces (`0x100002DE..E3`,
`0x100000E8/EA`) every description/report box authors. Fixed by a new
`LayoutImporter.BuildWidget` carve-out (mirroring the existing `UiMeter`
text-overlay carve-out): build any child with its OWN non-empty
`StateMedia`, leaving purely structural/property-only children dropped
as before. Independently re-derived blast radius: **37 distinct
(layout, element) pairs across 15 layouts** (see the commit message for
the full enumeration), including MAIN GAME UI (`0x21000005/0x1000059A`)
and the chat transcript (`0x2100006F/0x10000011` — closing the BUILD half of
pre-filed issue #366's own "fix shape" recommendation, which proposed
this EXACT carve-out). Full App suite (5304 tests): zero regressions.
**The user's own visual check of chat + the main game UI is still owed**
— automated coverage cannot catch a purely visual placement regression.
**Closeout Group 3 (F5/F6, 2026-08-16):** one of this carve-out's
media-bearing children — the chat new-text indicator, `0x1000048C`,
live-DAT-confirmed authored `Invisible=true` on every layout it appears
in (`0x21000005`/`0x21000006`/`0x2100005B`/`0x2100006F`) — was building as
a visible phantom element retail never shows. Fixed with a NARROW honor
scoped to exactly this carve-out (`LayoutImporter.BuildWidget`'s
`UiText or UiField` branch sets a built child's `Visible = false` when its
own `AuthoredInvisible` flag is set), not the general #408 client-wide
honor. Verified in both directions: the invisible chat indicator now
builds hidden, and the eight chargen/main-game-UI gold-frame pieces
(`0x100002DE-E3`/`0x100000E8`/`0x100000EA`) do NOT author `Invisible` and
stay visible — confirmed both by a live-DAT sweep
(`MediaBearingChildSweep_EnumeratesWhichAffectedChildrenAuthorInvisible`)
and a fixture regression test.
- **GF-14 Summary paperdoll backdrop black — FIXED (Campaign CC gate round
1, Batch D, same fix as GF-7 above — both pages call the identical
`gmCG3DView::Update` on their own `gmCG3DView` instance).** **Summary
textbox wrapper + scrollbar — FIXED (Batch C, Commit 2 for the
frame/build half, Commit 3 for the scrollbar LINK and the how-to text's
own content — see the Suspected-shared-roots entry and Commit 3's own
composition of `gmCGSummaryPage::SetHowToText` into `0x10000404`).**
- **GF-16 Hover tooltips missing on all pages** (retail pops tooltips).
DEFERRED to its own gate round — filed as
[`docs/ISSUES.md` #409](../ISSUES.md) with the decomp anchors
(`UIElement::StartTooltipAtMouse @0x00460D70`,
`UIElementManager::StartTooltip @0x0045DE90` + `@0x00459700`, layout DID
`0x21000041`, properties P0x47-P0x4B, ~253 authored elements, prefs
`Misc_TooltipEnable`/`Misc_TooltipDelay`) the Batch D investigation
surfaced. Out of Batch D's scope: it is a CLIENT-WIDE mechanism, not the
chargen 3D preview backdrop Batch D actually fixed (GF-7/GF-14 above).
## Suspected shared roots (to be CONFIRMED by the investigation, not assumed)
1. ~~Missing frames/labels/statics across every page (GF-3, GF-4 labels,
GF-12) — one importer/mount-level gap OR retail writes them at runtime;
decide per element from the authored DAT + decomp.~~ CONFIRMED, CLOSED
at Batch C: TWO distinct mechanisms, both a single shared fix each. (a)
GF-3/GF-4's labels were runtime-composition gaps (unbound description
textbox; a value-write clobbering a caption) — fixed per-page, Commit 1.
(b) GF-12's frames were the importer-level gap the investigation
suspected: `LayoutImporter.BuildWidget`'s `ConsumesDatChildren` handling
dropped every dat child of a `UiText`/`UiField`, client-wide — fixed by
the new media-bearing-child carve-out, Commit 2.
2. ~~Rich text (escape decoding, wrap, scroll, frame) — one text-widget gap
feeding GF-2/GF-3/GF-11a/GF-14.~~ CONFIRMED, CLOSED at Batch C: the new
shared `DatRichText.Compose` helper (escape-normalize + per-segment
word-wrap + per-segment palette color, Commit 1) plus the Commit-2 frame/
scrollbar un-consume plus Commit-3's scrollbar-to-text-scroll linkage
(`UiScrollbar.Model = text.Scroll`, the same pattern
`ChatWindowController` already used) together close the WHOLE family —
GF-2/GF-3/GF-11a/GF-14's text half are all FIXED; GF-14's backdrop half
(GF-7 family) is unrelated and stays open.
3. ~~Selection state media (GF-1 dot, GF-8 sub-tabs, GF-11b white marker,
GF-10 zoom art) — the AP-222 measured mechanism (state media authored
vs applied) across widget kinds.~~ CLOSED, split into TWO distinct
mechanisms, both fixed at Batch B: (a) GF-1/GF-8 share a genuinely
UNRECOGNIZED custom state-name pair (`UiButton` never admitted
"Unselected"/"Selected" into its available-states set at all); GF-10
was pure wiring (the standard Normal/Highlight pair was never even
requested). (b) GF-11b turned out NOT to be a state-media gap — the
marker's own media swap already worked; the actual gap was AP-222's
real mechanism, per-state LABEL COLOR/OUTLINE (a property commit
distinct from the art/media commit, and NOT gated by the same art-
availability check `ActiveState` is). See each GF's own FIXED entry
above and the retired AP-222 / narrowed AP-215 register rows.
4. ~~Preview backdrop (GF-7/GF-14) — what gmCG3DView clears/draws.~~
CONFIRMED, CLOSED at Batch D: `gmCG3DView::Update`'s own
`m_pbgObject`/`m_bgSetupID` pair, sourced from the heritage's
`HeritageGroup_CG.environmentSetupID` field — already parsed into this
codebase as `ChargenHeritageOptions.EnvironmentSetupId` but never
consumed before this fix. See GF-7's own FIXED entry above for the full
decomp citation.
5. ~~Input routing on Summary (GF-15) — focus/typing path on the stacked
chargen screen.~~ CLOSED: focus/typing routing was never broken (live-
verified); the real cause was `RetailDialogFactory` never re-asserting
its open dialogs' z-order against the chargen/char-management screens'
own per-tick `BringToFront` — see GF-15's own entry above. Batch A
fixed it.
## Process
Root-cause investigation FIRST (report-only, static + live-DAT probe
tests, NO client launches while the user's client may be running), then
batched fix rounds per family with Opus review, one republish per batch.
## RE-TEST 3 (lead's own live captures of `1.0.2-cc.m`, 2026-08-16) — R4-1..R4-4
Captured by the lead driving the real client (testaccount, graceful
close). Heritage/Profession/Appearance/Town/Skills-selection/Summary all
render retail-shaped; four residuals visible in the captures:
- **R4-1: Skills credits value overlaps the caption again**
("Available Skill0Credits") — the re-test-2 wrap fix removed Batch E's
caption-width confinement without re-solving the overlap (its own
report flagged the risk). Root-fix the caption/value geometry from the
authored data (where does retail's value actually sit relative to the
caption on `0x100003F9`?).
- **R4-2: the scrollbar thumb TILES** — multiple diamond sprites stacked
down the track (Summary's overview bar shows ~9, Skills 2) instead of
ONE thumb at the scroll position. The re-test-2 single-sprite-thumb
fallback draws repeated/tiled sprites.
- **R4-3: the skills info-box formula line clips** at the box's bottom
edge (the four-line composition exceeds the authored interior).
- **R4-4: the Appearance help text starts mid-sentence** ("right arrows
next to the article of clothing…") — the opening paragraph is either
scrolled off (box has no visible scrollbar) or missing from the
composition; check what retail authors/composes for that box.
**RE-TEST 3 fix batch (2026-08-16, R4-1..R4-4) is CODE-COMPLETE, pending the
user's visual gate.** All four findings root-caused and fixed via decomp +
live-DAT evidence, no invented pixel offsets. App suite live-DAT env
5372/3 → 5379/3 (+7, zero regressions); Runtime 1735/0 unchanged; full
solution 14585 tests / 4 skips / 1 failure (Core.Net
`NakEmissionTests.LossSoak_TwoPercentBidirectional_ZeroMessageLoss_LedgersConverge`,
the documented full-solution-only flake — confirmed passing standalone,
unrelated to this batch's files). No client launches.
- **R4-1 FIXED — root cause was a MISSING raw-edge reflow, not a wrap/clip
gap.** Live-DAT probe: the Skills credits value child (`0x100002f3`) is
BASE-INHERITED across four sibling buttons of DIFFERING widths —
Health/Stamina/Mana at 150px share the exact same child id/rect (local
X=116) as the wider, 231px Skills credits button — and the child's own
`OriginalParentWidth` (150, baked in wherever it was first resolved,
matching Health's own actual width) diverges from Skills credits' real
231px parent. `UiButton.ValueBox` was built from the child's RAW
(un-reflowed) rect, never running it through `UiLayoutPolicy` — the SAME
retail raw-edge system (`UIElement::UpdateForParentSizeChange
@0x00462640`) already used for every LIVE mounted `UiElement` via
`UiElement.ApplyAnchor`. The child's own edge modes (Left=2/Right=1,
live-DAT-confirmed "track the far edge as the parent grows") shift the
value box from X=116 to X=197 for Skills credits specifically — landing
immediately after the caption's own measured 193px span (ends ≈x=196)
instead of colliding mid-caption. Separately, `ValueAlign` mapped
`HJustify.Right` (raw dat 3/5, live-DAT-confirmed authored on ALL four
value children) to Center — `UIElement_Text::CalcJustification
@0x00467260`'s own `ecx_5==3||5` branch is a DISTINCT far-edge formula,
not Center's halved offset; added a `LabelAlignment.Right` case.
Health/Stamina/Mana (whose `OriginalParentWidth` already matches their
own actual width) reflow to their byte-identical raw rect — the fix is
additive, not a per-button special case.
Files: `src/AcDream.App/UI/Layout/DatWidgetFactory.cs` (`BuildButton`'s
value-child block, new `ReflowValueChildRect`), `src/AcDream.App/UI/UiButton.cs`
(`LabelAlignment.Right`, `OnDraw`'s value-draw `vx` switch). Tests:
`DatWidgetFactoryTests.BuildButton_ValueChildBaseInheritedNarrowerParent_ReflowsToWiderButton`
(+ its `..._OriginalParentMatchesActual_RectUnchanged` negative
companion); live-DAT
`CharacterCreationLiveDatTests.SkillsCreditsButton_ValueBoxReflowsPastCaption_HealthValueBoxUnchanged`
(pins the real installed DAT's `197,0,34,28` vs `116,0,34,28`).
- **R4-2 FIXED — the single-sprite-thumb fallback was TILING (UV-repeat)
a small marker graphic instead of drawing it once.** The re-test-2 fix
(R3-4/R3-7) correctly identified the thumb sprite but fed it to
`DrawTiled` (GL_REPEAT UV wrap) — for a small fixed "diamond" marker
drawn into a track-proportional thumb rect far taller than its own
native size (`UIElement_Scrollbar::UpdateLayout @0x4710d0`'s
`max(MinThumb, trackLen*ThumbRatio)` formula, unchanged/still correct
for the rect's SIZE), the texture sampler repeated the marker several
times down the track (~9 on Summary's overview bar, ~2 on Skills,
matching the live capture). New `DrawThumbMarker` draws exactly ONE
instance at the sprite's own native size, centered within the SAME
computed rect — neither tiled (the bug) nor stretched into an elongated
bar (a naive `DrawSprite` fix would have distorted the diamond shape).
The shade slider's own scalar-mode draw path (`DrawVerticalScalar`) was
never touched — it already used the correct native-size `DrawSprite`
pattern this fix now mirrors for model-mode bars.
File: `src/AcDream.App/UI/UiScrollbar.cs` (`DrawVerticalModel`/
`DrawHorizontalModel`'s fallback branch, new `DrawThumbMarker`). Test:
`UiScrollbarTests.SingleSpriteThumb_DrawsOneUntiledInstance_NotRepeatedDownTrack`
— reads back the actual emitted quad's UV V-coordinate via
`TextRenderer.DebugSpriteSegmentVerts` and asserts it never exceeds 1.0
(native); confirmed this test FAILS (V=7.875) against the pre-fix
`DrawTiled` call by temporarily reverting and re-running.
- **R4-3 FIXED — the description pane's own authored box is genuinely
taller than the decorative frame that visually contains it.** Live-DAT
geometry walk: the gold frame (`0x100003fa`, the SAME GF-12 corner/edge
sprite family as the Appearance help box) spans Y=430 H=110 (bottom
Y=540), but the description pane (`0x100003fc`) spans Y=460 H=100
(bottom Y=560) — 20px PAST the frame's own bottom border. Composition-
height simulation against every one of the 38 skills carrying detail
data (real `ChargenTableReader` descriptions + the worst-case
description+bonus+formula line count) confirmed the pane's OWN raw
100px interior comfortably fits every case (worst: 5 lines / 80px < 90px
interior) ruling out a wrap-width or line-spacing bug. The real
mismatch is the SIBLING frame's smaller authored bottom, which the pane
was never clamped to, letting a tall composition's last line(s) draw
past the frame's own visible border into blank page space. Retail's
`ShowSkillsText @0x00481250` has no code linking the panes to the frame
(plain `SetText`, no size/clip handoff) the frame's own authored Y+H
is the only available ground truth, not a decomp-confirmed clip
mechanism, so this is filed as register **AD-105** (a genuine
inference, flagged rather than silently assumed, same shape as R3-3's
own AD-104 scoped correction). `CharacterCreationSkillsPage`'s
constructor now clamps `_infoText.Height` to the frame's bottom edge
whenever it would otherwise be taller (additive; never grows it).
File: `src/AcDream.App/UI/Layout/CharacterCreationSkillsPage.cs`
(constructor, new `InfoBoxFrameElementId` clamp block). Tests: fixture
`CharacterCreationUiControllerTests.SkillsPage_InfoBoxDescriptionPane_HeightClampedToFrameBottom`
(the shared `BuildSkillsPage` fixture gained a deliberately-shorter
frame element); live-DAT
`CharacterCreationLiveDatTests.SkillsInfoBoxFrame_ShorterThanDescriptionPane`
(pins the 20px real-DAT mismatch itself).
- **R4-4 FIXED two stacked gaps, the same "page never touched this
element" shape as prior holdouts.** The help box (`0x100003ab`) is a
purely DAT-authored static paragraph (no `gmCGAppearancePage` runtime
composition function exists for it, unlike Town/Summary's
`SetTownString`/`SetHowToText` confirmed absent from the named
decomp) that `CharacterCreationAppearancePage` never referenced at all,
so it kept `UiText`'s own chat-style default
(`PreserveEndOnLayout=true`, "keep a view that is already at the end
pinned there"). Its content overflows a 292px-tall frame, and
`UiScrollable.SetExtents`'s own `wasAtEnd` check is vacuously true the
FIRST time a Scroll model transitions from its zero-initialized state
(`ContentHeight=0/ViewHeight=0/ScrollY=0` `MaxScroll=0`
`AtEnd=(0>=0)=true`) to real overflowing content with
`PreserveEndOnLayout` still true, that spuriously pins the very first
render to the BOTTOM, hiding the opening paragraphs exactly as reported
(the visible text is mid-way through the third paragraph). This is a
static instructions box, not a chat transcript `PreserveEndOnLayout`'s
own doc already carves out exactly this shape ("top-oriented reports
such as Character Information disable it"). Also wired the box's own
NESTED authored scrollbar (property `0x72`, live-DAT-confirmed a direct
Type-11 child of the text box the SAME nesting shape
`CharacterCreationSummaryPage.HowToScrollRelativeId` already uses)
never wired by this page before, so a user can reach the rest of the
text even where the box's own height still doesn't fit everything.
File: `src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs`
(constructor, new `HelpTextId`/`HelpScrollRelativeId` block). Tests:
fixture
`CharacterCreationUiControllerTests.AppearancePage_HelpText_TopOriented_AndOwnScrollbarIsWired`
(the shared `BuildAppearancePage` fixture gained the help box + its
nested scrollbar child, StateMedia-bearing so `UiText`'s own dat-
children carve-out actually builds it).
## GATE PASSED — 2026-08-16, build `1.0.2-cc.o`
The user's final pass on `1.0.2-cc.o` (carrying the R4 fixes) returned
**"Ok works. Gate pass!"** Campaign CC's connected gate is CLOSED
USER-ACCEPTED. Every finding family in this doc (GF-1..16, R2-1..8,
R3-1..9, R4-1..4) is fixed and user-verified, except the two explicitly
dispositioned non-bugs: the `[ Name ]` prefill (three independent
exhaustive negatives retail's field is empty; closed as a recollection
slip unless retail evidence surfaces) and the items deferred to their own
tracked issues (#408 general authored-Invisible, #409 tooltips, #410
VJustify default, AP-231's formula connector).