Merge claude/hopeful-maxwell-214a12 — D.2b UI Studio + faithful importer + Character window

UI Studio (preview panels through the production renderer), importer dat-fidelity (Fix A/B/C/4/5:
the importer carries dat font/justification/color; boundary look=importer / state=runtime), and the
Character window Attributes tab (reads as retail). 3062 tests green.
Handoff: docs/research/2026-06-26-mockup-stage-handoff.md.

# Conflicts:
#	docs/ISSUES.md
#	docs/architecture/retail-divergence-register.md
This commit is contained in:
Erik 2026-06-26 12:33:51 +02:00
commit ca94b479bf
61 changed files with 125881 additions and 44 deletions

View file

@ -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 | (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.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
# UI Studio — dump-panel render sweep (2026-06-25)
All 26 windows in `docs/research/2026-06-25-retail-ui-layout-dump.json` were rendered through
the studio's dump LayoutSource (`AcDream.App ui-studio --dump <slug> --screenshot <png>`) and
read back as PNGs. Goal: verify the generic dump source renders every retail window, identify
real render bugs vs. expected gaps.
## Verdict
The generic dump source renders **all 26 panels with no crash**. Render fidelity tracks how
much of a panel is **static chrome** (present in the dump, so it draws) vs **runtime content**
(slots, stats, appraise text — NOT in the dump, so it can't draw). This is the dump's nature,
not a studio bug.
## Per-panel status (read from `studio-shots/`)
| Status | Panels |
|--------|--------|
| **Full / great** (frame + static content) | `vitals` (3 bars + heart/glyphs), `side_by_side_vitals`, `toolbar` (all spell/combat icons + backpack), `map`, `main_panel`, `radar` (disc), `character` (frame+tabs+scrollbar), `combat`, `powerbar`, `negative_effects`, `positive_effects`, `options`, `quests`, `social`, `spellbook`, `main_chat`, `floating_chat_1..4`, `environment_panel` |
| **Partial** (chrome only; runtime content absent) | `inventory` — stone backdrop + top frame render, but the nested sub-window chrome (gm3DItemsUI / gmBackpackUI / paperdoll) is sparse and the slots are runtime-filled. **The one panel with a possible real gap — investigate the nested-layout / sub-window node handling.** |
| **Blank** (runtime-only window, no static sprites in dump) | `examine` (appraise window — populated only on examine), and likely the small `indicators` / `smartbox` / `vitae` |
## Tooling shipped this sweep
- `--screenshot <path>` headless mode (`f778b10`): render the loaded panel (dump or dat) to a
PNG off-screen via `PanelFbo` + `glReadPixels` + ImageSharp, then exit. Hidden window
(`IsVisible=false`). The autonomous self-verification primitive.
- Contact-sheet montage via PowerShell `System.Drawing` (one image, 26 thumbs) — the cheap
per-sweep overview (`studio-shots/_contact.png`, gitignored).
## Rect basis (confirmed in `DumpLayout`)
Dump `rect` fields are ABSOLUTE retail screen coords; `DumpLayout` converts to parent-relative
(`child.Left = child.Rect.X - parent.Rect.X`) and places the root at (0,0). Verified correct by
the toolbar (icons land precisely) + vitals.
## Next (loop)
1. **Inventory nested chrome** — determine whether the sub-window content is missing because
(a) those nodes are `Group`s with no static sprite (expected), or (b) `DumpLayout` isn't
expanding a `base_layout_id` sub-layout reference (a fixable bug). Dump the inventory panel's
node `widget_kind`/`rect`/`image_id` list and compare to what rendered.
2. Spot-confirm the remaining blanks (`indicators`/`smartbox`/`vitae`) are runtime-only.
3. The dump source is functionally complete as a static previewer; richer (live-data) previews
are the dat-import + fixture path (the parked Task 4 bags/equip), not the dump path.

View file

@ -0,0 +1,51 @@
# Character window (Attributes tab) — retail reference + polish targets
Transcribed from two user-provided retail screenshots (2026-06-26): the **not-selected** state and the
**Strength-selected** state. The PNGs live in the user's chat; this doc is the durable target spec for
the remaining polish on acdream's `CharacterStatController` (LayoutDesc 0x2100002E). "✗" = acdream gap.
## State 1 — nothing selected
- **Tabs:** Attributes (active, gold label), Skills, Titles.
- **Name:** "Horan" — **WHITE** (✗ acdream renders the name gold).
- **Heritage line:** "Female Aluvian Adventurer" — white.
- **PK line:** "Non-Player Killer" — white.
- **Level area (right):** small caption "Character Level" (two lines: "Character" / "Level"), then the
number **"240" as LARGE GOLD SPRITE DIGITS** — a number-sprite font, not plain text (✗ acdream draws
"126" as plain gold text, and the caption truncates to "Character Le").
- **"Total Experience (XP):"** caption + value "100,000,017,999" (✗ caption missing in acdream).
- **"XP for next level:"** caption + value "99,738,801" with the red fill bar behind it (✗ caption +
value missing — the value element is consumed by the UiMeter at import).
- **Attribute list — 9 rows**, each = icon + name + right-aligned value:
- Row text is **LARGER** (roughly the icon height ~24px) and rows are **TIGHTER** (less vertical gap)
than acdream's current small-text / wide-spacing.
- Order: Strength 200, Endurance 10, Coordination 10, Quickness 200, Focus 10, Self 10,
Health 5/5, Stamina 10/10, Mana 10/10.
- **Footer:** "Select an Attribute to Improve" (title) / "Skill Credits Available: 96" /
"Unassigned Experience: 87,757,321,741". (acdream now matches — confirm the title says **Attribute**,
not skill.)
## State 2 — Strength selected
- Same header.
- **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.)
- **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
numeric/"maxed" placeholder — show "Infinity!" when the attribute is at max).
- "Unassigned Experience:" + "87,757,321,741".
- Raise buttons (≜10 + triangle) shown at the selected row's right.
## Polish checklist (acdream)
1. [ ] Name color → white.
2. [ ] Level → gold sprite digits (find the number-sprite font/ids); caption "Character"/"Level" 2-line.
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.
7. [ ] Selected footer title → white.
8. [ ] Maxed attribute → "Experience To Raise: Infinity!".
9. [ ] Footer title wording = "Select an Attribute to Improve" (Attribute).

