acdream/docs/research/2026-08-16-campaign-cc-gate-round1-findings.md
Erik e6acb800cc fix(chargen): Campaign CC gate round 1 re-test 3 — R4-1..R4-4
Four visual residuals from the lead's own live-client captures of
1.0.2-cc.m, all root-caused via decomp + live-DAT evidence:

- R4-1: Skills credits value overlapped mid-caption again. Root cause
  was a missing UiLayoutPolicy raw-edge reflow on UiButton's value-child
  rect (the child is base-inherited across four sibling buttons of
  differing widths, so its baked-in OriginalParentWidth diverges from
  the actual 231px-wide Skills credits button) plus an HJustify.Right
  value child mapped to Center instead of a real far-edge Right.
- R4-2: the single-sprite scrollbar thumb tiled (GL_REPEAT) instead of
  drawing once — DrawTiled was reused for a small fixed marker graphic
  whose native size is far smaller than the track-proportional thumb
  rect. New DrawThumbMarker draws exactly one native-size instance.
- R4-3: the skills info-box formula line clipped past the surrounding
  gold frame's own authored bottom edge (the pane's own raw box is 20px
  taller than the frame that visually contains it) — clamp the pane's
  Height to the frame's bottom (register AD-105, since retail's
  ShowSkillsText has no code relationship to the frame to cite).
- R4-4: the Appearance help text started mid-sentence — the box was
  never touched by its page controller, so it kept UiText's chat-style
  PreserveEndOnLayout=true default; the scroll model's wasAtEnd check is
  vacuously true on its first-ever overflow transition, pinning the
  first render to the bottom. Set PreserveEndOnLayout=false (a static
  top-oriented report, not a transcript) and wired the box's own nested
  authored scrollbar, never wired before.

App suite live-DAT env 5372/3 -> 5379/3 (+7, zero regressions). Runtime
1735/0 unchanged. Full solution 14585/4 skips/1 failure (the documented
Core.Net NakEmission full-solution-only flake, confirmed standalone-pass).

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

75 KiB
Raw Blame History

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/0x26UIElement_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.ScrollCharacterCreationSummaryPage'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 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=0MaxScroll=0AtEnd=(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).