From de9229eed5b5e3d636d44155d06588838c161ca9 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 14 Jun 2026 14:00:14 +0200 Subject: [PATCH] =?UTF-8?q?docs(D.2b):=20design=20spec=20=E2=80=94=20retai?= =?UTF-8?q?l=20panel=20frame=20+=20live=20Vitals=20(Approach=20C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brainstormed design for the D.2b retail-look UI backend: our own KSML-style markup + controls.ini stylesheet + retained-mode toolkit on Silk.NET (no embedded browser, zero external deps — Approach C, chosen over Ultralight/CEF and RmlUi for memory/dep-weight/faithfulness). Spec 1 scope: an 8-piece dat-sprite window frame + live Vitals bars bound to the existing VitalsVM, gated behind ACDREAM_RETAIL_UI=1, rendered via a reused TextRenderer batch. Render-only (input/hit-test, AcFont glyphs, anchor solver, LayoutDesc importer all deferred). Grounded by a read-only research workflow (7 readers + gap-critic). The critic corrected several stale memory/plan-doc facts now baked into the spec's do-not-trust list: VitalsVM is a sealed class (not the old record); chrome sprite IDs are unverified (Step-0 dat prove-out resolves them empirically); controls.ini exists and #FFDBD6A8 is editbox text not a bg; DatCollection reads are thread-safe; KSML is rich-text not the layout language (we mirror ElementDesc). Phase D.2b / Milestone M5 (parallelizable with M3/M4 — opened as a parallel track while M1.5 stays the active critical-path milestone). Retires divergence row TS-30 + adds one IA row when the chrome ships. Also gitignores the /.superpowers/ visual-companion scratch dir. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 2 + ...026-06-14-d2b-retail-panel-frame-design.md | 349 ++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-14-d2b-retail-panel-frame-design.md diff --git a/.gitignore b/.gitignore index 357fded9..ca2f9cf2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,8 @@ references/* # Claude Code session state .claude/ +# Superpowers brainstorm visual-companion scratch (mockups regenerate; not source) +/.superpowers/ launch.log launch-*.log launch.utf8.log diff --git a/docs/superpowers/specs/2026-06-14-d2b-retail-panel-frame-design.md b/docs/superpowers/specs/2026-06-14-d2b-retail-panel-frame-design.md new file mode 100644 index 00000000..4884b0ff --- /dev/null +++ b/docs/superpowers/specs/2026-06-14-d2b-retail-panel-frame-design.md @@ -0,0 +1,349 @@ +# 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 +**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. + +--- + +## 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. + +**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. + +**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. + +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. + +## 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`). + +**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"). +- The full anchor solver (`StateDesc::UpdateSizeAndPosition` port). +- The `LayoutDesc` binary importer (sub-project 3). +- Reskinning Chat / Debug / Settings panels. +- 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) + +The research caught several load-bearing "facts" that were wrong or unverified. +These are binding: + +| Claimed (memory / plan doc) | 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. | + +## 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. + +``` +┌──────────────────────────────────────────────────────────┐ +│ retail dat (read-only fidelity source) │ +│ controls.ini → style tokens · RenderSurface 0x06xxxxxx │ +│ → sprites · Font 0x40xxxxxx → glyphs (deferred) │ +└───────────────┬──────────────────────────────────────────┘ + │ assets via TextureCache.GetOrUpload +┌───────────────▼──────────────────────────────────────────┐ +│ 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) │ +└───────────────┬──────────────────────────────────────────┘ + │ VMs out / Commands in (unchanged) +┌───────────────▼──────────────────────────────────────────┐ +│ AcDream.UI.Abstractions (exists) — IPanel/IPanelHost/ │ +│ IPanelRenderer/ICommandBus/PanelContext/VitalsVM │ +└───────────────┬──────────────────────────────────────────┘ + │ +┌───────────────▼──────────────────────────────────────────┐ +│ game state (unchanged) — CombatState, LocalPlayerState │ +└──────────────────────────────────────────────────────────┘ +``` + +**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. + +## 5. Render foundation — reuse, don't rebuild + +`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: + +- **`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. + +## 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)). + +**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 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 +constant** (with a divergence row) until the `LayoutDesc` tree is parsed +(sub-project 3) to supply the real insets. + +## 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: + +```xml + + + + + +``` + +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). + +**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). + +**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. + +## 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. + +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. + +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`. + +## 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: + +- `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. + +## 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. +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.) + +## 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 | + +## 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). + +## 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`. +- `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`. + +## 15. Open gaps & deferred sub-projects + +- **Input/hit-testing contract** — neither `IPanelHost` nor `IPanel` reports + bounds; required before drag/close-click. Next sub-phase. +- **`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. +- **`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). +- [ ] `controls.ini` loads when present, falls back cleanly when absent. +- [ ] `IPanelRegistry` on `IPluginHost`; a `MarkupPanel` exists and is unit-tested + against a fake binding. +- [ ] TS-30 deleted + one new IA-row added, same commit as the chrome. +- [ ] `dotnet build` green, `dotnet test` green. +- [ ] Visual verification by the user.