From e264d8392fe17437323dc663d006591fdfa60131 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 24 Aug 2026 21:58:27 +0200 Subject: [PATCH] =?UTF-8?q?docs(CT):=20CT1=20fix=20round=20=E2=80=94=20sea?= =?UTF-8?q?led=20RowHighlightSprite=20verdict,=20verified=20resize=20mecha?= =?UTF-8?q?nism,=20strengthened=20pins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies the Opus dual-lens review corrections to CT1's DAT ground-truth research (docs/research/2026-08-24-campaign-ct-dat-ground-truth.md): - Window constraints (BLOCKER): replaced the "likely a hardcoded ResizeTo/SetMinSize" guess with the verified mechanism — UIElement::ResizeTo clamps only via element attributes 0x3C-0x3F, nothing writes them at runtime, and retail resizes the SHARED gmPanelUI host (LayoutDesc 0x2100006E, slot 0x1000018E) rather than 0x2100002E's own content root. Flags the unresolved 300x600-vs-300x362 size tension for CT3/CT6 and marks the host elements NOT PROBED by CT1. - RowHighlightSprite upgraded from a flagged hedge to a SEALED VERDICT: the stat row's selected-state media is 0x06000F93 (gmAttributeUI::UpdateSelection -> InfoRegion::SetState on template 0x10000248), not 0x06001397 (which is legitimately the spellbook row's separate selected-overlay mechanism). Falsifies the matching comment in CharacterStatController.cs and dated-corrects the older 2026-06-26 doc at the spot that originated the wrong sprite id. - Replaced the "18px gutter + 7px = 25px" derived story with the bare authored rectangles (the numbers don't compose cleanly: 300-281=19, and the 282px row overlaps the 281px scrollbar band by 1px) — CT5 must implement the authored numbers directly, never a derived listWidth-18 formula. - Plan doc: corrected the UpdateButtons ghost rule (no selection -> Ghosted, not "ghosts when selected == current") and added the AddTitleToList row-write contract for CT3. - Pins: CharacterPanelLiveDatTests now honors ACDREAM_DAT_DIR first (matching InstalledDatFactAttribute and its sibling live-DAT test classes), hoists five vacuous bare-foreach assertions to counted .ToList() pins, and adds the stat ListBox + scrollbar rect pins that CT5/CT6 depend on. - Doc hygiene: marked several probe-session observations (header geometry "identical" claim, 0x06004CC2 characterization, the master-map/category-map dump) as unpinned inference vs. committed fact, corrected the 0x1000052D "throwaway container" mislabel, and stated the header table's parent-relative coordinate frame. - Recorded the CT5 gold this round found: InfoRegion::InfoRegion's icon-DID lookup (a third GetDIDByEnum consumer, category 0x10000002) and gmSkillUI::RebuildSkillList's section-header order confirmation, plus the RowHeight=22-vs-authored-20 divergence for attribute rows. Verified: ACDREAM_RUN_INSTALLED_DAT_TESTS=1 CharacterPanelLiveDatTests filter 9/9 green; hermetic App suite filter (CI's Lane exclusion list) 6111/6111 green. No production code changed. Co-Authored-By: Claude Fable 5 --- ...6-08-24-character-panel-parity-campaign.md | 17 +- ...06-26-character-window-retail-reference.md | 17 +- ...2026-08-24-campaign-ct-dat-ground-truth.md | 271 ++++++++++++++---- .../UI/Layout/CharacterPanelLiveDatTests.cs | 57 +++- 4 files changed, 297 insertions(+), 65 deletions(-) diff --git a/docs/plans/2026-08-24-character-panel-parity-campaign.md b/docs/plans/2026-08-24-character-panel-parity-campaign.md index 9a4ad6c8..223caa43 100644 --- a/docs/plans/2026-08-24-character-panel-parity-campaign.md +++ b/docs/plans/2026-08-24-character-panel-parity-campaign.md @@ -40,10 +40,19 @@ land; every deviation a slice introduces adds its row in the same commit. `AddTitleToList @0x0049A840` resolves the display string via `CharacterTitleTable::GetCharacterTitleFromID` (DAT title-string table — CT2 locates the DID) and inserts SORTED - (`FindSortedInsertPosition @0x0049A760`). -- `UpdateButtons @0x0049A500`: the display button GHOSTS (state 0xd) - when the selected row's title id == `mDisplayTitle`; Normal (1) - otherwise. Selection change (msg 4/0x43) re-runs it. + (`FindSortedInsertPosition @0x0049A760`). It writes the resolved title + text into row child `0x10000537` and stamps the row's id via + `SetAttribute_Enum(row, 0x1000008E, titleId)`, inserting the row via + `AddItemFromTemplateList(listBox, 0, insertPos)` — CT3 mirrors this + exact write shape when populating `0x10000532`. +- **CORRECTED (CT1 fix round 2026-08-24):** `UpdateButtons @0x0049A500` + — the display button is **GHOSTED (state 0xd) UNLESS a row is + SELECTED whose title id differs from the current display title; no + selection → Ghosted.** (Not "ghosts when selected == current" — that + phrasing had the no-selection case backwards.) Verbatim mechanism: + a no-match selection falls through to index `0xFFFFFFFF` → + `GetItem` returns null → `SetState(0xd)`. Selection change + (msg 4/0x43) re-runs it. - Clicking `0x10000535` sends `CM_Social::Event_SetDisplayCharacterTitle(titleId)` (`ListenToElementMessage @0x0049A6D0`). diff --git a/docs/research/2026-06-26-character-window-retail-reference.md b/docs/research/2026-06-26-character-window-retail-reference.md index 8ddb9c2a..949e3a7c 100644 --- a/docs/research/2026-06-26-character-window-retail-reference.md +++ b/docs/research/2026-06-26-character-window-retail-reference.md @@ -31,6 +31,20 @@ the remaining polish on acdream's `CharacterStatController` (LayoutDesc 0x210000 - **Selected row (Strength):** highlighted with a **DARKER background + bars above/below** — retail's selected-row sprite `0x06001397` (Button state 6). (✗ acdream uses a translucent GOLD tint — replace with the dark-bar sprite.) + + > **CORRECTION 2026-08-24 (Campaign CT, CT1 fix round).** The sprite id + > above is WRONG for this element. Campaign CT's live-DAT probe + > (`docs/research/2026-08-24-campaign-ct-dat-ground-truth.md`, §2 + > "SEALED VERDICT") found the attribute/skill row's actual Highlight + > media is `0x06000F93`, drawn via `gmAttributeUI::UpdateSelection + > @0x0049DEE0`'s `SetState(6)` → `InfoRegion::SetState @0x004F0EE0` on + > the row template `0x10000248` itself. `0x06001397` is real, but it + > belongs to a DIFFERENT mechanism: the spellbook row's separate + > selected-overlay child element (`0x10000342` under prototype + > `0x10000343`, via `UIElement_UIItem::SetSelectedState @0x004E1240`). + > This note's "Button state 6" framing was accidentally right about the + > STATE number but wrong about which sprite that state resolves to on + > this element. CT5 is the owning slice for the fix. - **Footer flips to:** - Title: **"Strength: 200"** — **WHITE** text (✗ acdream uses the body/gold color). - "Experience To Raise:" + **"Infinity!"** (Strength is maxed → cost is infinite; ✗ acdream shows a @@ -45,7 +59,8 @@ the remaining polish on acdream's `CharacterStatController` (LayoutDesc 0x210000 3. [ ] Add "Total Experience (XP):" caption. 4. [ ] Add "XP for next level:" caption + value (un-consume from the meter, or render alongside). 5. [ ] Row text larger (≈icon height) + rows tighter. -6. [ ] Selection highlight → sprite 0x06001397 (dark bars), not gold tint. +6. [ ] Selection highlight → sprite 0x06000F93 (dark bars), not gold tint. (Corrected 2026-08-24 — + see the CORRECTION note above; the sprite id in this checklist item was originally 0x06001397.) 7. [ ] Selected footer title → white. 8. [ ] Maxed attribute → "Experience To Raise: Infinity!". 9. [ ] Footer title wording = "Select an Attribute to Improve" (Attribute). diff --git a/docs/research/2026-08-24-campaign-ct-dat-ground-truth.md b/docs/research/2026-08-24-campaign-ct-dat-ground-truth.md index ad302fb4..ddb933ab 100644 --- a/docs/research/2026-08-24-campaign-ct-dat-ground-truth.md +++ b/docs/research/2026-08-24-campaign-ct-dat-ground-truth.md @@ -19,8 +19,21 @@ through the Attributes page chain (`0x10000227 > 0x1000022B > 0x10000226 > 0x10000230`) and one through the Skills page chain (`... > 0x1000022C > ...`). **The two copies are geometrically and stylistically IDENTICAL** — this is the "duplicated stat-management branches" quirk documented on -`PrepareSkillScrollbar` in `CharacterStatController.cs`, confirmed here to -be a harmless duplicate (not a divergent one) for every header id checked. +`PrepareSkillScrollbar` in `CharacterStatController.cs`. **Caveat: this +"identical" claim rests on the deleted probe tests, not the committed +pins.** `CharacterPanelLiveDatTests.HeaderElements_AuthorExpectedFontsAndColors` +only asserts font/color/margin equality across the two copies (and the +2-count) — it does not assert X/Y/W/H geometric equality between them. +Treat "geometrically identical" as probe-session observation, not a +pinned fact, until a future slice adds a geometry-equality assertion. + +**Coordinate frame:** every `X,Y` below is **parent-relative**, not +page-relative — e.g. Name `0,0` means the top-left of its immediate +parent container (`0x10000230`), not the top-left of the Attributes/ +Skills page. Cross-reference +`docs/research/2026-06-25-character-window-faithful-spec.md` for the +page-relative numbers if you need the header block's position within +the page itself; do not mix the two frames when placing elements. | Element | Id | X,Y | W×H | HJustify | VJustify | FontDid | FontColor | Outline | Margins | |---|---|---|---|---|---|---|---|---|---| @@ -110,23 +123,59 @@ for item 1 (icon alignment) and item 2 (value-column gutter): — this is the "authored margin between the value column and the border" the owner reported (item 2), confirmed as exactly 7px at the row-template level. -- **The row (282px) is already inset from the ListBox's full width - (300px) by 18px** to clear the always-reserved scrollbar gutter - (scrollbar at X=281, W=16) — so the TOTAL space between the value - text's right edge and the ListBox's outer right edge is - `300 - 275 = 25px`, of which 18px is the permanent scrollbar gutter and - 7px is the row template's own inset. Both numbers matter for CT5: - the row width (282, confirmed correct — matches the existing - `SkillContentWidth = 282f` constant already in the code) and the - internal 175/100 value-column placement (not currently matched). -- Row background: `Normal` state file `0x06004CC2` (the same generic - panel-chrome fill used elsewhere client-wide), `Highlight` state file - **`0x06000F93`**. `CharacterStatController.RowHighlightSprite` is - currently `0x06001397u` — **this is a divergent constant**; CT5 should - either confirm `0x06001397` is deliberately used for a DIFFERENT - highlight surface (e.g. the vitals/skill list uses a shared sprite - elsewhere) or correct it to `0x06000F93` for the attribute/skill row - highlight specifically. Flagged, not fixed, in this slice. +- **The bare authored rectangles** (no derived arithmetic — see the + caveat below): ListBox `0x1000023D` is `X=0 Y=112 W=300 H=160`; its + scrollbar `0x1000023E` is `X=281 W=16 H=160`; the data row template is + `W=282`; the value column inside the row is `X=175 W=100`, + right-justified, 7px from the row's own right edge (`282 - (175+100) = + 7`). +- **These numbers do NOT compose into a tidy "gutter" story — do not + infer one.** `300 - 281 = 19`, not the row's 282px width's complement + (`300 - 282 = 18`); and the 282px row actually OVERLAPS the 281px + scrollbar band by 1px (`281 < 282`). An earlier draft of this doc + described the row as "inset by 18px to clear an always-reserved + scrollbar gutter" and summed 18+7=25 — that decomposition does not + close against the authored numbers above and is **inference, not + fact**; withdrawn. CT5 must implement the authored numbers directly + (row width 282 — matches the existing `SkillContentWidth = 282f` + constant already in the code; value column X=175 W=100; scrollbar + X=281 W=16), never a derived `listWidth - 18` or similar formula. +- Row background: `Normal` state file `0x06004CC2`. **The description + "generic panel-chrome fill used elsewhere client-wide" is UNVERIFIED** + — no cross-reference sweep for other consumers of `0x06004CC2` was run + this slice; only the file id itself, as authored on this specific + template, is pinned. `Highlight` state file **`0x06000F93`**. + +### SEALED VERDICT: RowHighlightSprite is wrong, not merely flagged + +Retail's selected attribute/skill row draws the row template's Highlight +state media — `0x06000F93`, not `CharacterStatController`'s current +`0x06001397u`. This is no longer a hedge; the decomp confirms the +mechanism end to end: + +- `gmAttributeUI::UpdateSelection @0x0049DEE0` calls + `SetState(selected ? 6 : 1)` on the row. +- `InfoRegion::SetState @0x004F0EE0` forwards that state to the row + element instantiated from template `0x10000248` — the exact template + this slice dumped, whose Highlight-state file is `0x06000F93`. +- State `6` IS `UIStateId.Highlight` — so the selected row draws + `0x10000248`'s own `Highlight` media, not a separately-chosen sprite. + +`CharacterStatController.cs`'s comment near lines 111–113 ("matches +retail... sprite 0x06001397 visual intent") is **falsified for this +element**. CT5 must correct `RowHighlightSprite` to `0x06000F93` for the +STAT rows and should reconsider `UseSelectionBars`/`HighlightBg`, which +currently emulate the wrong sprite's art (a translucent gold tint tuned +to look like `0x06001397`'s dark bars, not `0x06000F93`'s actual look). + +**`0x06001397` is not a phantom constant — it is legitimate ELSEWHERE.** +The spellbook row prototype `0x10000343` has a separate selected-overlay +CHILD element `0x10000342` whose media IS `0x06001397` +(`UIElement_UIItem::SetSelectedState @0x004E1240` mechanism — a +different code path from `InfoRegion::SetState`, and a different visual +composition: an overlay child, not a state-swap on the row itself). CT5 +must correct the STAT rows ONLY and must NOT touch `SpellbookRowStyle.cs` +or its tests — `0x06001397` is correct there. ### 0x10000249 / 0x1000024A / 0x1000024B / 0x1000024C — skill SECTION HEADER captions @@ -151,6 +200,38 @@ for the existing constants already in `CharacterStatController.cs`: These four are correctly ported already; no CT5 work needed here. +### Row-height divergence (CT5 gold, found this fix round) + +The row template above authors `H=20` (line `Id=0x10000248 Type=3 +(container) X=0 Y=0 W=282 H=20`, already pinned by +`CharacterPanelLiveDatTests.AttributeRowTemplate_...`). Current code +matches this for skill rows (`CharacterStatController.SkillRowHeight = +20f`) but NOT for attribute rows, which use a separate +`CharacterStatController.RowHeight = 22f` constant. CT5 must fix the +attribute-row path to 20px; there is no authored basis for 22 anywhere +in the row template. + +### Row instantiation + icon-DID anchors (CT5 gold, found this fix round) + +`InfoRegion::InfoRegion @0x004F1450` instantiates each stat row via +`AddItemFromTemplateList(listBox, 0, ...)` — template index **0**, i.e. +`0x10000248`, the shared data row confirmed above — and binds +`0x1000012A` (label), `0x1000012B` (value), `0x10000129` (icon) via +`UIRegion::SetImageByDID(icon, did, 3)` (icon draw mode 3). Per-attribute +icon DIDs come from `DBObj::GetDIDByEnum(statEnum, category +0x10000002)` in `gmAttributeUI::PostInit @0x0049DB70` — a THIRD consumer +of the `GetDIDByEnum` master-map mechanism documented in §5 below +(alongside the title EnumMapper/StringTable pair and `RetailKeyNames`), +which is enough precedent that CT2/CT5 should factor a shared +`GetDIDByEnum(enumValue, category)` helper instead of hardcoding a third +independent DID pair. + +`gmSkillUI::RebuildSkillList @0x0049C3A0` adds template indices **1–4** +(`0x10000249..0x1000024C`) for the section-header captions, confirming +the section-header order already pinned by +`SkillSectionHeaderTemplates_MatchExistingSpriteConstants` above: +Specialized, Trained, Untrained, Unusable. + ## 3. Titles page (0x10000539 subtree, imported as part of 0x2100002E) `0x10000539` (Type 0x10000046, the Titles page container) is NOT @@ -188,10 +269,19 @@ scrollbar-gutter inset because the ListBox width itself already excludes the scrollbar column). Same targeted-root import gotcha as the stat templates applies here: -`LayoutImporter.ImportInfos(dats, 0x2100005Eu)` (the whole-layout overload) -returns the SAME element (0x1000052D, root of a throwaway container) but -does NOT surface 0x10000536 as a reachable child — use -`LayoutImporter.ImportInfos(dats, 0x2100005Eu, 0x10000536u)` instead. +`LayoutImporter.ImportInfos(dats, 0x2100005Eu)` (the whole-layout +overload) returns the SAME element `0x1000052D`, but does NOT surface +`0x10000536` as a reachable child. **`0x1000052D` is not a throwaway +container** — per the `BaseElement`/`BaseLayoutId` table in +`docs/research/2026-06-25-character-window-faithful-spec.md` (line +~19), `0x1000052D` is the authored `BaseElement` that the Titles page +root `0x10000539` inherits its content from (`0x10000539`'s +`BaseLayoutId` is `0x2100005E`, `BaseElement` is `0x1000052D`) — it is +the real authored Titles-page content, just reached by a different path +than the live-mounted tree. Use +`LayoutImporter.ImportInfos(dats, 0x2100005Eu, 0x10000536u)` (the +targeted single-root overload) to reach the row template `0x10000536` +underneath it. ## 4. Window min/max constraints @@ -234,17 +324,64 @@ window-frame elements are its own self-contained LayoutDesc, while 0x2100002E is CONTENT ONLY (tab bar + pages), with retail's window chrome supplied by a separate mechanism. -**Implication for CT6:** the character window's authored minimum height -(if one exists in retail) is not going to fall out of 0x2100002E's own -`MinHeight`/`MaxHeight` properties — those are simply absent. CT6 needs -to find where retail's `gmStatManagementUI`/its owning window-frame class -enforces a minimum window size (likely a hardcoded `ResizeTo`/`SetMinSize` -call in that class's C++, or a shared base window-frame behavior applied -uniformly — see the plan's own CT6 wording, "verified as the STANDARD path -for every registered window"). This is NOT a simple "read the DAT -property" fix like `MountSideVitals` was; treat the "Already in-tree" -plan bullet as **inaccurate** and start CT6 from the decomp for the -window-frame class instead of assuming the wiring is already 90% done. +Treat the "Already in-tree" plan bullet as **inaccurate**: start CT6 +from the decomp for the window-frame class instead of assuming the +wiring is already 90% done. The paragraph below replaces this doc's +earlier "likely a hardcoded ResizeTo/SetMinSize call" guess with the +verified mechanism. + +### Verified resize mechanism (2026-08-24, supersedes the hypothesis above) + +`UIElement::ResizeTo @0x00463C30` clamps ONLY via element attributes — +`0x3C` (clamp-max-height), `0x3E` (clamp-min-height), `0x3D` +(clamp-max-width), `0x3F` (clamp-min-width) — read off `this`, the +element actually being resized. A decomp-wide grep for writers of those +four attributes turns up NOTHING: no runtime code ever sets them at +runtime. The clamp source is exclusively **authored DAT properties on +whichever element `ResizeTo` is called against, full stop.** There is no +hardcoded `SetMinSize` call anywhere in the class hierarchy; the earlier +"likely a hardcoded ResizeTo/SetMinSize call" phrasing in this doc was a +guess and is WRONG. + +The element `ResizeTo` is called against is not `0x2100002E`'s own root. +Per `docs/research/2026-07-17-retail-shared-main-panel-pseudocode.md` +(lines ~83-107) and the slot table in +`docs/research/2026-08-11-fa-panel-structure.md` (row for `0x1000018E`), +retail's Character/Skills tab content is one child slot inside the +SHARED `gmPanelUI` host, LayoutDesc `0x2100006E`: + +```text +gmPanelUI host 0x100005FE 310 x 372 +content parent 0x10000180 300 x 362 (anchored all edges) +Character/Skills slot 0x1000018E panel id 11, 300 x 362 +top-center Dragbar 0x1000065C Type 2 +bottom-center Resizebar 0x10000660 Type 9 +``` + +`gmPanelUI::ResizeTo @0x004BC6E0` is a bare tailcall into +`UIElement::ResizeTo` — the HOST is what gets resized (via its +Resizebar), not `0x2100002E`'s content root; the content root's own +absent MinHeight/MaxHeight (confirmed above) is therefore consistent +with retail's actual mechanism, not evidence of a missing DAT property. + +**NOT PROBED by CT1.** The host elements above (`0x100005FE`, +`0x10000180`, `0x1000018E`, `0x1000065C`, `0x10000660`) were read from +the cited pseudocode doc, not re-probed live against the installed DAT +this session. CT6's first step is to probe those host slots directly +(min/max + resize authoring) before porting anything, then read +`UIElement_Resizebar::StartMouseResizing @0x0046B7E0` verbatim for the +drag-time clamp application. + +**Size tension for CT3/CT6 to resolve.** `0x2100002E`'s own root is +authored **300×600** (the Titles page alone is 300×575, plus the 25px +tab bar = 600 — §3), but retail mounts that content into the host's +**300×362** slot (`0x1000018E`). CT3's title ListBox height (`0x10000532` +is 270×455 per §3) and CT6's resize contract both assume a taller +available area than the host slot's authored 362px. This doc does not +resolve which number governs at runtime (scroll-clipped content inside a +fixed slot vs. the slot itself growing to accommodate) — CT6 must +resolve it from the decomp before implementing the resize contract, not +infer it from either number in isolation. ## 5. The title-string table (DAT ground truth for `CharacterTitleTable::GetCharacterTitleFromID`) @@ -288,6 +425,15 @@ this investigation. ### Live-DAT resolution (verified end-to-end this session) +**The master-map / category-map tables and the `RetailKeyNames` +cross-validation below are UNPINNED probe output** — they come from the +same deleted `Assert.Fail` probe tests as the rest of this doc and are +not backed by a committed `InstalledDatFact` assertion (unlike the +`TitleStringTable_ResolvesWarMageEndToEnd` pin, which DOES commit the +final two DIDs and the end-to-end string resolution). Treat the +category-4 dump and the `RetailKeyNames` match column as this session's +observation, re-derivable from the DAT but not regression-guarded. + `DatReaderWriter.DBObjs.EnumIDMap` (ACE's historical name: `DidMapper`, file-type byte `0x25`) is the object type both master and category maps use; `DatReaderWriter.DBObjs.EnumMapper` (file-type byte `0x22` on the @@ -354,11 +500,15 @@ exactly "War Mage". map → target DID) — CT2 can either hardcode the two resolved DIDs (`0x22000041` for the EnumMapper, `0x2300000E` for the StringTable, the way `RetailKeyNames` hardcodes its three) or port the two-level - indirection generically. Given `RetailKeyNames` already established the - "just hardcode the resolved DIDs, cite the probe" precedent for this - exact category-4 family, CT2 should follow the same precedent unless a - THIRD consumer of `GetDIDByEnum` appears that would justify factoring - out a shared helper. + indirection generically. **A THIRD consumer has now appeared** (found + this fix round): `gmAttributeUI::PostInit @0x0049DB70` resolves + per-attribute icon DIDs via `DBObj::GetDIDByEnum(statEnum, category + 0x10000002)` — see the "Row instantiation + icon-DID anchors" note in + §2. With `RetailKeyNames` (category 4) and the title chain (categories + 1 and 4) already hardcoding resolved DIDs, this third independent + category (`0x10000002`) is the point where CT2/CT5 should factor a + shared `GetDIDByEnum(enumValue, category)` helper instead of adding a + fourth ad-hoc hardcoded pair. 2. `EnumMapper.IdToStringMap[titleId]` → raw canonical name (already readable via `dats.Portal.TryGet`). 3. `DatStringResolver.ComputeHash(rawName)` (already exists, no new code). @@ -371,14 +521,18 @@ already does steps 3–4 for other consumers. ## Corrections to the plan (summary) -1. **Window constraints are NOT already 90% wired.** The plan's - "Already in-tree" bullet claims `DatConstraintSource` registration for - the character window; the actual `MountCharacter()` call sets no such - field, and the DAT layout itself authors no MinHeight/MaxHeight on its - root to source one from even if it were wired. CT6 needs decomp - research into where retail's authored minimum for this specific window - actually lives (likely a class-level constant/behavior, not a - per-window DAT property) before it can port anything. +1. **Window constraints are NOT already 90% wired — and the clamp + mechanism is now VERIFIED, not guessed.** The plan's "Already in-tree" + bullet claims `DatConstraintSource` registration for the character + window; the actual `MountCharacter()` call sets no such field, and the + DAT layout itself authors no MinHeight/MaxHeight on its root to source + one from even if it were wired. `UIElement::ResizeTo @0x00463C30` + clamps only via element attributes `0x3C`–`0x3F`, which nothing writes + at runtime — the clamp source is always authored DAT properties on the + resized element, full stop, and the resized element is the SHARED + `gmPanelUI` host (`0x2100006E`, slot `0x1000018E`), not `0x2100002E`'s + own root. See §4's "Verified resize mechanism" for the full chain and + the unresolved 300×600-vs-300×362 size tension CT6 must still resolve. 2. **The row-template elements are not walkable via the normal `ImportInfos(dats, layoutId)` overload.** `#375`'s prototype-skip logic deliberately excludes same-layout template-list targets from the built @@ -387,10 +541,25 @@ already does steps 3–4 for other consumers. (`LayoutDesc 0x21000045`) and `0x10000536` (`LayoutDesc 0x2100005E`) — documented here so CT5 doesn't waste a cycle rediscovering the same "NOT FOUND" dead end this slice hit first. -3. **`RowHighlightSprite` may already be wrong.** The DAT's row-template - Highlight state uses `0x06000F93`; the current constant in - `CharacterStatController.cs` is `0x06001397`. Not fixed in this slice - (no production changes); flagged for CT5's review. +3. **`RowHighlightSprite` IS wrong — SEALED, not merely flagged.** The + DAT's row-template Highlight state is `0x06000F93`, reached via + `gmAttributeUI::UpdateSelection`'s `SetState(6)` → + `InfoRegion::SetState` on the row itself; the current constant in + `CharacterStatController.cs` is `0x06001397`, which belongs to a + DIFFERENT mechanism (the spellbook row's selected-overlay child, + `UIElement_UIItem::SetSelectedState`). CT5 must correct the STAT rows' + `RowHighlightSprite` to `0x06000F93` and must NOT touch + `SpellbookRowStyle.cs` — see §2's "SEALED VERDICT" note for the full + anchor chain. 4. Everything else in the plan's "Retail recon" section (the Titles page element roster, the header element ids, the PostInit binding order) checks out exactly against the live DAT — no other corrections. +5. **Several findings above are probe-session observations, not + committed pins** — flagged this fix round so CT2–CT6 don't cite them + as regression-guarded facts: the header block's "two copies + geometrically identical" claim (only fonts/colors are pinned, not + full geometry — §1), the `0x06004CC2` "generic panel chrome" + characterization (§2), and the master-map/category-map dump plus the + `RetailKeyNames` cross-validation table (§5). The title chain's final + two DIDs and end-to-end string resolution ARE pinned + (`TitleStringTable_ResolvesWarMageEndToEnd`). diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterPanelLiveDatTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterPanelLiveDatTests.cs index c8265233..65589b48 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterPanelLiveDatTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterPanelLiveDatTests.cs @@ -22,7 +22,8 @@ namespace AcDream.App.Tests.UI.Layout; public sealed class CharacterPanelLiveDatTests { private static string DatDirectory => - Path.Combine( + Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR") + ?? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); @@ -61,7 +62,14 @@ public sealed class CharacterPanelLiveDatTests Assert.Equal(Vector4.One, name.FontColor); } - foreach (var heritage in Flatten(tree!).Where(e => e.Id == CharacterStatController.HeritageId)) + // CT1 fix round: hoisted to .ToList() + an explicit count assertion. + // A bare `foreach (x in seq.Where(...))` with no count check passes + // vacuously if the importer ever returns an empty sequence — it + // proves nothing about the two Attributes/Skills duplicate chains + // actually being present. + var heritageOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.HeritageId).ToList(); + Assert.Equal(2, heritageOccurrences.Count); + foreach (var heritage in heritageOccurrences) { Assert.Equal(0x40000002u, heritage.FontDid); Assert.Equal(Vector4.One, heritage.FontColor); @@ -70,7 +78,9 @@ public sealed class CharacterPanelLiveDatTests } // Item 4: PK status line is authored PURE WHITE. - foreach (var pk in Flatten(tree!).Where(e => e.Id == CharacterStatController.PkStatusId)) + var pkOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.PkStatusId).ToList(); + Assert.Equal(2, pkOccurrences.Count); + foreach (var pk in pkOccurrences) { Assert.Equal(0x40000002u, pk.FontDid); Assert.Equal(Vector4.One, pk.FontColor); @@ -79,7 +89,9 @@ public sealed class CharacterPanelLiveDatTests // Item 5: level color is a pale gold (~RGB 255/242/127) WITH an // authored outline — CharacterStatController.Gold (1, 0.82, 0.36, 1) // with no outline is a divergent hand-picked runtime color. - foreach (var level in Flatten(tree!).Where(e => e.Id == CharacterStatController.LevelId)) + var levelOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.LevelId).ToList(); + Assert.Equal(2, levelOccurrences.Count); + foreach (var level in levelOccurrences) { Assert.Equal(0x40000010u, level.FontDid); Assert.True(level.Outline); @@ -89,9 +101,14 @@ public sealed class CharacterPanelLiveDatTests Assert.Equal(0.498f, level.FontColor!.Value.Z, precision: 2); } - foreach (var xpLabel in Flatten(tree!).Where(e => e.Id == CharacterStatController.XpNextLabelId)) + var xpLabelOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.XpNextLabelId).ToList(); + Assert.Equal(2, xpLabelOccurrences.Count); + foreach (var xpLabel in xpLabelOccurrences) Assert.Equal(0x40000000u, xpLabel.FontDid); - foreach (var xpValue in Flatten(tree!).Where(e => e.Id == CharacterStatController.XpNextValueId)) + + var xpValueOccurrences = Flatten(tree!).Where(e => e.Id == CharacterStatController.XpNextValueId).ToList(); + Assert.Equal(2, xpValueOccurrences.Count); + foreach (var xpValue in xpValueOccurrences) Assert.Equal(0x40000000u, xpValue.FontDid); } @@ -119,6 +136,13 @@ public sealed class CharacterPanelLiveDatTests }; foreach (ElementInfo listBox in listBoxes) { + // CT1 fix round: authored ListBox rect — CT5's row alignment + // and CT6's resize/scrollbar contract both depend on this. + Assert.Equal(0f, listBox.X); + Assert.Equal(112f, listBox.Y); + Assert.Equal(300f, listBox.Width); + Assert.Equal(160f, listBox.Height); + Assert.Equal(CharacterStatController.ListScrollbarId, listBox.ScrollbarElementId); Assert.Equal(5, listBox.TemplateList.Count); foreach (uint id in expected) @@ -128,6 +152,17 @@ public sealed class CharacterPanelLiveDatTests t => t.TemplateLayoutId == 0x21000045u && t.TemplateElementId == id); } } + + // CT1 fix round: the ListBox's own authored scrollbar rect — the + // "always reserved" gutter CT5/CT6 both depend on. + var scrollbars = Flatten(tree!).Where(e => e.Id == CharacterStatController.ListScrollbarId).ToList(); + Assert.Equal(2, scrollbars.Count); + foreach (ElementInfo scrollbar in scrollbars) + { + Assert.Equal(281f, scrollbar.X); + Assert.Equal(16f, scrollbar.Width); + Assert.Equal(160f, scrollbar.Height); + } } /// @@ -151,9 +186,13 @@ public sealed class CharacterPanelLiveDatTests Assert.Equal(20f, row.Height); Assert.Equal(0x06004CC2u, row.StateMedia["Normal"].File); // Row-template Highlight sprite (0x06000F93) — CharacterStatController's - // current RowHighlightSprite private constant is 0x06001397, a - // divergence flagged (not fixed) by CT1; see the research doc's - // "Corrections to the plan" §3. + // current RowHighlightSprite private constant is 0x06001397. CT1's + // fix round upgraded this from a flagged divergence to a SEALED + // VERDICT (gmAttributeUI::UpdateSelection SetState(6) -> + // InfoRegion::SetState on this exact template); 0x06001397 belongs + // to the spellbook row's separate selected-overlay mechanism. CT5 + // fixes RowHighlightSprite for the stat rows only; see the research + // doc's "Corrections to the plan" §3. Assert.Equal(0x06000F93u, row.StateMedia["Highlight"].File); ElementInfo icon = Assert.Single(row.Children, c => c.Id == 0x10000129u);