View file

@ -0,0 +1,126 @@
# Handoff — the full multi-window UI mockup stage (2026-06-26)
For the next agent. This session built the **UI Studio**, made the **importer faithful to the dat**,
and brought the **Character window** to a retail look. The next stage is the user's actual goal: turn the
studio into a **full interactive multi-window mockup** — several windows live in one host, draggable +
resizable + clickable — so the whole UI can be tested without the game.
Read this, then `claude-memory/project_ui_studio.md` (the SSOT) and `project_d2b_retail_ui.md`.
---
## 1. Where we are (what's done + works)
### The UI Studio (the dev tool)
`AcDream.App ui-studio <datDir> [--layout 0xNNNN | --dump <slug>] [--screenshot <png>]` — a standalone
Silk.NET/GL window that renders a UI panel **through the production renderer** (`RenderBootstrap.Create`
builds a `RenderStack` = the subset of GameWindow's render stack the UI needs; **GameWindow is untouched**).
- Code: `src/AcDream.App/Studio/` (`StudioWindow`, `FixtureProvider`, `PanelFbo`, `DumpLayout`, `UiDumpModel`,
`StudioInspector`, `LayoutSource`) + `src/AcDream.App/Rendering/RenderBootstrap.cs`.
- **Two panel sources:** `--layout 0xNNNN` (real dat-import via `LayoutImporter`, populated by the PRODUCTION
controllers through `FixtureProvider` + `SampleData`), and `--dump <slug>` (the 26-window retail dump,
`docs/research/2026-06-25-retail-ui-layout-dump.json`, static structure only).
- **Headless** `--screenshot <png>` (PanelFbo → glReadPixels → PNG) — the autonomous self-verify primitive.
Use it constantly.
- **Interactive:** click-routing forwards the ImGui canvas mouse → the panel's `UiHost` (coord map =
`mouse ImGui.GetItemRectMin()`, no extra Y-flip). An **Interact / Inspect** toggle in the toolbar:
Interact = clicks drive the panel; Inspect = clicks select elements in the tree.
### The faithful importer (the "better way" — the load-bearing win)
The treadmill of hand-tuning each panel's look in C# was because the importer DROPPED the dat's per-element
visual properties. The dat carries them in `StateDesc.Properties`: **0x1A FontDID**, **0x14/0x15** H/V
justification, **0x1B FontColor**. We wired them in, **backward-compatibly** (the importer applies them as
DEFAULTS at build time; a controller that still sets them explicitly overrides → existing panels
byte-unchanged):
- **Fix A** `4143042` — justification → `UiText.Centered/RightAligned/VerticalJustify`.
- **Fix B** `6e0be4b` — FontColor → `UiText.DefaultColor` (character colors proved RUNTIME — the dat carries none).
- **Fix C** `a0d3395` — FontDid → per-element font via a resolver (`Func<uint,UiDatFont?>`) threaded through
`LayoutImporter.Import``DatWidgetFactory`. **STUDIO path only**; GameWindow passes null (issue **#157**).
- **Fix 5** `ad4ed51``LayoutImporter.BuildWidget` builds a `UiMeter`'s non-slice (text) children while
still consuming the Type-3 slice containers (vitals unaffected).
- **Fix 4** `1b9dd6c` — investigated, NO fix: the dat has no Visible flag; runtime gm*UI is correct.
**THE BOUNDARY (the key conceptual result):** the importer faithfully carries an element's **LOOK**
(font, justification, color, position) from the dat. **STATE + BEHAVIOR** (which group is visible, tab /
selection activation) is genuinely **runtime gm*UI logic** and belongs in the controller — it is NOT an
importer gap (Fix 4 + the Fix-5 tab-skip proved this). Don't try to push state/visibility into the importer.
### The Character window (the proof panel)
LayoutDesc `0x2100002E`, `src/AcDream.App/UI/Layout/CharacterStatController.cs` — Attributes tab reads as
retail: 3 tabs, header, big-gold level number (its own dat FontDid), captions, 9-row attribute list
(icons + right-aligned values + Health/Stamina/Mana), click-to-select (top/bottom selection bars + footer
State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered
footer. **User accepted it as good-enough 2026-06-26** ("still needs some polish for later" — see issue for
deferred items). Specs: `docs/research/2026-06-25-character-window-faithful-spec.md`,
`2026-06-25-attributes-tab-interactive-spec.md`, `2026-06-26-character-window-retail-reference.md`.
### Other panels with production controllers (the mockup's building blocks)
`VitalsController` (0x2100006C), `ToolbarController` (0x21000016), `InventoryController` + `PaperdollController`
(0x21000023), `ChatWindowController`, `CharacterStatController` (0x2100002E). All bind real importer-mounted
elements via `FindElement(datId)` (note: `UiElement.EventId` ≠ the dat id — the dat id lives in
`ImportedLayout._byId`).
---
## 2. The mockup goal + proposed approach
**Goal:** the studio shows ONE panel today. The mockup shows the **whole UI at once** — multiple windows in
one host, each draggable + resizable + clickable — a real testbed.
**Why it's reachable:** the production `UiHost` (`src/AcDream.App/UI/UiHost.cs`) ALREADY manages multiple
draggable/resizable windows and routes input — it's the game's actual UI layer. The studio just needs to host
the full thing instead of one FBO'd panel. (Per memory, the game has a window manager: F12 toggles inventory;
there's a `UiHost.ToggleWindow` / `WindowNames` API.)
**Proposed path (a new studio "mockup mode"):**
1. Load SEVERAL panels into ONE `UiHost` (not one panel per FBO) — a "desktop." Render the host directly to
the studio GL window (not the inspector FBO), so native input/drag/resize work.
2. Window open/close/arrange (reuse the game's window-manager API).
3. Drag + resize via the production frame chrome (already exists for the retail windows).
4. Keep the inspector available as an optional overlay.
The studio is the THIN host; the production `UiHost` is the engine. Don't reimplement window management —
drive the production one.
---
## 3. Must-fix before / during the mockup
- **#156** — the **inventory window (0x21000023) renders all-black in the studio** (pre-existing fixture gap,
NOT a regression). It's a key window for the mockup; fix the studio's inventory fixture/sub-window setup.
- **#157** — GameWindow doesn't thread the Fix-C font resolver (live game = global font). Turnkey (the issue
has the exact 4 import sites + the `ConcurrentDictionary` + `GetOrAdd` pattern).
- **Character deferred polish** — the user flagged "some polish for later" (level-glyph fidelity, icon
crispness, sample strings). Filed as an issue; the user will enumerate.
## 4. Patterns + lessons (don't relearn these the hard way)
- **Faithful-panel pattern:** bind real importer-mounted elements via `FindElement(datId)`; `EventId` ≠ dat id.
- **Importer dat-fidelity:** the look comes from the dat; when re-deriving fonts/colors/positions in code feels
like a treadmill, AUDIT what the dat element carries vs what the importer drops, then wire it backward-compat.
- **The boundary:** look = importer; state/behavior = runtime controller. Don't fight it.
- **Apparatus over speculation:** for a layout bug, DUMP the actual rendered rects (walk the tree, print
abs rect + visible + text). One position dump cracked the footer-overlap after FIVE speculative passes.
cdb is for runtime *behavior* questions, not "why is our element at the wrong place."
- **Watch subagents for scope creep:** the Fix-C subagent finished its task then committed unrequested AP-58 +
#148 (reverted per the user, kept in reflog `46c5cd2`/`82e13b7`). Constrain dispatches to the assigned task.
- **Shared-infra changes:** regression-screenshot vitals + toolbar (the canaries) every time; chat is the one
panel the studio can't easily preview, so never change global `UiText`/`UiMeter` behavior without verifying it.
## 5. Reference map
- Studio spec/plan: `docs/superpowers/specs/2026-06-25-ui-studio-previewer-design.md`,
`docs/superpowers/plans/2026-06-25-ui-studio-previewer.md`; dump sweep `docs/research/2026-06-25-studio-dump-panel-sweep.md`.
- Character window: the three specs above + the retail reference.
- Importer audit (this session): the dat carries FontDID/justification/FontColor in `StateDesc.Properties`;
`ElementReader`/`LayoutImporter`/`DatWidgetFactory`/`UiText` are the wiring path.
- The retail UI dump: `docs/research/2026-06-25-retail-ui-layout-dump.json` (26 windows, real ids + rects + sprites).
- Deferred studio features (the original plan, Tasks 5-8): live 3-D doll, markup hot-reload, editable props,
camera sliders — secondary to the mockup.
- Memory SSOT: `claude-memory/project_ui_studio.md`, `project_d2b_retail_ui.md`.
## 6. First concrete steps for the next agent
1. Fix **#156** (inventory studio render) — you need the inventory window for any real mockup.
2. Prototype **mockup mode**: render the production `UiHost` (with vitals + toolbar + character + chat opened)
directly to the studio GL window, native input. Confirm drag + resize + click work through the production host.
3. Then iterate window open/close/arrange + the desktop layout.