acdream/docs/superpowers/specs/2026-06-14-d2b-retail-panel-frame-design.md
Erik b18403da02 feat(D.2b): wire UiHost + live retail Vitals panel (render-only); retire TS-30
Wires the dormant AcDream.App/UI retained-mode tree into GameWindow under
ACDREAM_RETAIL_UI=1: an 8-piece dat-sprite UiNineSlicePanel framing three
UiMeter vital bars bound to the existing VitalsVM. Render-only (UiHost input not
yet bridged to the InputDispatcher — next sub-phase). Coexists with the ImGui
devtools path; no regression there.

Visually verified against a live retail client: the bars match retail's vitals
structure (three stacked horizontal bars, current/max numbers centered) — so the
earlier "orbs" assumption was wrong (retail vitals ARE bars), and stamina is GOLD
not cyan (the #10F0F0 research note was wrong). UiMeter gains a centered numeric
Label (stub debug font for now). Spec §8 + the markup example corrected to match.

Bookkeeping: retired divergence row TS-30 (flat-rect panels -> real dat chrome)
and added IA-15 (our UiHost/markup engine vs keystone.dll's LayoutDesc tree).

Remaining polish (filed, §15): glassy gradient bar fill sprite + the retail dat
font for the numbers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 16:56:57 +02:00

392 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# D.2b — Retail panel frame + live Vitals (Approach C: KSML-style engine) — Design
**Date:** 2026-06-14
**Status:** Design approved (brainstorm) + **re-grounded 2026-06-14** onto the existing `AcDream.App/UI/` retained-mode scaffold (see §0). Pending spec re-review → implementation plan.
**Phase:** D.2b — Custom retail-look UI backend ([roadmap:427](../../../docs/plans/2026-04-11-roadmap.md))
**Milestone:** M5 "Looks like retail" — **explicitly PARALLELIZABLE with M3/M4** ([milestones:378](../../../docs/plans/2026-05-12-milestones.md)). Opened as a parallel track while M1.5 is the active critical-path milestone; the M5 parallelizable flag is the milestone-discipline carve-out.
**Grounding:** read-only research workflow `wf_39a90d37-e5a` (7 readers + gap-critic) + a direct read of `src/AcDream.App/UI/`. Every binding fact cites `file:line` in `src/` or a named-retail symbol.
---
## 0. Re-grounding correction (read this first)
The first draft of this spec proposed building a `RetailPanelHost : IPanelHost` +
`RetailPanelRenderer : IPanelRenderer` and a retained-mode toolkit *from scratch*.
**That was wrong.** A direct read of `src/AcDream.App/UI/` found a **complete,
dormant retained-mode toolkit** — the 2026-04-17 scaffold the roadmap names as
"the implementation foundation here" ([roadmap:427](../../../docs/plans/2026-04-11-roadmap.md)):
- **`UiRoot`** ([UiRoot.cs](../../../src/AcDream.App/UI/UiRoot.cs)) — the hard
part is already built: mouse routing, keyboard focus, mouse capture, a full
drag-drop state machine, tooltip timer, modal handling, click/right-click
detection, world fall-through. Retail-faithful event codes in
[UiEvent.cs](../../../src/AcDream.App/UI/UiEvent.cs).
- **`UiElement`** (geometry/tree/hit-test), **`UiPanel`/`UiLabel`/`UiButton`**
([UiPanel.cs](../../../src/AcDream.App/UI/UiPanel.cs)), **`UiHost`**
([UiHost.cs](../../../src/AcDream.App/UI/UiHost.cs) — packages `UiRoot` +
`TextRenderer` + font, with `Tick`/`Draw`/`WireMouse`/`WireKeyboard`),
**`UiRenderContext`** ([UiRenderContext.cs](../../../src/AcDream.App/UI/UiRenderContext.cs)
— transform stack + `DrawRect`/`DrawString`).
`UiHost` is **dormant** — never instantiated in `GameWindow` (verified: `new
UiHost(` appears only in a doc-comment). And `UiPanel.cs` is the *exact file*
divergence row TS-30 points at: it draws a flat translucent rect *"until our
AcFont/UiSpriteBatch consumes [9-slice dat sprites] directly."*
**Consequence:** the retail UI is this existing `UiRoot` tree — a separate system
from the ImGui `IPanelRenderer` path, **not** an `IPanelRenderer` implementation.
Spec 1 *wires the dormant `UiHost`* and *adds the few missing pieces*, rather than
building a backend. This is strictly less code and more faithful. §4/§5/§8/§9/§10
below are written against the scaffold.
*(Process note: the grounding workflow's "UI" readers keyed on the ImGui/Abstractions
framing in their prompts and never globbed `src/AcDream.App/UI/`. Lesson: a
subsystem-discovery pass must glob by directory, not only by the framing the
parent already has in mind.)*
## 1. Context & goal
acdream needs a retail-faithful game UI. The shipped path (D.2a) is an ImGui
overlay gated on `ACDREAM_DEVTOOLS=1` — a debugger aesthetic, intentionally
temporary. D.2b stands up the *retail-look* UI (the dormant `UiHost` tree) that
draws retail's actual dat assets, while the ImGui devtools path stays untouched.
**The user's framing (2026-06-14):** AC's UI engine is Keystone — and Keystone
was *already* markup + stylesheet (KSML, an HTML-clone XML defined by `ksml.xsd`,
+ `controls.ini`, a CSS-like INI stylesheet). So "make it look + behave like
retail, but author it in a CSS/HTML-style way" re-expresses AC's own design in
its modern equivalent.
**Approach decision (Approach C).** Three integration families were weighed:
(A) embed a real web engine (Ultralight/CEF), (B) a native HTML/CSS-subset lib
(RmlUi), (C) our own KSML-style markup + stylesheet over a retained-mode toolkit
on Silk.NET. **C chosen** for: zero external deps (keeps the native-AOT goal
intact), lowest memory (~310 MB vs CEF's 150300 MB), full control, and maximal
faithfulness — it mirrors Keystone directly, and the retained-mode toolkit C
needs *already exists* (§0).
This spec covers **Spec 1**: wire the scaffold + add the markup/stylesheet/sprite
gaps, proven end-to-end on **one** panel — the universal window frame wrapping
the live Vitals bars.
## 2. Scope
**In Spec 1:**
- Wire the dormant **`UiHost`** into `GameWindow`, gated by a new
`RuntimeOptions.RetailUi` toggle (`ACDREAM_RETAIL_UI=1`). The ImGui devtools
path is untouched and may run simultaneously.
- Add dat-sprite drawing: `UiRenderContext.DrawSprite` (UV-rect) + a
`TextRenderer` textured-sprite path + a `ui_text.frag` `uUseTexture=2` branch.
- A **`UiNineSlicePanel : UiPanel`** that draws the 8-piece dat-sprite window
frame + center fill (upgrading the exact code TS-30 cites) — title bar
(`UiLabel`) + a close button (`UiButton`, which already exists).
- A **`UiMeter : UiElement`** vital bar bound to a `Func<float>` reading
`VitalsVM`.
- The XML markup format (mirrors `ElementDesc`) + a `MarkupDocument` parser that
**instantiates a `UiElement` subtree** + a minimal `controls.ini` stylesheet
loader.
- The plugin-facing contract: plugins contribute a `UiElement`/markup subtree
added to `UiRoot` (§9) — designed now, first consumer first-party.
**Deferred to later sub-phases (explicitly OUT):**
- **Wiring `UiHost`'s input** (`WireMouse`/`WireKeyboard`) into the existing
Phase-K `InputDispatcher`. The `UiRoot` input *machinery* exists; *integrating*
two input consumers (route unconsumed `WorldMouseFallThrough` back to the game)
is its own concern. Spec 1 is **render-only** (`Tick` + `Draw`), so the frame +
live bars show but the close button isn't clicked and the window isn't dragged.
- The dat A8 glyph font loader (`AcFont`) → numeric overlays.
- The full anchor solver (`StateDesc::UpdateSizeAndPosition` port).
- The `LayoutDesc` binary importer (sub-project 3).
- Reskinning Chat / Debug / Settings.
- Login / char-select / chargen screens (raw-JPEG backgrounds, sub-project 4).
## 3. Source-verified facts (do-not-trust list)
The grounding caught several load-bearing "facts" that were wrong/unverified.
These are binding:
| Claimed (memory / first draft) | Reality (source-verified) |
|---|---|
| Build a retained-mode toolkit + `RetailPanelHost`/`RetailPanelRenderer` | The toolkit **exists** in `src/AcDream.App/UI/` (§0); the retail UI is the `UiRoot` tree, not an `IPanelRenderer` backend |
| `VitalsVM` is `record VitalsVM(int HpCurrent, …)` | Sealed class: `HealthPercent` (float), `StaminaPercent`/`ManaPercent` (float?), `*Current`/`*Max` (uint?), ctor `(CombatState, LocalPlayerState?)`, `SetLocalPlayerGuid(uint)` — [VitalsVM.cs:35](../../../src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs) |
| Chrome sprite IDs `0x06004CC2`/`0x21000040`/`0x060074BF..C6` are known | **Unverified + contradictory.** `0x06001125` (cited elsewhere) is the char-select highlight. **No chrome ID is trusted — Step 0 dat prove-out resolves them empirically.** |
| `#FFDBD6A8` "parchment cream" is the panel background | It is `[editbox]`/`[treeview]` **text** color. Real frame tokens: `[title]` bg `#FFFFFFFF`, font `Verdana-10-bold`, height 19; `[body]` bg `#00000000` (transparent), `color_border=#FF4F657D` |
| `DatCollection` is NOT thread-safe | Concurrent **reads are safe** since v2.1.7 ([2026-06-09 investigation](../../../docs/research/2026-06-09-dat-reader-thread-safety-investigation.md)). No UI-specific lock. |
| KSML is the panel-layout language | KSML is **rich-text-in-text-regions**; the panel layout format is the binary `LayoutDesc`/`ElementDesc` tree ([acclient.h:33693](../../../docs/research/named-retail/acclient.h)). Our markup mirrors `ElementDesc`. |
## 4. Architecture & placement
The retail UI lives in **`src/AcDream.App/UI/`** (where the scaffold already is).
New widgets/parsers join it as dedicated files. Code-Structure Rule 1 is honored
(nothing substantial added to `GameWindow.cs` — only a few wiring lines); Rule 2
(Core stays GL-free) and Rule 3 (panels target `AcDream.UI.Abstractions`) are
unaffected because the retail UI is a *separate* tree, not an `IPanelRenderer`
panel.
```
┌──────────────────────────────────────────────────────────┐
│ retail dat (read-only fidelity source) │
│ controls.ini → style tokens · RenderSurface 0x06xxxxxx │
│ → sprites · Font 0x40xxxxxx → glyphs (deferred) │
└───────────────┬──────────────────────────────────────────┘
│ TextureCache.GetOrUpload(id) → Texture2D
┌───────────────▼──────────────────────────────────────────┐
│ src/AcDream.App/UI/ (scaffold EXISTS; + = new in Spec 1) │
│ UiHost (exists, dormant) ─ wire into GameWindow │
│ UiRoot/UiElement (exist) ─ input + tree + hit-test │
│ UiRenderContext (exists) + DrawSprite(UV-rect) │
│ UiPanel/UiLabel/UiButton (exist) │
│ + UiNineSlicePanel : UiPanel (8-piece dat chrome) │
│ + UiMeter : UiElement (vital bar) │
│ + MarkupDocument (XML → UiElement subtree) │
│ + ControlsIni (stylesheet loader) │
│ uses Rendering/TextRenderer (+ sprite path, + DepthMask) │
└───────────────┬──────────────────────────────────────────┘
│ UiMeter.Fill = () => vm.HealthPercent
┌───────────────▼──────────────────────────────────────────┐
│ AcDream.UI.Abstractions (exists) — VitalsVM (unchanged) │
│ ↑ ImGui IPanelHost/IPanelRenderer path stays for │
│ ACDREAM_DEVTOOLS, fully independent of the above │
└──────────────────────────────────────────────────────────┘
```
**Coexistence.** Two UI systems run side by side, independently:
`ACDREAM_DEVTOOLS=1` → the ImGui overlay (unchanged); `ACDREAM_RETAIL_UI=1`
the `UiHost` tree. The retail pass renders in the post-3D slot
([GameWindow.cs:8232 region](../../../src/AcDream.App/Rendering/GameWindow.cs))
with deterministic ordering relative to ImGui. `UiHost.Draw` already does
`TextRenderer.Begin → Root.Draw(ctx) → TextRenderer.Flush`
([UiHost.cs:58](../../../src/AcDream.App/UI/UiHost.cs)).
## 5. Render foundation — extend the existing 2D path
`UiHost` draws the `UiRoot` tree through a `UiRenderContext` backed by the shared
`TextRenderer` ([UiHost.cs:58-67](../../../src/AcDream.App/UI/UiHost.cs)). That
`TextRenderer` does solid rects + R8 text today but **not** textured RGBA sprites
([ui_text.frag:9](../../../src/AcDream.App/Rendering/Shaders/ui_text.frag),
[TextRenderer.cs](../../../src/AcDream.App/Rendering/TextRenderer.cs)). Spec 1
adds the sprite path:
- **`ui_text.frag`** += a `uUseTexture==2` branch: `FragColor = texture(uTex,
vUv) * vColor;` (the existing `0`=solid and `1`=R8-coverage branches are
untouched).
- **`TextRenderer`** += `DrawSprite(uint texture, float x,y,w,h, float
u0,v0,u1,v1, Vector4 tint)` accumulating into **per-texture** sprite buffers
(`Dictionary<uint, List<float>>`), and a `Flush` pass that, after rects+text,
draws each texture's batch with `uUseTexture=2`. Reuses the existing
`AppendQuad` (which already takes `u0,v0,u1,v1`) + `UploadBuffer` machinery.
- **`TextRenderer.Flush`** += explicit **`DepthMask(false)`** (queried + restored)
— it disables `DepthTest` today but never sets `DepthMask`
([TextRenderer.cs:171](../../../src/AcDream.App/Rendering/TextRenderer.cs)).
Per the project's "render self-contained GL state" rule.
- **`UiRenderContext`** += `DrawSprite(uint texture, float x,y,w,h, float
u0,v0,u1,v1, Vector4 tint)` that adds the current transform and forwards to
`TextRenderer.DrawSprite` (mirrors the existing `DrawRect` forwarder at
[UiRenderContext.cs:50](../../../src/AcDream.App/UI/UiRenderContext.cs)).
No new shader class, VAO, or batcher — we extend the proven path the scaffold
already uses. (`Shader` is the simple file-based class
[Shader.cs](../../../src/AcDream.App/Rendering/Shader.cs); `GLSLShader`'s bindless
machinery is not needed.)
## 6. Dat assets & the Step-0 prove-out gate
`TextureCache.GetOrUpload(uint surfaceId)` returns a conventional `Texture2D`
GL handle (1×1 magenta on failure) — exactly right for the UI batch
([TextureCache.cs:70](../../../src/AcDream.App/Rendering/TextureCache.cs)). The
decode chain + `PFID_*` formats already work
([SurfaceDecoder.cs:39](../../../src/AcDream.Core/Textures/SurfaceDecoder.cs)).
`GameWindow` already holds a `TextureCache`; `UiHost`/`UiNineSlicePanel` receive
it (or a `Func<uint,uint>` sprite-resolver) by injection.
**Step 0 is empirical and comes first.** Because no chrome sprite ID is verified,
the first implementation task draws each candidate ID
(`0x06004CC2`, `0x060074BF..C6`, `0x0600129C`, …) as a raw quad and visually
confirms which decode to frame-shaped art vs magenta vs the wrong sprite. The
confirmed IDs are recorded in code comments before any chrome layout is written.
**No ID is hardcoded on faith.**
The frame is **8 quads + a center fill** (4 corner + 4 edge sprites + center),
not one stretched 9-slice texture. Slice/edge metrics are a **documented stopgap
constant** (with a divergence row) until the `LayoutDesc` tree is parsed
(sub-project 3).
## 7. Markup + stylesheet model
**Markup** mirrors `ElementDesc` 1:1 ([acclient.h:33693](../../../docs/research/named-retail/acclient.h));
`MarkupDocument` parses it and **instantiates a `UiElement` subtree** (a
`UiNineSlicePanel` with child `UiLabel`/`UiMeter`/`UiButton`). Authoring shape:
```xml
<panel id="acdream.vitals" x="10" y="30" w="220" h="96" title="Vitals">
<meter id="health" x="8" y="24" w="200" h="13" fill="{HealthPercent}" color="#FF0000"/>
<meter id="stamina" x="8" y="44" w="200" h="13" fill="{StaminaPercent}" color="#D9A626"/>
<meter id="mana" x="8" y="64" w="200" h="13" fill="{ManaPercent}" color="#0000FF"/>
</panel>
```
This is the shape the future `LayoutDesc` importer will *emit*, so authoring and
imported formats converge. It is **not** KSML (rich-text, deferred). `{Binding}`
expressions resolve against a supplied binding object (the `VitalsVM`) via
reflection on the property name.
**Anchor codes** are *defined* now (0=fixed, 1=stretch, 2=proportional-translate,
3=center, 4=proportional-scale — from `StateDesc::UpdateSizeAndPosition`
@`0x0069BF20`) but the **solver is deferred**: the Vitals window is fixed-size
(placed via the existing `UiElement.Left/Top`), so Spec 1 needs no solver.
**Stylesheet.** A small INI loader parses `controls.ini` keyed by element-type
section, honoring `#AARRGGBB` (alpha-first) and `font://Face-Pt[-style]`. Cascade:
element-type defaults → per-element `class=` → inline attributes. **Optional**
(§10): absent AC install → source-verified `[title]`/`[body]` fallback tokens.
## 8. VM binding (the Vitals slice)
The vitals panel is a `UiNineSlicePanel` (chrome) containing a `UiLabel` (title)
and three `UiMeter`s. Each `UiMeter` holds a `Func<float?> Fill` bound to the
real `VitalsVM` ([VitalsVM.cs:67](../../../src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs)):
`() => vm.HealthPercent`, `() => vm.StaminaPercent`, `() => vm.ManaPercent`. The
VM already does all server plumbing, so we do **not** re-derive vitals from the
retail `gmVitalsUI`/`CACQualities` decomp.
`UiMeter.OnDraw` draws the empty bar (`ctx.DrawRect`), the filled portion as a
**partial-size rect** (`width = pct * Width`), and a centered `current/max` numeric
overlay (`Func<string?> Label`). **Retail's vitals ARE exactly this — three stacked
horizontal bars (confirmed against a live retail client 2026-06-14), NOT orbs.**
Colors: Health red, **Stamina gold** (the earlier `#10F0F0` cyan research note was
wrong), Mana blue. A `null` fill/label (pre-`PlayerDescription`) renders gracefully.
The remaining gap to pixel-retail is the **glassy gradient bar fill sprite** + the
**retail dat font** for the numbers (today the stub `BitmapFont` draws them) — both
polish, deferred to §15.
The `VitalsVM` is constructed and given the player GUID the same way as today
([GameWindow.cs:1330](../../../src/AcDream.App/Rendering/GameWindow.cs) ctor,
:1984 `SetLocalPlayerGuid`); the retail-UI build path reuses that same VM
instance.
## 9. Plugin contract (designed now, first consumer first-party)
The plugin API is a day-1 constraint; plugin authors must be able to add retail
UI. The natural unit is a **`UiElement`/markup subtree added to `UiRoot`** (not
`IPanel`/`IPanelRenderer`, which is the ImGui devtools path). Spec 1 adds:
- A small `IUiRegistry` (in `AcDream.Plugin.Abstractions`) — `void
AddMarkupPanel(string markupPath, object binding)` (and/or `void
AddElement(UiElement)` once a plugin-safe element surface is decided). For
Spec 1, `AddMarkupPanel` is enough.
- `IPluginHost` gains `IUiRegistry Ui { get; }`
([IPluginHost.cs:8](../../../src/AcDream.Plugin.Abstractions/IPluginHost.cs)
has none today); `AppPluginHost` implements it
([AppPluginHost.cs:5](../../../src/AcDream.App/Plugins/AppPluginHost.cs)).
- Because plugin `Enable()` runs in `Program.cs` **before** the GL window opens
([Program.cs:55-60](../../../src/AcDream.App/Program.cs)), `AddMarkupPanel`
**buffers** registrations into a list that `GameWindow` drains into `UiRoot`
after `UiHost` is constructed. The threading/timing concern lives in the host;
the plugin call is unconditional.
- The first consumer is the first-party vitals panel (built directly in
`GameWindow`, not through the registry). Wiring an actual plugin-supplied markup
panel end-to-end is exercised by a smoke test but is otherwise the thin
follow-up. This task group is the **last** in the plan so the visible vitals
slice can land first if it slips.
## 10. Confirmed decisions (approved 2026-06-14)
1. **Render-only first slice.** `Tick` + `Draw` only; the `UiHost` input wiring
(`WireMouse`/`WireKeyboard`) is **not** connected to the existing Phase-K
`InputDispatcher` yet, so the close button isn't clickable and the window
isn't draggable. Rationale (corrected): the `UiRoot` input *machinery* already
exists — what's deferred is *integrating two input consumers* (routing
unconsumed `WorldMouseFallThrough` back to the game's dispatcher), which is its
own sub-phase.
2. **`controls.ini` optional.** Assume `C:\Turbine\Asheron's Call\` may or may not
exist. Add an `ACDREAM_AC_DIR` `RuntimeOptions` field; when absent, fall back
to the source-verified `[title]`/`[body]` token values. The build never fails
on a missing AC install.
## 11. Build sequence
| Step | Deliverable | Proves |
|---|---|---|
| 0 | Dat prove-out harness: draw candidate chrome IDs, confirm the real ones | Resolves the chrome-ID contradiction empirically |
| 1 | `ui_text.frag` `uUseTexture=2` + `TextRenderer.DrawSprite` + `DepthMask` + `UiRenderContext.DrawSprite` | A dat sprite composites over the 3D scene |
| 2 | `UiNineSlicePanel` draws an empty titled frame from confirmed dat sprites (stopgap insets) | Retail-shaped chrome renders |
| 3 | `UiMeter` + a hand-built vitals `UiNineSlicePanel` subtree bound to `VitalsVM`, wired via `UiHost` under `ACDREAM_RETAIL_UI` (render-only) | End-to-end live data + the scaffold lights up |
| 4 | `ControlsIni` parser (TDD) feeding the panel's title/colors | Stylesheet cascade |
| 5 | `MarkupDocument` parser (TDD) → builds the same vitals subtree from `vitals.xml` | The Approach-C markup engine |
| 6 *(last)* | `IUiRegistry` on `IPluginHost` + buffered drain into `UiRoot` + smoke test | Plugin-ready |
## 12. Error handling & edge cases
- **Missing/undecodable sprite** → `GetOrUpload` magenta fallback is visible;
Step 0 catches it. A null/zero DataID in markup logs a warning, draws nothing.
- **AC install absent** → `controls.ini` load skipped, baked fallback tokens used.
- **Vitals null percents** → empty bar (`UiMeter.Fill` returns null).
- **Window resize** → `UiHost.Draw` already sets `Root.Width/Height` to the
current screen size each frame ([UiHost.cs:61](../../../src/AcDream.App/UI/UiHost.cs));
fixed-coord panels stay put. No DPI scaling (known out-of-scope gap).
- **Both toggles on** → ImGui Vitals and retail Vitals may both show (fine in dev).
## 13. Testing
- **`ControlsIni` parser** (pure, no GL) — unit tests for `#AARRGGBB`, `font://`,
cascade order. Lives in `src/AcDream.App/UI/`, tested in `tests/AcDream.App.Tests/`
(App-layer, Rule 6).
- **`MarkupDocument` parser** — unit tests for XML → `UiElement` tree shape
(types, geometry) and `{Binding}` resolution against a fake binding object.
- **`UiMeter` fill geometry** — unit test that fill fraction → partial rect width
(pure math; `UiMeter.ComputeFillRect(pct, w, h)` as a static helper so it's
testable without GL).
- **`UiNineSlice` geometry** — unit test that a frame size + insets → the 9 dst
rects (`UiNineSlicePanel.ComputeSliceRects` static helper).
- **Plugin smoke** — a test plugin calls `host.Ui.AddMarkupPanel` and the buffered
registration is drained (assert the panel is added to `UiRoot`).
- **Visual acceptance** (user) — retail-shaped Vitals frame with live bars under
`ACDREAM_RETAIL_UI=1`; ImGui path unaffected under `ACDREAM_DEVTOOLS=1`.
- `dotnet build` + `dotnet test` green.
## 14. Bookkeeping
- **Phase D.2b**, Milestone **M5** (parallelizable; NOT on the M1.5 critical
path). The CLAUDE.md "Current state" line stays on M1.5.
- **Divergence register:** in the commit that ships `UiNineSlicePanel` rendering a
real dat sprite, **delete row TS-30** ([retail-divergence-register.md:166](../../../docs/architecture/retail-divergence-register.md))
— its cited file (`UiPanel.cs`) is upgraded by the subclass — and **add one**
new IA-row (Intentional Architecture; keystone.dll has no PDB/decomp) for the
markup/serialization layer. Assign the next sequential IA number at commit time.
Retail oracle: "LayoutDesc 0x21xxxxxx; controls.ini panel-property vocabulary;
keystone.dll layout evaluation (no PDB)". Do **not** duplicate IA-12 (UI
toolkit *behavioral* approximation). A second row for the stopgap slice insets
is added when they ship.
- **Spec file:** this document.
## 15. Open gaps & deferred sub-projects
- **Input integration** — connect `UiHost.WireMouse`/`WireKeyboard` to the Phase-K
`InputDispatcher`, routing unconsumed `WorldMouseFallThrough`/`WorldKeyFallThrough`
back to the game. Next sub-phase (lights up the close button + window drag that
`UiRoot` already supports).
- **`AcFont`** — dat A8 glyph loader (Font `0x40000xxx``ForegroundSurfaceDataId`
→ RenderSurface, upload as **R8** so `ui_text.frag`'s `.r`-coverage branch works
unchanged) → numeric overlays + retail fonts. (Today `UiLabel` uses the
stb_truetype `BitmapFont`.)
- **Anchor solver** — port `StateDesc::UpdateSizeAndPosition`; with the importer.
- **`LayoutDesc` binary importer** (sub-project 3) — bulk-transpile retail layouts
→ our markup, supplying real insets + coords. Symbols: `LayoutDesc::InqFullDesc`
@`0x0069A520`, `ElementDesc::Incorporate` @`0x0069B5A0`
([2026-05-08 pseudocode](../../../docs/research/2026-05-08-retail-ui-layout-resolution-pseudocode.md)).
- **`PFID_CUSTOM_RAW_JPEG`** decode + login/char-select/chargen (sub-project 4).
## 16. Acceptance criteria
- [ ] Step 0 prove-out done; real chrome sprite IDs confirmed + recorded in code.
- [ ] In `ACDREAM_RETAIL_UI=1`: a retail-shaped Vitals window renders via the wired
`UiHost``UiNineSlicePanel` 8-piece dat-sprite border + title + drawn close
button — with three `UiMeter` bars tracking HP/Stam/Mana live as the
character takes damage / regens.
- [ ] In `ACDREAM_DEVTOOLS=1`: ImGui Vitals/Chat/Debug/Settings unchanged.
- [ ] `controls.ini` loads when present, falls back cleanly when absent.
- [ ] `MarkupDocument` builds the vitals subtree from `vitals.xml`; pure parsers
unit-tested; plugin smoke test drains a buffered `AddMarkupPanel`.
- [ ] TS-30 deleted + one new IA-row added, same commit as the chrome.
- [ ] `dotnet build` green, `dotnet test` green.
- [ ] Visual verification by the user.