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

4.8 KiB
Raw Blame History

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.