docs(D.2b): re-ground spec onto existing AcDream.App/UI scaffold
A direct read of src/AcDream.App/UI/ found a complete (dormant) retained-mode toolkit the grounding workflow missed: UiRoot (input routing, focus, capture, drag-drop, tooltip, click detection, world fall-through), UiElement, UiPanel/UiLabel/UiButton, UiHost (Tick/Draw + WireMouse/WireKeyboard), UiRenderContext, retail-faithful UiEvent codes. It's never wired into GameWindow, and UiPanel.cs is the exact file divergence row TS-30 cites. So the retail UI is this existing UiRoot tree — NOT an IPanelHost/IPanelRenderer backend. Rewrote the architecture sections: Spec 1 now WIRES the dormant UiHost and adds only the gaps (DrawSprite + frag uUseTexture=2, UiNineSlicePanel, UiMeter, MarkupDocument that builds a UiElement subtree, ControlsIni). Input machinery already exists in UiRoot; deferring it is now about integrating two input consumers, not a missing contract. Plugin contract becomes a UiElement/ markup subtree added to UiRoot (IUiRegistry on IPluginHost), not IPanel. Net: strictly less new code, more faithful, retires TS-30 by subclassing the file it cites. Added §0 documenting the correction + the process lesson (subsystem-discovery must glob by directory, not by the parent's framing). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
de9229eed5
commit
d50023f6d9
1 changed files with 269 additions and 229 deletions
|
|
@ -1,95 +1,127 @@
|
|||
# D.2b — Retail panel frame + live Vitals (Approach C: KSML-style engine) — Design
|
||||
|
||||
**Date:** 2026-06-14
|
||||
**Status:** Design approved (brainstorm), pending spec review → implementation plan
|
||||
**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, 2026-06-14). Every binding fact below cites `file:line` in `src/` or a named-retail symbol; nothing rests on a memory note alone.
|
||||
**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 replaces the *visual layer* with our own toolkit that draws
|
||||
retail's actual dat assets and matches retail's look, while the stable
|
||||
`AcDream.UI.Abstractions` contracts (ViewModels, Commands, `IPanel`) stay
|
||||
unchanged underneath.
|
||||
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" is not a foreign graft — it
|
||||
re-expresses AC's own design in its modern equivalent.
|
||||
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
|
||||
distribution goal intact), lowest memory (~3–10 MB vs CEF's 150–300 MB), full
|
||||
control, and maximal architectural faithfulness — it mirrors Keystone directly.
|
||||
The cost (most code to write) is acceptable because the engine is ours forever
|
||||
and the plugin API (a day-1 core constraint) gets a clean markup authoring
|
||||
surface.
|
||||
on Silk.NET. **C chosen** for: zero external deps (keeps the native-AOT goal
|
||||
intact), lowest memory (~3–10 MB vs CEF's 150–300 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**: the engine skeleton + the plugin-facing markup
|
||||
contract, proven end-to-end on **one** real panel — the universal window frame
|
||||
wrapping the live Vitals bars.
|
||||
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:**
|
||||
- A new retail-look backend in `src/AcDream.App/UI/Retail/` implementing
|
||||
`IPanelHost` + `IPanelRenderer` from `AcDream.UI.Abstractions`.
|
||||
- The 8-piece dat-sprite window frame (4 corners + 4 edges + center fill), a
|
||||
title bar, and a *drawn* close button.
|
||||
- Three live vital bars bound to the existing `VitalsVM`.
|
||||
- The XML markup format (mirrors `ElementDesc`) + a minimal `controls.ini`
|
||||
stylesheet loader.
|
||||
- The plugin-facing contract: `IPanelRegistry` on `IPluginHost` + a `MarkupPanel`
|
||||
shim, so the engine is plugin-ready by construction.
|
||||
- A new `RuntimeOptions.RetailUi` toggle (`ACDREAM_RETAIL_UI=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):**
|
||||
- Input / hit-testing (window drag, working close-click). Spec 1 is **render-only**.
|
||||
- The dat A8 glyph font loader (`AcFont`) → numeric overlays ("182/210").
|
||||
- **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 panels.
|
||||
- Reskinning Chat / Debug / Settings.
|
||||
- Login / char-select / chargen screens (raw-JPEG backgrounds, sub-project 4).
|
||||
- Extraction into a standalone `src/AcDream.UI.Retail/` project (see §4).
|
||||
|
||||
## 3. What the grounding corrected (do-not-trust list)
|
||||
## 3. Source-verified facts (do-not-trust list)
|
||||
|
||||
The research caught several load-bearing "facts" that were wrong or unverified.
|
||||
The grounding caught several load-bearing "facts" that were wrong/unverified.
|
||||
These are binding:
|
||||
|
||||
| Claimed (memory / plan doc) | Reality (source-verified) |
|
||||
| Claimed (memory / first draft) | Reality (source-verified) |
|
||||
|---|---|
|
||||
| `VitalsVM` is `record VitalsVM(int HpCurrent, …)` | It is a **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.** A second reader cited `0x06001125` etc. from a *non-existent* file; `0x06001125` is actually 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 the `[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 (1.1M-read hammer test, [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`, not KSML. |
|
||||
| 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 docs name a future `src/AcDream.UI.Retail/` project, but the three pieces we
|
||||
must reuse — `TextureCache`, `TextRenderer`, `Shader` — all live in
|
||||
**`AcDream.App`** ([TextureCache.cs:70](../../../src/AcDream.App/Rendering/TextureCache.cs),
|
||||
[TextRenderer.cs](../../../src/AcDream.App/Rendering/TextRenderer.cs),
|
||||
[Shader.cs](../../../src/AcDream.App/Rendering/Shader.cs)). A separate project
|
||||
cannot reach them without first extracting a shared rendering-primitives project
|
||||
(a large, unrelated refactor). Unlike `AcDream.UI.ImGui` (which needs only the
|
||||
ImGui packages), the retail backend needs dat sprites, which are App-resident.
|
||||
|
||||
**Decision:** Spec 1 builds the backend in **`src/AcDream.App/UI/Retail/`** as
|
||||
dedicated classes. This honors Code-Structure Rule 1 (nothing substantial added
|
||||
to `GameWindow.cs`'s body — only a few wiring lines), Rule 2 (Core stays
|
||||
GL-free), and Rule 3 (panels still target `AcDream.UI.Abstractions`; the backend
|
||||
*implements* the host/renderer contracts). The clean `AcDream.UI.Retail` project
|
||||
extraction is a follow-up, gated on a rendering-primitives home existing.
|
||||
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.
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
|
|
@ -97,82 +129,93 @@ extraction is a follow-up, gated on a rendering-primitives home existing.
|
|||
│ controls.ini → style tokens · RenderSurface 0x06xxxxxx │
|
||||
│ → sprites · Font 0x40xxxxxx → glyphs (deferred) │
|
||||
└───────────────┬──────────────────────────────────────────┘
|
||||
│ assets via TextureCache.GetOrUpload
|
||||
│ TextureCache.GetOrUpload(id) → Texture2D
|
||||
┌───────────────▼──────────────────────────────────────────┐
|
||||
│ NEW: src/AcDream.App/UI/Retail/ │
|
||||
│ RetailPanelHost : IPanelHost │
|
||||
│ RetailPanelRenderer : IPanelRenderer (+ chrome) │
|
||||
│ UiSpriteBatch (wraps TextRenderer + UV-rect quads) │
|
||||
│ NineSlice (8 pieces + center) · ControlsIni (parser) │
|
||||
│ MarkupDocument (XML → ElementDesc-shaped tree) │
|
||||
│ 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) │
|
||||
└───────────────┬──────────────────────────────────────────┘
|
||||
│ VMs out / Commands in (unchanged)
|
||||
│ UiMeter.Fill = () => vm.HealthPercent
|
||||
┌───────────────▼──────────────────────────────────────────┐
|
||||
│ AcDream.UI.Abstractions (exists) — IPanel/IPanelHost/ │
|
||||
│ IPanelRenderer/ICommandBus/PanelContext/VitalsVM │
|
||||
└───────────────┬──────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────▼──────────────────────────────────────────┐
|
||||
│ game state (unchanged) — CombatState, LocalPlayerState │
|
||||
│ AcDream.UI.Abstractions (exists) — VitalsVM (unchanged) │
|
||||
│ ↑ ImGui IPanelHost/IPanelRenderer path stays for │
|
||||
│ ACDREAM_DEVTOOLS, fully independent of the above │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Coexistence with ImGui.** The retail pass renders in the same post-3D slot as
|
||||
ImGui's `Render()` ([GameWindow.cs:8232](../../../src/AcDream.App/Rendering/GameWindow.cs)),
|
||||
with deterministic ordering. `ACDREAM_RETAIL_UI=1` activates the retail Vitals
|
||||
panel; `ACDREAM_DEVTOOLS=1` keeps the ImGui overlay (Chat/Debug/Settings) working
|
||||
with **no regression**. Both may be on at once during development.
|
||||
**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 — reuse, don't rebuild
|
||||
## 5. Render foundation — extend the existing 2D path
|
||||
|
||||
`IPanelRenderer` is a 34-method, ImGui-shaped immediate-mode API; `Begin(string
|
||||
title)` carries no position/size/sprite/style ([IPanelRenderer.cs:23](../../../src/AcDream.UI.Abstractions/IPanelRenderer.cs)).
|
||||
It is **structurally incompatible** with positioned, chrome-decorated retail
|
||||
windows, so the markup engine does **not** route chrome through it. Instead:
|
||||
`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:
|
||||
|
||||
- **`UiSpriteBatch` wraps `TextRenderer`** — which already provides pixel→NDC
|
||||
conversion ([ui_text.vert:12](../../../src/AcDream.App/Rendering/Shaders/ui_text.vert)),
|
||||
dynamic VBO growth, and a save/restore pattern. We add a **source-UV-rect
|
||||
parameter** to its quad path so one sprite can be sliced into 8 border pieces.
|
||||
- **Extend `ui_text.frag`** with `uUseTexture=2` (RGBA sampling) for dat sprites;
|
||||
it currently does only solid-color (`0`) and R8 coverage (`1`)
|
||||
([ui_text.frag:9](../../../src/AcDream.App/Rendering/Shaders/ui_text.frag)). ~3-line edit.
|
||||
- Use the simple `Shader` class, **not** `GLSLShader` — no bindless promotion,
|
||||
uniform cache, or `QueueGLAction` teardown is needed for a synchronous
|
||||
main-thread 2D pass.
|
||||
- **Self-contained GL state** (project rule [feedback_render_self_contained_gl_state]):
|
||||
the pass explicitly sets blend (`SrcAlpha/OneMinusSrcAlpha`), `DepthTest` off,
|
||||
**`DepthMask(false)`** (TextRenderer omits this today —
|
||||
[TextRenderer.cs:171](../../../src/AcDream.App/Rendering/TextRenderer.cs)),
|
||||
`CullFace` off, scissor — and restores them. It must not inherit state from the
|
||||
3D pass or ImGui.
|
||||
- **`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`
|
||||
handle (not the bindless `Texture2DArray` the world MDI path uses) — exactly
|
||||
right for the UI batch ([TextureCache.cs:70](../../../src/AcDream.App/Rendering/TextureCache.cs)).
|
||||
The decode chain (Surface → SurfaceTexture → RenderSurface → RGBA8) and the
|
||||
`PFID_*` formats (incl. `PFID_A8`) already work ([SurfaceDecoder.cs:39](../../../src/AcDream.Core/Textures/SurfaceDecoder.cs)).
|
||||
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 loads each candidate ID (`0x06004CC2`,
|
||||
`0x060074BF..C6`, `0x0600129C`, …) via `GetOrUpload`, draws each 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 the spec's follow-up and in code
|
||||
comments before any layout code is written. **No ID is hardcoded on faith.**
|
||||
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**, not one stretched 9-slice texture: 4
|
||||
corner sprites, 4 edge sprites (tiled or stretched along their axis), and a
|
||||
separate center-fill sprite. Slice/edge metrics are a **documented stopgap
|
||||
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) to supply the real insets.
|
||||
(sub-project 3).
|
||||
|
||||
## 7. Markup + stylesheet model
|
||||
|
||||
**Markup** mirrors `ElementDesc` 1:1 ([acclient.h:33693](../../../docs/research/named-retail/acclient.h)):
|
||||
an element has a type, id, `x/y/w/h/z`, the four anchor edge-codes, a
|
||||
`defaultState`, and a media list (sprite DataIDs). Example authoring shape:
|
||||
**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">
|
||||
|
|
@ -182,168 +225,165 @@ an element has a type, id, `x/y/w/h/z`, the four anchor edge-codes, a
|
|||
</panel>
|
||||
```
|
||||
|
||||
This is deliberately the shape the future `LayoutDesc` importer will *emit*, so
|
||||
the authoring format and the imported format converge. It is **not** KSML —
|
||||
KSML is reserved for rich-text content inside text regions (deferred).
|
||||
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, so Spec 1 places
|
||||
it at fixed pixel coords. Building the full solver now would be gold-plating
|
||||
(gap-critic risk #7).
|
||||
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 the `#AARRGGBB` color format (alpha-first) and the
|
||||
`font://Face-Pt[-style]` font URI. The cascade is: element-type defaults from
|
||||
ini → per-element `class=` section → inline attributes. `controls.ini` is
|
||||
**optional** (see §10): if the AC install is absent, the real `[title]`/`[body]`
|
||||
token values are baked as fallback.
|
||||
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)
|
||||
|
||||
Bind to the **real** `VitalsVM` — `HealthPercent` / `StaminaPercent` /
|
||||
`ManaPercent` ([VitalsVM.cs:67](../../../src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs)).
|
||||
The VM already does all server plumbing (CombatState + LocalPlayerState, updated
|
||||
from the wire), so we do **not** re-derive vitals from the retail
|
||||
`gmVitalsUI`/`CACQualities` decomp.
|
||||
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.
|
||||
|
||||
Each bar uses the retail **scissor-fill** technique: draw the empty background
|
||||
rect, set scissor to the bottom `pct * height` pixels, draw the filled rect.
|
||||
Colors Health `#FF0000`, Stamina `#10F0F0`, Mana `#0000FF`. This uses only the
|
||||
solid-color shader path (`uUseTexture=0`) — **no dat font needed**. The
|
||||
`StaminaPercent`/`ManaPercent` nullable case (null until `PlayerDescription`
|
||||
arrives) renders an empty bar.
|
||||
`UiMeter.OnDraw` draws the empty bar (`ctx.DrawRect`) then the filled portion as a
|
||||
**partial-size rect** (`width = pct * Width`) in the bar color — Health `#FF0000`,
|
||||
Stamina `#10F0F0`, Mana `#0000FF`. (For rectangular solid bars this is equivalent
|
||||
to retail's orb scissor-fill and avoids per-quad scissor state inside the batch;
|
||||
scissor/UV-crop comes when the actual orb *sprite* is drawn, later.) A `null`
|
||||
fill (stamina/mana pre-`PlayerDescription`) draws an empty bar.
|
||||
|
||||
The Vitals panel is constructed and registered the same way as today — built in
|
||||
the live-session path and given the player GUID at EnterWorld via
|
||||
`SetLocalPlayerGuid` ([GameWindow.cs:1984](../../../src/AcDream.App/Rendering/GameWindow.cs)) —
|
||||
but registered into `RetailPanelHost` instead of `ImGuiPanelHost` when
|
||||
`ACDREAM_RETAIL_UI=1`.
|
||||
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)
|
||||
|
||||
`IPluginHost` exposes only `Log`/`State`/`Events` today — no UI surface
|
||||
([IPluginHost.cs:9](../../../src/AcDream.Plugin.Abstractions/IPluginHost.cs)). Spec 1
|
||||
adds:
|
||||
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:
|
||||
|
||||
- `IPanelRegistry Panels { get; }` on `IPluginHost` — a one-method
|
||||
`void Register(IPanel)` wrapper over `IPanelHost.Register` (does **not** expose
|
||||
`RenderAll` to plugins).
|
||||
- A `MarkupPanel(string id, string title, string markupPath, object binding)`
|
||||
`IPanel` implementation: owns a parsed `MarkupDocument` + a binding object whose
|
||||
properties the `{Binding}` expressions resolve against.
|
||||
- ALC note: if `AcDream.UI.Abstractions` types cross the plugin boundary, add it
|
||||
to the host-shared exclusion set alongside `AcDream.Plugin.Abstractions`
|
||||
([PluginAssemblyLoadContext.cs:13](../../../src/AcDream.Core/Plugins/PluginAssemblyLoadContext.cs)).
|
||||
- Registrations from `IAcDreamPlugin.Enable()` (main thread, before the GL window
|
||||
opens) buffer into a list the host drains into `RetailPanelHost` after init —
|
||||
the threading concern lives in the host, the plugin call is unconditional.
|
||||
|
||||
The first consumer is the first-party Vitals panel, but the contract lands here
|
||||
so the markup format is designed against a real plugin path rather than
|
||||
retrofitted. Wiring an actual plugin-supplied panel end-to-end is a thin
|
||||
follow-up.
|
||||
- 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.** Frame + live (updating) bars; the close button is
|
||||
drawn-not-clickable and the window is not draggable. Input/hit-testing is its
|
||||
own sub-phase — neither `IPanelHost` nor `IPanelRenderer` carries a hit-test or
|
||||
bounds contract today, and building it up front is scope creep.
|
||||
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 for when it's present;
|
||||
when absent, fall back to the source-verified `[title]`/`[body]` token values.
|
||||
The build never fails on a missing AC install. (Chrome is sprite-based, so
|
||||
`controls.ini` is barely load-bearing for Spec 1 anyway.)
|
||||
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: load candidate chrome IDs, render raw quads, confirm real IDs | Resolves the chrome-ID contradiction empirically |
|
||||
| 1 | One decoded dat sprite drawn at fixed coords (shader `uUseTexture=2`, self-contained GL state) | A dat sprite composites correctly over the 3D scene |
|
||||
| 2 | 8-piece border + center → an empty titled frame (UV-rect quads, stopgap insets) | The frame renders |
|
||||
| 3 | Three scissor-fill bars bound to real `VitalsVM` (solid-color path) | End-to-end data binding, no font needed |
|
||||
| 4 | `RetailPanelHost` wired into the frame loop, gated by `RuntimeOptions.RetailUi`; ImGui unaffected | Backend slots under the seam; no `ACDREAM_DEVTOOLS` regression |
|
||||
| 5 *(deferred)* | `AcFont` dat-glyph loader → numeric overlays | Only if numbers are wanted in this slice |
|
||||
| 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 by
|
||||
design; Step 0 catches it. A null/zero DataID in markup logs a warning and
|
||||
draws nothing (no throw).
|
||||
- **AC install absent** → `controls.ini` load is skipped, baked fallback tokens
|
||||
used (no throw).
|
||||
- **Vitals null percents** (pre-`PlayerDescription`) → empty bar.
|
||||
- **Window resize** → fixed-coord placement re-clamps to stay on-screen via the
|
||||
existing `OnFramebufferResize` panel-layout reset
|
||||
([GameWindow.cs:10375](../../../src/AcDream.App/Rendering/GameWindow.cs)). No DPI
|
||||
scaling (a known, out-of-scope gap — `_window.Size` is treated as framebuffer
|
||||
size).
|
||||
- **Both toggles on** → both UIs render; the retail Vitals and the ImGui Vitals
|
||||
may both show (acceptable in dev).
|
||||
- **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` parsing,
|
||||
`font://` URI parsing, the cascade order. Since the parsers live in
|
||||
`src/AcDream.App/UI/Retail/` (per §4), their tests go in
|
||||
`tests/AcDream.App.Tests/` (App-layer, Rule 6). If/when the backend is
|
||||
extracted to a standalone `AcDream.UI.Retail` project, the tests move with it
|
||||
to `tests/AcDream.UI.Retail.Tests/` (registered in `AcDream.slnx`).
|
||||
- **`MarkupDocument` parser** — unit tests for the XML → element-tree mapping and
|
||||
`{Binding}` resolution against a fake binding object.
|
||||
- **`NineSlice` geometry** — unit test that 8 pieces + center tile to the right
|
||||
rects for a given frame size + insets.
|
||||
- **Visual acceptance** (user) — the Vitals frame renders retail-shaped with live
|
||||
bars in `ACDREAM_RETAIL_UI=1`; ImGui panels unaffected in `ACDREAM_DEVTOOLS=1`.
|
||||
- **`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 — this is a parallel
|
||||
track, not a milestone flip.
|
||||
- **Divergence register:** in the commit that ships the first real dat-sprite
|
||||
chrome, **delete row TS-30** ([retail-divergence-register.md:166](../../../docs/architecture/retail-divergence-register.md))
|
||||
and **add one** new IA-row (Intentional Architecture — keystone.dll has no
|
||||
PDB/decomp, a byte-port is impossible by definition) 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 (which already covers the
|
||||
UI toolkit's *behavioral* approximation). A second row for the stopgap slice
|
||||
insets is added if/when they ship.
|
||||
- **Spec file:** this document, `docs/superpowers/specs/2026-06-14-d2b-retail-panel-frame-design.md`.
|
||||
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/hit-testing contract** — neither `IPanelHost` nor `IPanel` reports
|
||||
bounds; required before drag/close-click. Next sub-phase.
|
||||
- **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). Sub-phase for numeric overlays.
|
||||
- **Anchor solver** — port `StateDesc::UpdateSizeAndPosition`. With the
|
||||
`LayoutDesc` importer.
|
||||
- **`LayoutDesc` binary importer** (sub-project 3) — bulk-transpile retail's
|
||||
layouts → our markup, supplying real slice insets + coords. Resolver symbols:
|
||||
`LayoutDesc::InqFullDesc` @`0x0069A520`, `ElementDesc::Incorporate` @`0x0069B5A0`
|
||||
(algorithm captured in [2026-05-08 pseudocode](../../../docs/research/2026-05-08-retail-ui-layout-resolution-pseudocode.md)).
|
||||
- **Standalone `AcDream.UI.Retail` project** — after a rendering-primitives home
|
||||
is extracted.
|
||||
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 `ACDREAM_RETAIL_UI=1`: a retail-shaped Vitals window renders with an
|
||||
8-piece dat-sprite border + title bar + drawn close button, and three
|
||||
scissor-fill bars that track HP/Stam/Mana live as the character takes
|
||||
damage / regens.
|
||||
- [ ] In `ACDREAM_DEVTOOLS=1`: ImGui Vitals/Chat/Debug/Settings unchanged (no
|
||||
regression).
|
||||
- [ ] 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.
|
||||
- [ ] `IPanelRegistry` on `IPluginHost`; a `MarkupPanel` exists and is unit-tested
|
||||
against a fake binding.
|
||||
- [ ] `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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue