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
|
||||
`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`).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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<DatReaderWriter.DBObjs.EnumMapper>`).
|
||||
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`).
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue