acdream/docs/research/2026-06-25-character-window-faithful-spec.md
Erik 16c21e299c fix(ui): match retail vitae and character info
Restore Vitae's omitted penalty paragraph, replace the invented character summary with gmCharacterInfoUI's ordered report and property meanings, preserve authored translucent body surfaces, and initialize the end-session button in its visible Normal DAT state.

Release build and all 5,830 tests pass with five intentional skips. Connected visual gate pending.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-17 12:06:14 +02:00

5 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/playtime/deaths, resist grades, innate attributes, chess/fishing, earned augmentations, load") is a SEPARATE child panel: gmCharacterInfoUI (register 0x004b8c90), root 0x10000183 under the production shared-panel LayoutDesc 0x2100006E. Its authored m_pMainText (0x1000011d) and scrollbar are imported from that child tree; the controller only supplies the report. This is different from 0x2100002E. See 2026-07-17-retail-vitae-character-info-pseudocode.md for the exact report.

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.