test(chargen): Campaign CC gate round 1 re-test 2 — R3-8 name-field exhaustive dump + closeout docs

R3-8: dumped EVERY property present on 0x10000402 (not just P0x17) across
every state, cross-referenced against UIElement_Text::OnSetAttribute's
complete case list (no UIElement_TextInput class exists in retail — the
name field is a plain UIElement_Text/m_filter-bearing field). The full
recognized-property space has no placeholder/prompt mechanism independent
of P0x17. The BaseElement/prototype-inheritance hypothesis is also ruled
out — the existing regression test already probes the fully-merged
ElementInfo (post BaseElement resolution) and finds nothing. The only
StringInfo-kind property present, 0x49, resolves 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 #409/GF-16 already
document client-wide (0x48's own DID, 0x21000041, is the EXACT tooltip
popup LayoutDesc #409 cites) — a hover tooltip, not an in-field
placeholder. No code change, per this batch's own "do not invent a
placeholder" contract — third independent negative result on this
question via three different mechanisms. The lead should request a live
retail screenshot before any further investigation.

Also carries the shared live-DAT regression suite for R3-1 through R3-7
(CharacterCreationLiveDatTests.cs holds tests spanning multiple findings
in one file, so they land together) and the RE-TEST 2 findings-doc
closeout writeup for all eight items.

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.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 17:20:42 +02:00
parent 2886f79f37
commit 91f84dec9f
2 changed files with 562 additions and 1 deletions

View file

@ -1,6 +1,6 @@
# 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-9
## 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:
@ -40,6 +40,250 @@ Heritage PASSES. Remaining, with retail side-by-side screenshots:
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