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>
This commit is contained in:
Erik 2026-06-14 16:56:57 +02:00
parent 064ef41ce4
commit b18403da02
4 changed files with 102 additions and 15 deletions

View file

@ -37,7 +37,7 @@ accepted-divergence entries (#96, #49, #50).
---
## 1. Intentional architecture (IA) — 14 rows
## 1. Intentional architecture (IA) — 15 rows
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
@ -55,6 +55,7 @@ accepted-divergence entries (#96, #49, #50).
| IA-12 | UI toolkit mirrors retail behavior from research docs, not a byte-port — keystone.dll is outside decomp coverage; observed constants embedded (drag 3 px, tooltip 1000 ms) | `src/AcDream.App/UI/README.md:3` | keystone.dll has no PDB/decomp; semantics reconstructed from the six `docs/research/retail-ui/` deep-dives, keeping retail's event-type constants so panel switch-cases transplant cleanly | Edge-case input semantics the research under-specified (drag threshold, tooltip timing, focus hand-off, capture corners) differ silently with no oracle to diff against | keystone.dll Device DAT_00837ff4; docs/research/retail-ui/04-input-events.md |
| IA-13 | GameEventType registry deliberately omits event types retail ignores; unknown events fall through unhandled | `src/AcDream.Core.Net/Messages/GameEventType.cs:11` | Retail also ignores them — dropping matches retail by construction | If the "retail ignores X" judgment is wrong for any opcode (or a server mod uses one), the event is silently dropped with no diagnostic pointing at the omission | retail GameEvent dispatch (ignored-event set) |
| IA-14 | Rendering + dat-handling base is WorldBuilder's tested port, not a fresh retail-decomp port (Phase N.4/O design stance) | `docs/architecture/worldbuilder-inventory.md` (code at `src/AcDream.{Core,App}/Rendering/Wb/`) | WB visually verified on the AC world, MIT, same stack; known WB↔retail deltas resolved case-by-case — terrain split kept retail `FSplitNESW` (**#51**, pinned by `SplitFormulaDivergenceTest`), scenery drift accepted (AP-31) | A WB-upstream divergence not yet caught ships silently as "our" behavior; guard = the inventory doc's 🟢/🔴 split + per-formula divergence tests | retail decomp per algorithm; `tests/.../SplitFormulaDivergenceTest.cs` |
| IA-15 | D.2b retail UI is our own UiHost/UiElement retained-mode tree drawing an 8-piece dat-sprite window frame (later: XML markup + controls.ini stylesheet), not a byte-port of keystone.dll's LayoutDesc binary tree | `src/AcDream.App/UI/UiNineSlicePanel.cs` + `RetailChromeSprites.cs` | keystone.dll has no PDB/decomp so a byte-port is impossible by definition; we mirror retail's ElementDesc field model + controls.ini tokens, and the chrome sprites ARE the real dat RenderSurfaces (Step-0 prove-out 2026-06-14 confirmed 0x06004CC2 center + 0x060074BF..C6 bevel) | The 8-piece edge/corner→position mapping is a guess until the LayoutDesc 0x21000040 parse; anchor resolution at non-800x600 + controls.ini cascade corners differ silently with no oracle | LayoutDesc 0x21000040; controls.ini tokens; keystone.dll layout eval (no PDB) |
---
@ -130,7 +131,7 @@ accepted-divergence entries (#96, #49, #50).
---
## 4. Temporary stopgap (TS) — 30 rows
## 4. Temporary stopgap (TS) — 29 rows
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
@ -163,7 +164,6 @@ accepted-divergence entries (#96, #49, #50).
| TS-27 | Retransmit handling absent: `RetransmitRequests`/`RejectRetransmit` parsed, but nothing re-sends lost outbound or requests missing inbound sequences (class-doc gap list otherwise stale — ack/position/chat exist) | `src/AcDream.Core.Net/WorldSession.cs:29` | Deferred since the one-shot test harness; dev loop is loopback (no loss) | On any lossy link a dropped fragment is gone forever — entities never spawn, chat vanishes, reassembly stalls; server retransmit requests ignored until session timeout. Stale doc list also misleads readers | PacketHeaderFlags RequestRetransmit 0x1000 / Retransmission 0x1 |
| TS-28 | LoginComplete sent on PlayerCreate (0xF746) arrival; retail sends it after the portal-space transition animation finishes (no such animation exists yet) | `src/AcDream.Core.Net/Messages/GameActionLoginComplete.cs:30` | acdream has no portal-space animation; "InWorld" phrasing in the file is slightly stale (trigger is PlayerCreate) | Server flips the character out of the loading state and pushes initial updates while the client may still be streaming — server logic assuming retail's load-screen duration fires against a half-initialized client | retail post-EnterWorld flow (holtburger messages.rs:391-422) |
| TS-29 | Background music (MIDI) + ambient loops not ported: PlayMusic/StopMusic no-op; StartAmbient reserves a handle that never plays | `src/AcDream.App/Audio/OpenAlAudioEngine.cs:331` | Explicitly outside R5 audio-phase scope; a landblock-attached ambient system is planned separately | Silent world where retail has music/atmosphere; code trusting StartAmbient's handle to mean "playing" is already subtly wrong (StopAmbient looks up a never-created source) | retail MIDI + ambient system (r05) |
| TS-30 | UI panels drawn as flat translucent rectangles + 1 px border; retail composes 9-slice dat sprite backgrounds via LayoutDesc trees | `src/AcDream.App/UI/UiPanel.cs:10` | Development visibility until the D.2b retail-look toolkit consumes the dat assets | Purely visual until D.2b — but pixel-position assumptions built against the placeholder (hit regions, layout constants) may not survive the swap to retail sprite metrics | RenderSurface 0x06xxxxxx 9-slice; LayoutDesc 0x21xxxxxx |
---