From 0d87e4dc31a7af80103fed322e001c5a8c9d2288 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 25 Jun 2026 19:48:38 +0200 Subject: [PATCH] docs(studio): faithful character-window element spec (decomp-grounded) Element map for LayoutDesc 0x2100002E (the tabbed Attributes/Skills/Titles window) + the importer-mount reality + the StringTable verdict + the follow-up list. Grounds the CharacterStatController work in 0e644b5 and the remaining refinements. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...26-06-25-character-window-faithful-spec.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/research/2026-06-25-character-window-faithful-spec.md diff --git a/docs/research/2026-06-25-character-window-faithful-spec.md b/docs/research/2026-06-25-character-window-faithful-spec.md new file mode 100644 index 00000000..9f19e56a --- /dev/null +++ b/docs/research/2026-06-25-character-window-faithful-spec.md @@ -0,0 +1,82 @@ +# Character window — faithful element spec (LayoutDesc 0x2100002E) + +Decomp-grounded blueprint for the Character window, produced to REDO the guessed pilot. +Sources: `docs/research/named-retail/acclient_2013_pseudo_c.txt` (PDB-named decomp), +the user UI dump `docs/research/2026-06-25-retail-ui-layout-dump.json`, and acdream's own +importer-resolved tree (dumped live in the studio). **No guessing** — every row cites a +decomp address or a confirmed `FindElement` result. + +## What 0x2100002E actually is + +A **tabbed Attributes / Skills / Titles window** (300×600, root panel `0x10000227`, Type 8). +It is NOT a text report. Three tab-content slots each mount a sub-layout via +`BaseElement`+`BaseLayoutId` inheritance (acdream's `ShouldMountBaseChildren` path): + +| Slot | BaseElement | BaseLayoutId | gm*UI | Register | +|---|---|---|---|---| +| `0x1000022B` (Attributes) | `0x10000225` | `0x2100002C` | gmAttributeUI (root type 0x1000002A) | 0x0049db50 | +| `0x1000022C` (Skills) | `0x1000022E` | `0x2100002D` | gmSkillUI | 0x0049adb0 | +| `0x10000539` (Titles) | `0x1000052D` | `0x2100005E` | gmCharacterTitleUI | 0x0049aba0 | + +Both Attributes + Skills tabs share a header strip filled by the base class +**`gmStatManagementUI`** (`UpdateCharacterInfo` 0x004f0770, `UpdateExperience` 0x004f0a70, +`UpdatePKStatus` 0x004f00a0). The tab sub-layout `0x2100002C` nests `0x10000226` which +chains again into `0x21000045` — the real header/list content is **two inheritance levels deep**. + +The **text report** ("birth/age/deaths, innate attributes, augmentations, load") the earlier +pilot guessed is a SEPARATE sub-panel: LayoutDesc `0x2100001A`, `gmCharacterInfoUI` +(register 0x004b8c90), whose `m_pMainText` (`0x1000011d`) is created at RUNTIME. That window +is handled by `CharacterController` (create-the-element path) — different layout, not 0x2100002E. + +## Importer reality (dumped live, 2026-06-25) + +- `dump-vitals-layout` prints the RAW dat (no inheritance) → the content elements look "missing". + That is an artifact. The **importer** mounts them. Confirmed by `FindElement` on the studio's + imported 0x2100002E: + - `0x10000231` name → `UiText` ✓ + - `0x10000232` heritage → `UiText` ✓ + - `0x10000233` PK status → `UiText` ✓ + - `0x1000023B` level → `UiText` ✓ + - `0x10000235` total XP → `UiText` ✓ + - `0x10000236` XP-to-level meter → `UiMeter` ✓ + - `0x1000023D` list box → `UiDatElement` (generic container) ✓ + - `0x10000238` XP-to-level text → **NULL** (nested variant; not mounted — minor) +- `UiElement.EventId` is NOT the dat element id (root shows 0x10000001, not 0x10000227). The dat + id lives in `ImportedLayout._byId` (set from `info.Id`, LayoutImporter:107). Bind via + `FindElement(id)`, never by reading `widget.EventId`. + +## Header element map (gmStatManagementUI) + +| Id | Field | Content source | Rect (in sub-layout) | +|---|---|---|---| +| `0x10000231` | m_pNameText | player full name | 0,25,230,20 | +| `0x10000232` | m_pHeritageText | gender+heritage display (InqGenderHeritageDisplay) | 0,45,230,15 | +| `0x10000233` | m_pPKStatusText | PK status string (StringTable) | 0,60,230,15 | +| `0x1000023B` | m_pLevelText | PropertyInt 0x19 (level) | 235,60,65,50 | +| `0x10000234` | (label) | "Total Experience:" | 0,95,130,18 | +| `0x10000235` | m_pTotalXPText | PropertyInt64 1 | 130,95,100,18 | +| `0x10000236` | m_pXPToLevelMeter | (cur−base)/(cap−base), img 0x060011A6 | 0,113,230,17 | +| `0x10000237` | (label) | "To next level:" | 0,113,130,17 | +| `0x10000238` | m_pXPToLevelText | XP remaining | 130,113,100,17 | +| `0x1000023D` | m_pListBox | attribute/skill rows (runtime) | 0,137,300,398 | + +Attribute list rows: gmAttributeUI builds `AttributeInfoRegion` rows (name + base/buffed value + +raise button). Canonical AC display order: Strength, Endurance, Coordination, Quickness, Focus, Self. + +## StringTable + +`DatReaderWriter` CAN read `StringTable` (CLI uses `GetAllIdsOfType`). The decomp's +`StringInfo::SetStringIDandTableEnum(…, stringId, 0x10000001)` references table enum 0x10000001 → +the game string table dat (0x31000001 family); StringId=0 is a placeholder overwritten by a +preceding `compute_str_hash("ID_…")`. To reproduce exact wording: read the StringTable + a +StringInfo helper (hash → id → lookup, `AddVariable_*` substitution). NOT yet ported — current +controller uses canonical AC labels. + +## Status (this session) + +- **DONE (commit 0e644b5):** `CharacterStatController` binds name/heritage/PK/level/total-XP + + the XP meter fill + the six attributes into the real elements. Studio renders the real panel. +- **Follow-ups (known sources):** (1) tab-button active/inactive STATE so the 3 tabs draw their + sprites (0x06005D92-97); (2) StringTable wording; (3) full AttributeInfoRegion row template + (column-aligned values + per-attribute raise buttons); (4) wire `0x10000234`/`0x10000237` + labels + the right-side level area placement.