docs(CT): CT1 fix round — sealed RowHighlightSprite verdict, verified resize mechanism, strengthened pins
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 <noreply@anthropic.com>
This commit is contained in:
parent
d3877f1c0e
commit
e264d8392f
4 changed files with 297 additions and 65 deletions
|
|
@ -40,10 +40,19 @@ land; every deviation a slice introduces adds its row in the same commit.
|
||||||
`AddTitleToList @0x0049A840` resolves the display string via
|
`AddTitleToList @0x0049A840` resolves the display string via
|
||||||
`CharacterTitleTable::GetCharacterTitleFromID` (DAT title-string
|
`CharacterTitleTable::GetCharacterTitleFromID` (DAT title-string
|
||||||
table — CT2 locates the DID) and inserts SORTED
|
table — CT2 locates the DID) and inserts SORTED
|
||||||
(`FindSortedInsertPosition @0x0049A760`).
|
(`FindSortedInsertPosition @0x0049A760`). It writes the resolved title
|
||||||
- `UpdateButtons @0x0049A500`: the display button GHOSTS (state 0xd)
|
text into row child `0x10000537` and stamps the row's id via
|
||||||
when the selected row's title id == `mDisplayTitle`; Normal (1)
|
`SetAttribute_Enum(row, 0x1000008E, titleId)`, inserting the row via
|
||||||
otherwise. Selection change (msg 4/0x43) re-runs it.
|
`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
|
- Clicking `0x10000535` sends
|
||||||
`CM_Social::Event_SetDisplayCharacterTitle(titleId)`
|
`CM_Social::Event_SetDisplayCharacterTitle(titleId)`
|
||||||
(`ListenToElementMessage @0x0049A6D0`).
|
(`ListenToElementMessage @0x0049A6D0`).
|
||||||
|
|
|
||||||
|
|
@ -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 (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
|
selected-row sprite `0x06001397` (Button state 6). (✗ acdream uses a translucent GOLD tint — replace
|
||||||
with the dark-bar sprite.)
|
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:**
|
- **Footer flips to:**
|
||||||
- Title: **"Strength: 200"** — **WHITE** text (✗ acdream uses the body/gold color).
|
- Title: **"Strength: 200"** — **WHITE** text (✗ acdream uses the body/gold color).
|
||||||
- "Experience To Raise:" + **"Infinity!"** (Strength is maxed → cost is infinite; ✗ acdream shows a
|
- "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.
|
3. [ ] Add "Total Experience (XP):" caption.
|
||||||
4. [ ] Add "XP for next level:" caption + value (un-consume from the meter, or render alongside).
|
4. [ ] Add "XP for next level:" caption + value (un-consume from the meter, or render alongside).
|
||||||
5. [ ] Row text larger (≈icon height) + rows tighter.
|
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.
|
7. [ ] Selected footer title → white.
|
||||||
8. [ ] Maxed attribute → "Experience To Raise: Infinity!".
|
8. [ ] Maxed attribute → "Experience To Raise: Infinity!".
|
||||||
9. [ ] Footer title wording = "Select an Attribute to Improve" (Attribute).
|
9. [ ] Footer title wording = "Select an Attribute to Improve" (Attribute).
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,21 @@ through the Attributes page chain (`0x10000227 > 0x1000022B > 0x10000226 >
|
||||||
0x10000230`) and one through the Skills page chain (`... > 0x1000022C >
|
0x10000230`) and one through the Skills page chain (`... > 0x1000022C >
|
||||||
...`). **The two copies are geometrically and stylistically IDENTICAL** —
|
...`). **The two copies are geometrically and stylistically IDENTICAL** —
|
||||||
this is the "duplicated stat-management branches" quirk documented on
|
this is the "duplicated stat-management branches" quirk documented on
|
||||||
`PrepareSkillScrollbar` in `CharacterStatController.cs`, confirmed here to
|
`PrepareSkillScrollbar` in `CharacterStatController.cs`. **Caveat: this
|
||||||
be a harmless duplicate (not a divergent one) for every header id checked.
|
"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 |
|
| 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
|
— this is the "authored margin between the value column and the
|
||||||
border" the owner reported (item 2), confirmed as exactly 7px at the
|
border" the owner reported (item 2), confirmed as exactly 7px at the
|
||||||
row-template level.
|
row-template level.
|
||||||
- **The row (282px) is already inset from the ListBox's full width
|
- **The bare authored rectangles** (no derived arithmetic — see the
|
||||||
(300px) by 18px** to clear the always-reserved scrollbar gutter
|
caveat below): ListBox `0x1000023D` is `X=0 Y=112 W=300 H=160`; its
|
||||||
(scrollbar at X=281, W=16) — so the TOTAL space between the value
|
scrollbar `0x1000023E` is `X=281 W=16 H=160`; the data row template is
|
||||||
text's right edge and the ListBox's outer right edge is
|
`W=282`; the value column inside the row is `X=175 W=100`,
|
||||||
`300 - 275 = 25px`, of which 18px is the permanent scrollbar gutter and
|
right-justified, 7px from the row's own right edge (`282 - (175+100) =
|
||||||
7px is the row template's own inset. Both numbers matter for CT5:
|
7`).
|
||||||
the row width (282, confirmed correct — matches the existing
|
- **These numbers do NOT compose into a tidy "gutter" story — do not
|
||||||
`SkillContentWidth = 282f` constant already in the code) and the
|
infer one.** `300 - 281 = 19`, not the row's 282px width's complement
|
||||||
internal 175/100 value-column placement (not currently matched).
|
(`300 - 282 = 18`); and the 282px row actually OVERLAPS the 281px
|
||||||
- Row background: `Normal` state file `0x06004CC2` (the same generic
|
scrollbar band by 1px (`281 < 282`). An earlier draft of this doc
|
||||||
panel-chrome fill used elsewhere client-wide), `Highlight` state file
|
described the row as "inset by 18px to clear an always-reserved
|
||||||
**`0x06000F93`**. `CharacterStatController.RowHighlightSprite` is
|
scrollbar gutter" and summed 18+7=25 — that decomposition does not
|
||||||
currently `0x06001397u` — **this is a divergent constant**; CT5 should
|
close against the authored numbers above and is **inference, not
|
||||||
either confirm `0x06001397` is deliberately used for a DIFFERENT
|
fact**; withdrawn. CT5 must implement the authored numbers directly
|
||||||
highlight surface (e.g. the vitals/skill list uses a shared sprite
|
(row width 282 — matches the existing `SkillContentWidth = 282f`
|
||||||
elsewhere) or correct it to `0x06000F93` for the attribute/skill row
|
constant already in the code; value column X=175 W=100; scrollbar
|
||||||
highlight specifically. Flagged, not fixed, in this slice.
|
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
|
### 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.
|
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)
|
## 3. Titles page (0x10000539 subtree, imported as part of 0x2100002E)
|
||||||
|
|
||||||
`0x10000539` (Type 0x10000046, the Titles page container) is NOT
|
`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).
|
the scrollbar column).
|
||||||
|
|
||||||
Same targeted-root import gotcha as the stat templates applies here:
|
Same targeted-root import gotcha as the stat templates applies here:
|
||||||
`LayoutImporter.ImportInfos(dats, 0x2100005Eu)` (the whole-layout overload)
|
`LayoutImporter.ImportInfos(dats, 0x2100005Eu)` (the whole-layout
|
||||||
returns the SAME element (0x1000052D, root of a throwaway container) but
|
overload) returns the SAME element `0x1000052D`, but does NOT surface
|
||||||
does NOT surface 0x10000536 as a reachable child — use
|
`0x10000536` as a reachable child. **`0x1000052D` is not a throwaway
|
||||||
`LayoutImporter.ImportInfos(dats, 0x2100005Eu, 0x10000536u)` instead.
|
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
|
## 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
|
0x2100002E is CONTENT ONLY (tab bar + pages), with retail's window chrome
|
||||||
supplied by a separate mechanism.
|
supplied by a separate mechanism.
|
||||||
|
|
||||||
**Implication for CT6:** the character window's authored minimum height
|
Treat the "Already in-tree" plan bullet as **inaccurate**: start CT6
|
||||||
(if one exists in retail) is not going to fall out of 0x2100002E's own
|
from the decomp for the window-frame class instead of assuming the
|
||||||
`MinHeight`/`MaxHeight` properties — those are simply absent. CT6 needs
|
wiring is already 90% done. The paragraph below replaces this doc's
|
||||||
to find where retail's `gmStatManagementUI`/its owning window-frame class
|
earlier "likely a hardcoded ResizeTo/SetMinSize call" guess with the
|
||||||
enforces a minimum window size (likely a hardcoded `ResizeTo`/`SetMinSize`
|
verified mechanism.
|
||||||
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
|
### Verified resize mechanism (2026-08-24, supersedes the hypothesis above)
|
||||||
for every registered window"). This is NOT a simple "read the DAT
|
|
||||||
property" fix like `MountSideVitals` was; treat the "Already in-tree"
|
`UIElement::ResizeTo @0x00463C30` clamps ONLY via element attributes —
|
||||||
plan bullet as **inaccurate** and start CT6 from the decomp for the
|
`0x3C` (clamp-max-height), `0x3E` (clamp-min-height), `0x3D`
|
||||||
window-frame class instead of assuming the wiring is already 90% done.
|
(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`)
|
## 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)
|
### 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`,
|
`DatReaderWriter.DBObjs.EnumIDMap` (ACE's historical name: `DidMapper`,
|
||||||
file-type byte `0x25`) is the object type both master and category maps
|
file-type byte `0x25`) is the object type both master and category maps
|
||||||
use; `DatReaderWriter.DBObjs.EnumMapper` (file-type byte `0x22` on the
|
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
|
map → target DID) — CT2 can either hardcode the two resolved DIDs
|
||||||
(`0x22000041` for the EnumMapper, `0x2300000E` for the StringTable, the
|
(`0x22000041` for the EnumMapper, `0x2300000E` for the StringTable, the
|
||||||
way `RetailKeyNames` hardcodes its three) or port the two-level
|
way `RetailKeyNames` hardcodes its three) or port the two-level
|
||||||
indirection generically. Given `RetailKeyNames` already established the
|
indirection generically. **A THIRD consumer has now appeared** (found
|
||||||
"just hardcode the resolved DIDs, cite the probe" precedent for this
|
this fix round): `gmAttributeUI::PostInit @0x0049DB70` resolves
|
||||||
exact category-4 family, CT2 should follow the same precedent unless a
|
per-attribute icon DIDs via `DBObj::GetDIDByEnum(statEnum, category
|
||||||
THIRD consumer of `GetDIDByEnum` appears that would justify factoring
|
0x10000002)` — see the "Row instantiation + icon-DID anchors" note in
|
||||||
out a shared helper.
|
§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
|
2. `EnumMapper.IdToStringMap[titleId]` → raw canonical name (already
|
||||||
readable via `dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumMapper>`).
|
readable via `dats.Portal.TryGet<DatReaderWriter.DBObjs.EnumMapper>`).
|
||||||
3. `DatStringResolver.ComputeHash(rawName)` (already exists, no new code).
|
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)
|
## Corrections to the plan (summary)
|
||||||
|
|
||||||
1. **Window constraints are NOT already 90% wired.** The plan's
|
1. **Window constraints are NOT already 90% wired — and the clamp
|
||||||
"Already in-tree" bullet claims `DatConstraintSource` registration for
|
mechanism is now VERIFIED, not guessed.** The plan's "Already in-tree"
|
||||||
the character window; the actual `MountCharacter()` call sets no such
|
bullet claims `DatConstraintSource` registration for the character
|
||||||
field, and the DAT layout itself authors no MinHeight/MaxHeight on its
|
window; the actual `MountCharacter()` call sets no such field, and the
|
||||||
root to source one from even if it were wired. CT6 needs decomp
|
DAT layout itself authors no MinHeight/MaxHeight on its root to source
|
||||||
research into where retail's authored minimum for this specific window
|
one from even if it were wired. `UIElement::ResizeTo @0x00463C30`
|
||||||
actually lives (likely a class-level constant/behavior, not a
|
clamps only via element attributes `0x3C`–`0x3F`, which nothing writes
|
||||||
per-window DAT property) before it can port anything.
|
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
|
2. **The row-template elements are not walkable via the normal
|
||||||
`ImportInfos(dats, layoutId)` overload.** `#375`'s prototype-skip logic
|
`ImportInfos(dats, layoutId)` overload.** `#375`'s prototype-skip logic
|
||||||
deliberately excludes same-layout template-list targets from the built
|
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`) —
|
(`LayoutDesc 0x21000045`) and `0x10000536` (`LayoutDesc 0x2100005E`) —
|
||||||
documented here so CT5 doesn't waste a cycle rediscovering the same
|
documented here so CT5 doesn't waste a cycle rediscovering the same
|
||||||
"NOT FOUND" dead end this slice hit first.
|
"NOT FOUND" dead end this slice hit first.
|
||||||
3. **`RowHighlightSprite` may already be wrong.** The DAT's row-template
|
3. **`RowHighlightSprite` IS wrong — SEALED, not merely flagged.** The
|
||||||
Highlight state uses `0x06000F93`; the current constant in
|
DAT's row-template Highlight state is `0x06000F93`, reached via
|
||||||
`CharacterStatController.cs` is `0x06001397`. Not fixed in this slice
|
`gmAttributeUI::UpdateSelection`'s `SetState(6)` →
|
||||||
(no production changes); flagged for CT5's review.
|
`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
|
4. Everything else in the plan's "Retail recon" section (the Titles page
|
||||||
element roster, the header element ids, the PostInit binding order)
|
element roster, the header element ids, the PostInit binding order)
|
||||||
checks out exactly against the live DAT — no other corrections.
|
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`).
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,8 @@ namespace AcDream.App.Tests.UI.Layout;
|
||||||
public sealed class CharacterPanelLiveDatTests
|
public sealed class CharacterPanelLiveDatTests
|
||||||
{
|
{
|
||||||
private static string DatDirectory =>
|
private static string DatDirectory =>
|
||||||
Path.Combine(
|
Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR")
|
||||||
|
?? Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||||
"Documents", "Asheron's Call");
|
"Documents", "Asheron's Call");
|
||||||
|
|
||||||
|
|
@ -61,7 +62,14 @@ public sealed class CharacterPanelLiveDatTests
|
||||||
Assert.Equal(Vector4.One, name.FontColor);
|
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(0x40000002u, heritage.FontDid);
|
||||||
Assert.Equal(Vector4.One, heritage.FontColor);
|
Assert.Equal(Vector4.One, heritage.FontColor);
|
||||||
|
|
@ -70,7 +78,9 @@ public sealed class CharacterPanelLiveDatTests
|
||||||
}
|
}
|
||||||
|
|
||||||
// Item 4: PK status line is authored PURE WHITE.
|
// 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(0x40000002u, pk.FontDid);
|
||||||
Assert.Equal(Vector4.One, pk.FontColor);
|
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
|
// Item 5: level color is a pale gold (~RGB 255/242/127) WITH an
|
||||||
// authored outline — CharacterStatController.Gold (1, 0.82, 0.36, 1)
|
// authored outline — CharacterStatController.Gold (1, 0.82, 0.36, 1)
|
||||||
// with no outline is a divergent hand-picked runtime color.
|
// 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.Equal(0x40000010u, level.FontDid);
|
||||||
Assert.True(level.Outline);
|
Assert.True(level.Outline);
|
||||||
|
|
@ -89,9 +101,14 @@ public sealed class CharacterPanelLiveDatTests
|
||||||
Assert.Equal(0.498f, level.FontColor!.Value.Z, precision: 2);
|
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);
|
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);
|
Assert.Equal(0x40000000u, xpValue.FontDid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,6 +136,13 @@ public sealed class CharacterPanelLiveDatTests
|
||||||
};
|
};
|
||||||
foreach (ElementInfo listBox in listBoxes)
|
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(CharacterStatController.ListScrollbarId, listBox.ScrollbarElementId);
|
||||||
Assert.Equal(5, listBox.TemplateList.Count);
|
Assert.Equal(5, listBox.TemplateList.Count);
|
||||||
foreach (uint id in expected)
|
foreach (uint id in expected)
|
||||||
|
|
@ -128,6 +152,17 @@ public sealed class CharacterPanelLiveDatTests
|
||||||
t => t.TemplateLayoutId == 0x21000045u && t.TemplateElementId == id);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -151,9 +186,13 @@ public sealed class CharacterPanelLiveDatTests
|
||||||
Assert.Equal(20f, row.Height);
|
Assert.Equal(20f, row.Height);
|
||||||
Assert.Equal(0x06004CC2u, row.StateMedia["Normal"].File);
|
Assert.Equal(0x06004CC2u, row.StateMedia["Normal"].File);
|
||||||
// Row-template Highlight sprite (0x06000F93) — CharacterStatController's
|
// Row-template Highlight sprite (0x06000F93) — CharacterStatController's
|
||||||
// current RowHighlightSprite private constant is 0x06001397, a
|
// current RowHighlightSprite private constant is 0x06001397. CT1's
|
||||||
// divergence flagged (not fixed) by CT1; see the research doc's
|
// fix round upgraded this from a flagged divergence to a SEALED
|
||||||
// "Corrections to the plan" §3.
|
// 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);
|
Assert.Equal(0x06000F93u, row.StateMedia["Highlight"].File);
|
||||||
|
|
||||||
ElementInfo icon = Assert.Single(row.Children, c => c.Id == 0x10000129u);
|
ElementInfo icon = Assert.Single(row.Children, c => c.Id == 0x10000129u);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue