# 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).