From 64146bfc2aa8bf451b8a9cdbbd36d7bfa0b77251 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 15 Jun 2026 12:38:34 +0200 Subject: [PATCH] docs(D.2b): LayoutDesc importer design spec (data-driven retail windows) --- .../2026-06-15-layoutdesc-importer-design.md | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-15-layoutdesc-importer-design.md diff --git a/docs/superpowers/specs/2026-06-15-layoutdesc-importer-design.md b/docs/superpowers/specs/2026-06-15-layoutdesc-importer-design.md new file mode 100644 index 00000000..1fb36f07 --- /dev/null +++ b/docs/superpowers/specs/2026-06-15-layoutdesc-importer-design.md @@ -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).