docs(D.2b): LayoutDesc importer design spec (data-driven retail windows)
This commit is contained in:
parent
0f55599ba5
commit
64146bfc2a
1 changed files with 216 additions and 0 deletions
216
docs/superpowers/specs/2026-06-15-layoutdesc-importer-design.md
Normal file
216
docs/superpowers/specs/2026-06-15-layoutdesc-importer-design.md
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
# LayoutDesc Importer — Design
|
||||||
|
|
||||||
|
**Date:** 2026-06-15
|
||||||
|
**Status:** Approved (brainstorm) — pending spec review → implementation plan
|
||||||
|
**Track:** D.2b retail UI engine (next sub-phase; register the phase id in the roadmap before implementation)
|
||||||
|
**Supersedes nothing. Deletes nothing.** Coexists with the existing hand-authored path.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
D.2b shipped a working retail vitals window and a scrollable chat window, but each was
|
||||||
|
built by **hand**: dump the dat `LayoutDesc`, transcribe sprite ids + rects into
|
||||||
|
`vitals.xml` / `UiMeter` / `UiNineSlicePanel`, then discover-and-patch missing details
|
||||||
|
(the bar fill model, the dat-font, the tiling, the resize-grip overlay) one at a time.
|
||||||
|
That archaeology does not scale to AC's dozens of windows, and it keeps *missing* details
|
||||||
|
that are already in the dat (the grip overlay was found only because the user spotted it).
|
||||||
|
|
||||||
|
The `LayoutDesc` dat is a **complete, declarative description of every window** — element
|
||||||
|
tree, positions, sizes, anchors, sprites per state, draw-modes, fonts, borders, grips,
|
||||||
|
meters, labels, inheritance. It is retail's "HTML for windows." The fix is to **render the
|
||||||
|
dat** with one faithful interpreter rather than transcribe it per window.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Build a faithful `LayoutDesc` interpreter that reads a retail layout from the dat and
|
||||||
|
produces a `UiElement` tree the existing toolkit renders — so opening any retail window is
|
||||||
|
one call, with **no per-window graphics/layout code**. The only per-window code is live
|
||||||
|
**data wiring** (which is inherently per-window and tiny).
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Re-porting Keystone's C++ framework (its own renderer, string/container classes, vtable
|
||||||
|
dispatch, D3D blits). We port retail's **render algorithms**, not its framework — that is
|
||||||
|
what Silk.NET + .NET already provide. (See "Decisions → Structure".)
|
||||||
|
- Deleting or rewriting the existing toolkit/widgets/markup. They are reused.
|
||||||
|
|
||||||
|
## Decisions (from brainstorm 2026-06-15)
|
||||||
|
|
||||||
|
1. **Proof target = re-drive vitals.** Point the importer at the vitals `LayoutDesc`
|
||||||
|
(`0x2100006C`) and make it reproduce the hand-built window. Known-good baseline → clean
|
||||||
|
pass/fail. The hand-authored vitals path stays as the reference until the importer matches.
|
||||||
|
2. **Scope = full faithful interpreter.** Interpret the *complete* `LayoutDesc` format
|
||||||
|
(every element type, full `BaseElement`/`BaseLayoutId` inheritance, all draw-modes,
|
||||||
|
states, properties) — not just the slice vitals uses. Matches the project's
|
||||||
|
"behavior is retail" ethos.
|
||||||
|
3. **Structure = hybrid (Approach C).** Port each element type's render **algorithm**
|
||||||
|
verbatim from the decomp, onto our modern draw primitives. A single generic renderer
|
||||||
|
handles the trivial "stamp the sprite per draw-mode" types (the long tail, including
|
||||||
|
types not yet catalogued); dedicated widgets handle types with real behavior (meter,
|
||||||
|
text, scrollbar/chat, button). The decomp's render method for each type *decides* which
|
||||||
|
bucket it falls in — we do not guess. Faithfulness comes from porting the algorithms;
|
||||||
|
the hybrid is only about C# packaging.
|
||||||
|
4. **Coexistence, don't-delete.** `MarkupDocument` stays as the path for plugin/custom
|
||||||
|
panels (no dat layout). The existing widgets (`UiMeter`, `UiNineSlicePanel`,
|
||||||
|
`UiChatView`, `UiDatFont`) and primitives (tiling, scissor-fill, dat-font, nine-slice)
|
||||||
|
become the importer's behavioral renderers.
|
||||||
|
|
||||||
|
## Architecture & data flow
|
||||||
|
|
||||||
|
```
|
||||||
|
RETAIL WINDOWS (data-driven from the dat)
|
||||||
|
client_portal.dat ─► LayoutImporter ─► UiElement tree ─► UiRoot ─► renderers ─► screen
|
||||||
|
(LayoutDesc 0x21..) │ (UiDatElement +
|
||||||
|
│ behavioral widgets)
|
||||||
|
├─ resolve BaseElement / BaseLayoutId inheritance
|
||||||
|
├─ walk ElementDesc tree → widget (hybrid factory)
|
||||||
|
└─ apply rect / anchors / states / media / props from the dat
|
||||||
|
|
||||||
|
per-window Controller ─► binds LIVE data to elements by id (mirrors retail gm*UI)
|
||||||
|
WindowManager ─► open/close by layout id, z-order, focus, position persistence
|
||||||
|
|
||||||
|
PLUGIN / CUSTOM PANELS (hand-authored, unchanged)
|
||||||
|
*.xml ─► MarkupDocument ─► UiElement tree ─► (same UiRoot + renderers)
|
||||||
|
```
|
||||||
|
|
||||||
|
Two input paths (dat importer for retail windows, markup for custom/plugin), one rendering
|
||||||
|
toolkit. Nothing in the bottom (`UiHost`/`UiRoot`/`UiElement`) or the render primitives
|
||||||
|
changes.
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### 1. Format enumeration (Step 0 — foundational groundwork)
|
||||||
|
|
||||||
|
Because we chose "full faithful," the first deliverable is a **documented map** of the
|
||||||
|
complete format, not code. Sources, cross-checked against each other:
|
||||||
|
|
||||||
|
- **DatReaderWriter types** — `ElementDesc`, `StateDesc`, `MediaDesc*` and their enums
|
||||||
|
(`Type`, `DrawMode`, media kinds, state keys). Reflect/inspect as `dump-vitals-layout`
|
||||||
|
already does (props **and** fields).
|
||||||
|
- **Retail decomp** — the `UIElement_*` class hierarchy + each type's render method; the
|
||||||
|
property-key meanings; the **KSML keyword registrations** (the parser registers every
|
||||||
|
property name — the canonical vocabulary, e.g. `KW_DRAWMODE`, `KW_DURATION`, …).
|
||||||
|
- **Real layouts** — scan a sample of `LayoutDesc`s to confirm which Types/properties
|
||||||
|
actually occur and catch anything the above missed.
|
||||||
|
|
||||||
|
Output: a reference doc mapping each `Type` → meaning + render method, each property key →
|
||||||
|
meaning, each `DrawMode` → behavior, and the inheritance rules. This doc drives every other
|
||||||
|
component and is committed alongside the importer.
|
||||||
|
|
||||||
|
### 2. `LayoutImporter`
|
||||||
|
|
||||||
|
Reads a `LayoutDesc` by id and returns a `UiElement` subtree:
|
||||||
|
- Walk the `ElementDesc` tree.
|
||||||
|
- For each element: resolve inheritance (§3), pick a widget via the factory (§4), set its
|
||||||
|
rect (`X/Y/W/H`), anchors (edge flags → `AnchorEdges`), z-order, states, media, and
|
||||||
|
properties from the (resolved) element.
|
||||||
|
- Recurse into children.
|
||||||
|
- Expose `FindElement(uint id)` on the result so controllers wire by id.
|
||||||
|
|
||||||
|
Depends on: `DatCollection` (read layouts), the factory, the inheritance resolver,
|
||||||
|
`TextureCache.GetOrUploadRenderSurface` (sprites), `UiDatFont` (text). No GL itself — it
|
||||||
|
builds `UiElement`s; rendering stays in the toolkit.
|
||||||
|
|
||||||
|
### 3. Inheritance resolution
|
||||||
|
|
||||||
|
An element with `BaseElement`/`BaseLayoutId` inherits the base element's properties / states
|
||||||
|
/ media; the derived element overrides. Resolve by loading the base layout, finding the base
|
||||||
|
element, and merging (base first, then derived overrides) **before** instantiating.
|
||||||
|
Required even for vitals: the number-text element inherits its font/style from base layout
|
||||||
|
`0x2100003F`. Cycle-guard the resolution.
|
||||||
|
|
||||||
|
### 4. Hybrid widget factory (`Type` → renderer)
|
||||||
|
|
||||||
|
- **Behavioral** types → dedicated widgets (verbatim-algorithm ports): meter → `UiMeter`,
|
||||||
|
text → dat-font label, scrollable/list region → `UiChatView`/list widget, button →
|
||||||
|
`UiButton`, resizable window root → `UiNineSlicePanel`.
|
||||||
|
- **Trivial** types (image, container, border piece, grip) → `UiDatElement` (generic).
|
||||||
|
- **Unknown** type → `UiDatElement` (faithful fallback — still draws its media).
|
||||||
|
|
||||||
|
The Step-0 enumeration assigns each `Type` to a bucket by reading its retail render method
|
||||||
|
(trivial blit → generic; real algorithm → widget).
|
||||||
|
|
||||||
|
### 5. `UiDatElement` (generic renderer)
|
||||||
|
|
||||||
|
A `UiElement` holding the resolved element's active-state media + draw-mode + rect. Its
|
||||||
|
`OnDraw` ports retail's base blit branch:
|
||||||
|
- `Normal` → **tile** at native size (UV-repeat; the UI texture is `GL_REPEAT`-wrapped) —
|
||||||
|
the mechanism already proven for the bars + chrome.
|
||||||
|
- `Alphablend` → blended overlay.
|
||||||
|
- `Stretch` (if present) → scale.
|
||||||
|
- image → sprite; cursor → hover cursor.
|
||||||
|
Reuses the tiling, dat-font, nine-slice draw primitives.
|
||||||
|
|
||||||
|
### 6. Per-window controllers (live-data binding)
|
||||||
|
|
||||||
|
Mirror retail's `gm*UI` classes. A small controller per window grabs elements by id from the
|
||||||
|
imported tree and pushes live data in: `VitalsController` binds `HealthPercent` → meter fill,
|
||||||
|
`cur/max` → number text; `ChatController` binds the chat tail → the chat region. **This is
|
||||||
|
the only per-window code, and it is data wiring, not graphics.** Retail-faithful: e.g.
|
||||||
|
`gmVitalsUI::PostInit` grabs child meter elements by id and sets attribute `0x69` (fill).
|
||||||
|
|
||||||
|
### 7. `WindowManager`
|
||||||
|
|
||||||
|
`OpenWindow(layoutId, controller)` → import + attach to `UiRoot`, place at the dat's default
|
||||||
|
position (then persist user move/resize), manage z-order / focus / close. Orchestrates the
|
||||||
|
focus/drag/resize mechanics `UiRoot` already provides.
|
||||||
|
|
||||||
|
### 8. States / expand / hover
|
||||||
|
|
||||||
|
Each element carries its named states (`HideDetail`/`ShowDetail`, normal/hover/pressed) from
|
||||||
|
the dat; the active state selects which media draws. A click or hover flips the active state.
|
||||||
|
Click-to-expand and hover highlight fall out generically — no per-window code.
|
||||||
|
|
||||||
|
## Rollout order (milestones)
|
||||||
|
|
||||||
|
1. **Enumerate the format** (§1) → reference doc.
|
||||||
|
2. **`LayoutImporter`** + **inheritance resolution** (read + resolve + walk).
|
||||||
|
3. **`UiDatElement`** generic renderer (port the draw-mode blit branch).
|
||||||
|
4. **Hybrid factory** (Type → widget/generic).
|
||||||
|
5. **`VitalsController`** (bind by id).
|
||||||
|
6. **Re-drive vitals → diff against the current window.** ✅ conformance gate.
|
||||||
|
7. **`WindowManager`** (open/close/persist).
|
||||||
|
8. **Extend** to chat (`ChatController`), then new windows for free.
|
||||||
|
|
||||||
|
## Testing / conformance
|
||||||
|
|
||||||
|
- **Golden tree checks** — the importer-built vitals tree has the expected element rects,
|
||||||
|
resolved sprites, and active states (assert against the known `0x2100006C` values).
|
||||||
|
- **Inheritance unit tests** — base+override merge, cycle-guard.
|
||||||
|
- **Draw-mode unit tests** — the UV math for tile vs stretch vs the partial last tile.
|
||||||
|
- **Bind-by-id unit tests** — controller wires the right element.
|
||||||
|
- **Headless visual diff** — `render-vitals-mockup` / a tree-render comparison vs the
|
||||||
|
hand-built reference (no live server needed).
|
||||||
|
- **Final** — in-client visual verification (the user) once the gate passes.
|
||||||
|
|
||||||
|
## Coexistence / don't-delete (restated)
|
||||||
|
|
||||||
|
- `MarkupDocument` + `*.xml` stay for plugin/custom panels.
|
||||||
|
- `UiMeter`, `UiNineSlicePanel`, `UiChatView`, `UiDatFont`, the tiling/scissor-fill/dat-font/
|
||||||
|
nine-slice primitives stay — reused as the importer's behavioral renderers.
|
||||||
|
- The hand-authored vitals path stays as the conformance reference until the importer
|
||||||
|
matches it; only then is vitals flipped to the importer.
|
||||||
|
|
||||||
|
## Risks & open questions
|
||||||
|
|
||||||
|
- **The format enumeration is the foundational unknown.** If a Type/property/draw-mode is
|
||||||
|
mis-mapped, faithfulness breaks. Mitigation: Step 0 cross-checks three sources + real
|
||||||
|
layouts; the vitals conformance gate catches regressions.
|
||||||
|
- **Some behavioral types may need new widgets** (list, scrollbar, edit box). These are
|
||||||
|
generic, written once — not per-window. The generic fallback means an un-widgeted type
|
||||||
|
still renders its sprites in the meantime.
|
||||||
|
- **Position persistence** scope (per-window saved rects) — minimal at first (dat default +
|
||||||
|
in-session move/resize); durable persistence can follow.
|
||||||
|
- **Phase id** — register this as the next D.2b sub-phase in the roadmap before implementing.
|
||||||
|
|
||||||
|
## Reference anchors
|
||||||
|
|
||||||
|
- **Dat layouts:** vitals (stacked) `0x2100006C`; floaty row `0x21000014`; horizontal row
|
||||||
|
`0x21000075`; vitals number-text base layout `0x2100003F`.
|
||||||
|
- **Decomp:** `gmVitalsUI::PostInit` @`0x4bfce0` (bind by id), `UIElement_Meter::DrawChildren`
|
||||||
|
@`0x46fbd0` (scissor-fill), `SurfaceWindow::DrawCharacter` @`0x442bd0` (dat-font),
|
||||||
|
`ImgTex::TileCSI` @`0x53e740` (tiling), `UIRegion::DrawHere` @`0x69fa30` (element draw order),
|
||||||
|
the KSML keyword registrations (~`0x71b540`+).
|
||||||
|
- **Tools:** `AcDream.Cli dump-vitals-layout` (reflective full tree dump),
|
||||||
|
`dump-sprite-sheet` (composite sprite ids), `render-vitals-mockup` (headless window render).
|
||||||
|
- **Memory:** `project_d2b_retail_ui.md` (the two-layout lesson, sprite ids, render model,
|
||||||
|
dat-font, tools).
|
||||||
Loading…
Add table
Add a link
Reference in a new issue