# 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 Items, int BurdenCurrent, int BurdenCapacity); public sealed record ChatVM( IReadOnlyList 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.