docs(ui): plan the staged UI-backend strategy

Two-stage rollout, one stable abstraction layer:

  1. Short-term: Hexa.NET.ImGui as the backend. Wire up in days, iterate
     game logic (chat, inventory, vitals) in weeks. Looks like a debugger,
     acceptable while we prove the interaction logic end-to-end.

  2. `AcDream.UI.Abstractions` — ViewModels + Commands + `IPanel` /
     `IPanelRenderer` interfaces. Backend-agnostic. Plugin API targets
     this layer; plugins never see ImGui.

  3. Long-term: custom retail-look backend using dat assets. Swap panel
     by panel. ImGui stays forever as the `ACDREAM_DEVTOOLS=1` overlay.

The new doc (`2026-04-24-ui-framework.md`) captures:
- Full design of the three-layer split
- Why Hexa.NET.ImGui over ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui
  (AOT readiness, tracks upstream ImGui faster, cleaner native-lib
  bundling)
- Alternatives considered and ruled out (Myra, Avalonia, NoesisGUI,
  RmlUi, pure custom from day one)
- Implementation order (Sprint 1 vitals HUD → Sprint 2 interaction
  panels → Sprint 3 plugin API → Sprint 4+ more panels → later
  custom retail-look)
- Risks + mitigations and open questions deferred to implementation

Roadmap Phase D updated with a pointer to the new plan so future
sessions start from the latest strategy, not the original
all-custom-from-day-one Phase D description.

No code changes yet. Ready to start Sprint 1 when approved.
This commit is contained in:
Erik 2026-04-24 23:46:45 +02:00
parent 593b76fda1
commit 99ce541fd7
2 changed files with 277 additions and 0 deletions

View file

@ -120,6 +120,26 @@ Plus polish that doesn't get its own phase number:
**Goal:** chat window, nameplates, inventory, and audio. Can run concurrently with Phase B or C because it doesn't touch gameplay/net/rendering surfaces.
> **Updated 2026-04-24 — staged backend strategy.** We split Phase D into
> two stages so game logic can be validated quickly without waiting for
> the full retail-look custom toolkit. See
> [`docs/plans/2026-04-24-ui-framework.md`](2026-04-24-ui-framework.md)
> for the full design. Short version:
>
> 1. **D.2a — Hexa.NET.ImGui as the short-term backend.** Wire up in days,
> iterate game logic (chat-send, inventory actions, vitals HUD reading
> real state) in weeks. Looks like a debugger; that's fine.
> 2. **Stable `AcDream.UI.Abstractions` layer** — ViewModels + Commands +
> `IPanel` / `IPanelRenderer` interfaces. Backend-agnostic. Plugin API
> publishes against this layer and never sees ImGui.
> 3. **D.2b onward — custom retail-look backend** using dat assets (icons,
> panels, fonts). Swap one panel at a time; ImGui stays forever as the
> `ACDREAM_DEVTOOLS=1` overlay for packet trace / state dump / dat browser.
>
> Every sub-piece below (D.3 AcFont, D.4 dat sprites, D.5 core panels,
> D.6 HUD, D.7 cursors) ships against the abstraction layer, so it
> doesn't matter which backend is drawing when the port lands.
**Sub-pieces:**
- **D.1 — 2D ortho overlay + font rendering.** ✅ SHIPPED 2026-04-17 as the dev-facing debug overlay (StbTrueTypeSharp system-font atlas + `TextRenderer` + `DebugOverlay`).
- **D.2 — Retail UI framework + first panels.** Research + scaffold landed 2026-04-17 (see `docs/research/retail-ui/`). Ships:

View file

