acdream/docs/research/2026-06-25-character-window-faithful-spec.md
Erik 0d87e4dc31 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) <noreply@anthropic.com>
2026-06-25 19:48:38 +02:00

82 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 | (curbase)/(capbase), 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<StringTable>`). 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.