acdream/docs/plans/2026-04-24-ui-framework.md
Erik 99ce541fd7 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.
2026-04-24 23:46:45 +02:00

10 KiB

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:

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.

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)

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.