@ -0,0 +1,257 @@
# UI framework plan
**Date:** 2026-04-24
**Status:** design — not yet implemented
**Owner:** lead engineer (erik) + Claude
Captures the UI strategy agreed via discussion on 2026-04-24. Documents
the choices AND the alternatives considered so future sessions can
re-evaluate with the same context.
## Goal
acdream needs a playable game UI: chat, vitals HUD, inventory, character
panel, skills, spellbook, fellowship, allegiance, trade, options, map,
quest log, tooltips — and a first-class plugin API so plugin authors can
ship their own panels.
## Strategy: two-phase, one stable interface
```
┌─────────────────────────────────────────┐
│ UI backend ─ ImGui (short-term) │ ← swappable
│ ─ Custom retail (later) │
├─────────────────────────────────────────┤
│ ViewModels + Commands (per panel) │ ← stable contracts
├─────────────────────────────────────────┤
│ Game state + events + net (existing) │ ← unchanged
└─────────────────────────────────────────┘
```
- **Near term:** wire an ImGui-based overlay so we can iterate on game
logic fast — chat that actually sends + receives, inventory that
reflects real state, vitals bar that reads real HP/stam/mana. Looks
like a debugger, that's fine for now.
- **Later:** replace the visual layer with a custom toolkit that uses
retail dat assets (icons, panels, fonts) and matches retail's feel.
- **Always:** ViewModels and Commands stay stable across the swap. The
game logic never learns which backend is drawing it.
## Choice: Hexa.NET.ImGui for the short-term backend
Decision: **`Hexa.NET.ImGui`** + its bundled `Hexa.NET.ImGui.Backends.OpenGL3`.
### Why Hexa over ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui
- **Auto-generated from cimgui, tracks upstream ImGui closely** — docking,
viewports, tables current within days of release.
- **Native-AOT first-class** — single-file publish is painless; aligns
with where we want acdream to land long-term for distribution.
- **Per-RID native-lib bundling** is cleaner — no separate `cimgui.dll`
side-loading to manage.
- Ships its own Silk.NET-compatible OpenGL3 backend; the Silk.NET official
extension is unnecessary.
### What we give up
- `Silk.NET.OpenGL.Extensions.ImGui` is battle-tested with hundreds of
Silk.NET sample projects. Hexa's backend is newer, slightly higher risk
of edge-case bugs.
- Mitigation: keep integration tight (~50 lines) so switching to
ImGui.NET later is a one-morning operation if Hexa misbehaves.
### Why ImGui at all (vs. going straight to custom)
- Get game logic validated end-to-end in weeks, not months.
- ImGui stays forever as the **devtools layer** (`ACDREAM_DEVTOOLS=1`):
packet trace inspector, state dump, dat browser. Having a working
ImGui integration is a permanent asset even after game UI moves off it.
- Lets us design the **plugin API and ViewModel contracts against a
real running panel** rather than designing in the abstract.
## The three layers in detail
### Layer 1 — Game state (unchanged)
Already exists: `IGameState`, `IEvents` (plugin-host interfaces),
`WorldSession`, `PlayerWeenie`, `Inventory`, `SpellBook`, `LightManager`,
the live-session wire code. The UI reads from these; we add nothing.
### Layer 2 — ViewModels + Commands
New module: `src/AcDream.UI.Abstractions/`. Backend-agnostic.
**ViewModels** — per-panel data contracts. Example:
```csharp
public sealed record VitalsVM(
int HpCurrent, int HpMax,
int StamCurrent, int StamMax,
int ManaCurrent, int ManaMax,
float HpRegenRate,
bool Stunned,
bool LowHpWarning);
public sealed record InventoryVM(
IReadOnlyList<InventoryItemVM> Items,
int BurdenCurrent,
int BurdenCapacity);
public sealed record ChatVM(
IReadOnlyList<ChatLineVM> Recent,
int UnreadCount,
ChatChannel ActiveInputChannel);
```
Built from `IGameState` each frame (cheap record allocation). Observable
via `IEvents` subscription for panels that want push updates instead of
pull.
**Commands** — user actions going back.
```csharp
public sealed record UseItemCmd(uint ItemGuid);
public sealed record SendChatCmd(ChatChannel Channel, string Text);
public sealed record CastSpellCmd(uint SpellId, uint? TargetGuid);
public sealed record DragItemCmd(uint ItemGuid, uint DestContainerGuid, int Slot);
public sealed record EquipItemCmd(uint ItemGuid, EquipSlot Slot);
```
Dispatched to an `ICommandBus` that routes to the appropriate subsystem
(`WorldSession.SendSelect`, `ChatService.Send`, etc.).
### Layer 3 — UI backend
New module: `src/AcDream.UI.ImGui/`. References `AcDream.UI.Abstractions`.
- Thin adapter: each panel implements an `IPanel` interface — `Draw(VM)`
method, emits commands on interaction.
- Panels registered into an `IPanelHost` which the backend iterates
per frame.
- Keyboard / mouse / focus handled by ImGui natively.
Later: `src/AcDream.UI.Retail/` references the same `AcDream.UI.Abstractions`
and implements the same `IPanel` / `IPanelHost` interfaces — but draws
with our own retained-mode toolkit + retail dat assets.
## Plugin API (must be backend-agnostic)
```csharp
public interface IPanel
{
string Id { get; }
string Title { get; }
void Draw(in PanelContext ctx);
}
public readonly ref struct PanelContext
{
public readonly IGameState State;
public readonly ICommandBus Commands;
public readonly IPanelRenderer Renderer; // drawing primitives
// (widget calls flow through Renderer so the panel never references
// ImGuiNET / Hexa / our custom widget namespaces directly)
}
```
`IPanelRenderer` exposes a retail-UI-friendly primitive set: `Panel`,
`Label`, `Button`, `TextField`, `ScrollView`, `ListView`, `Icon`, `Tab`,
`ProgressBar`, `DragSource`, `DropTarget`. ImGui implementation wraps
ImGui calls; custom implementation uses our retained-mode toolkit.
**Key discipline:** no panel references Hexa.NET.ImGui directly. If a
panel needs a feature the abstraction doesn't expose, add it to
`IPanelRenderer`, don't reach through.
## Implementation order
### Sprint 1 — Infrastructure + first visible panel
1. `AcDream.UI.Abstractions`: `IPanel`, `IPanelHost`, `IPanelRenderer`,
`ICommandBus`, base ViewModels (start with `VitalsVM` only).
2. `AcDream.UI.ImGui`: Hexa.NET.ImGui wired, `ImGuiPanelRenderer`
implementation of `IPanelRenderer` (Label, Button, Panel, ProgressBar
is enough for vitals).
3. `GameWindow`: host the `IPanelHost`; render on top of scene when
`ACDREAM_DEVTOOLS=1`.
4. `VitalsPanel` — first real panel. Reads HP/stam/mana from
`IGameState`, renders three progress bars.
**Success criteria:** launch the client, see three coloured bars in the
top-left that actually reflect the character's current vitals as they
walk around / take damage / regen.
### Sprint 2 — Interaction panels
- `ChatPanel` — reads `ChatVM`, emits `SendChatCmd`. `ICommandBus`
routes to `WorldSession`.
- `InventoryPanel` — reads `InventoryVM`, click-to-select, double-click
to equip, drag target for future move.
- `CharacterPanel` — attributes, skills, XP.
### Sprint 3 — Plugin API hardening
- Document the `IPanel` contract.
- Port the smoke plugin to register a demo panel via the API.
- Confirm plugins can subscribe to game events AND draw UI through the
same interface.
### Sprint 4+ — More panels
Spellbook, allegiance, fellowship, trade, map, quest log, options.
Continue to expand `InventoryPanel` with drag-drop, split, appraise.
### Later — Custom retail-look backend
`AcDream.UI.Retail` implements `IPanelRenderer` with our own toolkit +
retail dat assets. Swap panels one at a time. ImGui overlay remains for
devtools.
## Non-goals for this first pass
- **Not** going to theme ImGui to look retail. Waste of effort when we'll
swap the backend. Devtools aesthetic is fine.
- **Not** porting retail's widget code. We use their ASSETS later, not
their widget implementation.
- **Not** building layout DSL / XAML-like markup. Panels register and
draw procedurally, same as ImGui.
## Alternatives considered
| Option | Pros | Cons | Why not picked |
|---|---|---|---|
| ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui | Official Silk.NET path, battle-tested | Lags upstream ImGui, AOT story has sharp edges | Hexa tracks upstream faster, cleaner AOT |
| Myra | Retained-mode, less-debug-y look | Needs a custom Silk.NET backend (~300 LOC), slower iteration | ImGui is faster to first pixel; aesthetics will move to custom anyway |
| Avalonia | Mature, XAML designer, great devtools | Hostile to Silk.NET render loop, huge dep | Integration cost too high, aesthetics wrong |
| NoesisGUI | Slick, XAML-like, production-quality | Commercial license, big dep | Premature optimization |
| RmlUi | HTML/CSS mental model | Bindings immature, own render backend needed | Too much glue |
| Pure custom on Silk.NET from day one | Full control, retail look immediately | Months of work before first visible panel | Can't validate game logic fast enough |
## Risks + mitigations
- **Risk:** `IPanelRenderer` grows to leak ImGui-isms.
**Mitigation:** code review every addition; if a feature only exists
in ImGui and the retail toolkit can't express it, don't add it.
- **Risk:** Swap to custom backend breaks a dozen panels simultaneously.
**Mitigation:** swap one panel at a time, keep ImGui rendering the
rest until all are ported.
- **Risk:** Plugin authors write panels that only work in ImGui.
**Mitigation:** smoke plugin registers a panel early; use it as a
canary whenever backend changes.
- **Risk:** Hexa.NET.ImGui stops being maintained.
**Mitigation:** integration is small (<100 LOC), switching to
ImGui.NET is a one-morning operation.
## Open questions (defer to implementation)
- Where does input focus live — ImGui captures keyboard by default when
a text field is active, does our game-side input system need to check
"did ImGui want this event"? (Yes, standard pattern. Wire
`io.WantCaptureKeyboard` gate.)
- Do devtools panels ship in release builds? (Yes, gated on env var, cost
is negligible when disabled.)
- Modal dialogs? Drag-drop? Fleshed out in Sprint 2 when we have
inventory actually working.