Closes Phase D.2a. Launch with ACDREAM_DEVTOOLS=1 now shows a live
ImGui "Vitals" window whose HP bar reads CombatState.GetHealthPercent
for the local player. Without the env var the branches are dead code,
no ImGui context is created, and behaviour is identical to before.
GameWindow hunks:
- fields: _imguiBootstrap / _panelHost / _vitalsVm + DevToolsEnabled
- init (OnLoad): construct bootstrap + host, register VitalsPanel
- GUID push: _vitalsVm?.SetLocalPlayerGuid(chosen.Id) at live-connect
- frame begin: _imguiBootstrap.BeginFrame(dt) after GL clear
- frame end: _panelHost.RenderAll(ctx) + _imguiBootstrap.Render() after debug overlay
- input gating: skip WASD when ImGui.GetIO().WantCaptureKeyboard
Backend pivot: Hexa.NET.ImGui → ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui.
First-light integration with the Hexa backend crashed 0xC0000005 inside
Hexa.NET.ImGui.Backends.OpenGL3.ImGuiImplOpenGL3.InitNative. Root cause:
Hexa's native OpenGL3 backend resolves GL function pointers via GLFW or
SDL internally; with Silk.NET (which uses neither) the pointers are null
and the native code crashes on first use. The mitigation path was
already planned — the design doc's Risk section called a pivot to
ImGui.NET a "one-morning operation" — and that's exactly what happened.
- Packages: Hexa.NET.ImGui 2.2.9 + Hexa.NET.ImGui.Backends 1.0.18
→ ImGui.NET 1.91.6.1 + Silk.NET.OpenGL.Extensions.ImGui 2.23.0
- ImGuiBootstrapper: was static Initialize(gl)+Shutdown() wrapping
Hexa's OpenGL3 init; now an IDisposable wrapping Silk.NET's
ImGuiController instance which handles GL backend init + input
subscription in one go.
- SilkInputBridge.cs deleted (~190 LOC): ImGuiController subscribes
IKeyboard / IMouse events itself, we don't need a bespoke bridge.
- ImGuiPanelRenderer: ImGuiNET.ImGui.* calls instead of
Hexa.NET.ImGui.ImGui.*. Widget surface unchanged.
Boundary discipline is preserved — no panel imports ImGuiNET; only
ImGuiPanelRenderer does. The D.2b custom toolkit will implement the
same IPanelRenderer contract without touching panel code.
Out of scope (tracked for follow-up):
- Stam/Mana currently return float? null (VitalsVM). Absolute values
need LocalPlayerState + PlayerDescription (0x0013) parsing to be
stored rather than discarded — filed as a post-D.2a issue.
- Mouse-capture gating (WorldMouseFallThrough-style click-through
tests) — not needed until we add clickable inventory items.
Roadmap + memory + architecture doc + UI framework plan updated in the
same commit per CLAUDE.md roadmap-discipline rules. 753 tests pass
(550 Core + 192 Core.Net + 11 new UI.Abstractions), 0 build warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
295 lines
12 KiB
Markdown
295 lines
12 KiB
Markdown
# UI framework plan
|
|
|
|
**Date:** 2026-04-24 (design), shipped 2026-04-25
|
|
**Status:** **Phase D.2a shipped** — `AcDream.UI.Abstractions` + ImGui backend
|
|
+ `VitalsPanel` gated on `ACDREAM_DEVTOOLS=1`. Backend pivoted from
|
|
`Hexa.NET.ImGui` to `ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui` during
|
|
first-light integration — see the pivot note below. Phase D.2b (custom
|
|
retail-look backend) remains design-only.
|
|
**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.
|
|
|
|
## 2026-04-25 pivot: Hexa.NET.ImGui → ImGui.NET
|
|
|
|
The original choice (documented below) was `Hexa.NET.ImGui` +
|
|
`Hexa.NET.ImGui.Backends.OpenGL3`. It did not survive first-light
|
|
integration:
|
|
|
|
- First launch with Hexa's backend crashed with `0xC0000005` inside
|
|
`Hexa.NET.ImGui.Backends.OpenGL3.ImGuiImplOpenGL3.InitNative`.
|
|
- Root cause: Hexa's native OpenGL3 backend does its own GL function
|
|
resolution, looking up symbols via GLFW or SDL. Silk.NET uses neither,
|
|
so the resolved function pointers were null and the native code
|
|
dereferenced them on init.
|
|
- Hexa's Silk.NET examples rely on GLFW being co-loaded (its default
|
|
on Hexa's own scenes) — not applicable here.
|
|
|
|
**Mitigation path** was already written into this doc (§"What we give
|
|
up": *"switching to ImGui.NET later is a one-morning operation if Hexa
|
|
misbehaves"*) and taken:
|
|
|
|
- Packages swapped → `ImGui.NET 1.91.6.1` + `Silk.NET.OpenGL.Extensions.ImGui 2.23.0`.
|
|
- `Silk.NET.OpenGL.Extensions.ImGui.ImGuiController` handles the whole
|
|
integration (GL backend init against the Silk.NET GL binding,
|
|
keyboard + mouse IO event subscription). No hand-written input
|
|
bridge needed.
|
|
- `ImGuiBootstrapper` is a ~10-line `IDisposable` wrapping the
|
|
`ImGuiController` instance. `ImGuiPanelRenderer` wraps `ImGuiNET.ImGui.*`.
|
|
- Boundary discipline preserved — panels never import `ImGuiNET`
|
|
directly; they only use `IPanelRenderer`. The backend swap is
|
|
invisible above the abstraction layer, as designed.
|
|
|
|
Sections below from §"Choice: Hexa.NET.ImGui" onward are kept as the
|
|
historical design reasoning. They remain useful if we ever re-evaluate
|
|
native AOT / upstream-tracking tradeoffs.
|
|
|
|
## 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.
|