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.
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.dllside-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.ImGuiis 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
IPanelinterface —Draw(VM)method, emits commands on interaction. - Panels registered into an
IPanelHostwhich 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
AcDream.UI.Abstractions:IPanel,IPanelHost,IPanelRenderer,ICommandBus, base ViewModels (start withVitalsVMonly).AcDream.UI.ImGui: Hexa.NET.ImGui wired,ImGuiPanelRendererimplementation ofIPanelRenderer(Label, Button, Panel, ProgressBar is enough for vitals).GameWindow: host theIPanelHost; render on top of scene whenACDREAM_DEVTOOLS=1.VitalsPanel— first real panel. Reads HP/stam/mana fromIGameState, 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— readsChatVM, emitsSendChatCmd.ICommandBusroutes toWorldSession.InventoryPanel— readsInventoryVM, click-to-select, double-click to equip, drag target for future move.CharacterPanel— attributes, skills, XP.
Sprint 3 — Plugin API hardening
- Document the
IPanelcontract. - 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:
IPanelRenderergrows 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.WantCaptureKeyboardgate.) - 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.