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