acdream/memory/project_ui_architecture.md

6.5 KiB

Project: UI architecture — three-layer stable pattern

Agreed 2026-04-24. Read once per session. Full design: docs/plans/2026-04-24-ui-framework.md.

The separation

┌─────────────────────────────────────────┐
│  UI backend (ImGui today / custom later)│  ← swappable
├─────────────────────────────────────────┤
│  ViewModels + Commands (per-panel)      │  ← stable contracts
├─────────────────────────────────────────┤
│  Game state + events + net (unchanged)  │  ← game logic
└─────────────────────────────────────────┘
  • UI backend (bottom swap axis): ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui for Phase D.2a (short-term, debugger-look, validates game logic fast). Custom retail-look toolkit for Phase D.2b (long-term, uses dat assets). ImGui stays forever as the ACDREAM_DEVTOOLS=1 overlay. (Pivoted from Hexa.NET.ImGui on 2026-04-25 — Hexa's native OpenGL3 backend resolves GL via GLFW/SDL internally and crashed 0xC0000005 against Silk.NET; the Silk.NET extension is purpose-built for this stack.)
  • ViewModels + Commands (the stable contract): per-panel data records (VitalsVM, InventoryVM, ChatVM, …) and action records (UseItemCmd, SendChatCmd, CastSpellCmd, …). Lives in AcDream.UI.Abstractions. Does not change across the backend swap.
  • Game state + events + net: existing IGameState, IEvents, WorldSession, etc. Unchanged. UI only reads/subscribes; never touches these.

Module layout (future)

  • src/AcDream.UI.Abstractions/IPanel, IPanelHost, IPanelRenderer, ICommandBus, all ViewModels + Commands. Backend- agnostic.
  • src/AcDream.UI.ImGui/ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui implementation of IPanelRenderer + ImGui bootstrap. Phase D.2a. ImGuiController (from the Silk.NET extension) handles GL backend + input event subscription; ImGuiBootstrapper is a thin IDisposable wrapper; ImGuiPanelRenderer wraps the widgets IPanelRenderer needs.
  • src/AcDream.UI.Retail/ (later) — custom retained-mode toolkit using dat assets, same IPanelRenderer contract. Phase D.2b.

Hard rules

  1. No panel references a backend namespace. If a panel imports ImGuiNET / Silk.NET.OpenGL.Extensions.ImGui or a custom-toolkit widget class directly, it's a bug — extend IPanelRenderer instead.
  2. Plugin API targets the abstraction layer only. Plugins define IPanel instances; they never see which backend draws them.
  3. Features that only ImGui can express → not in IPanelRenderer. If a feature can't be expressed by a future retained-mode toolkit with dat-sourced sprites/fonts, don't expose it in the abstraction.
  4. D.3 AcFont / D.4 dat sprites / D.7 cursor manager are D.2b dependencies — they plug into the custom backend only.
  5. D.5 core panels / D.6 HUD ship against the abstraction, not against a specific backend. They render via ImGui first, reskin under custom later.

Why staged (not pure custom from day one)

  • Gets the whole game-logic loop validated in weeks: chat-send, inventory-click, vitals-bar-read-from-real-state. Requires zero widget art.
  • Forces the abstraction design to pass a real backend test before we commit to the custom toolkit. If IPanelRenderer is wrong, we discover it during Phase D.2a, not after Phase D.2b lands with a dozen broken panels.
  • Custom retail-look toolkit then delivers the aesthetic layer without the plugin API or game logic needing to change.
  • Full design: docs/plans/2026-04-24-ui-framework.md
  • Roadmap entry: docs/plans/2026-04-11-roadmap.md Phase D (see the 2026-04-24 callout block).
  • Custom-backend research (applies to D.2b, NOT D.2a): docs/research/retail-ui/00-master-synthesis.md + slices 01-06.

2026-07-10 retained radar update

The older "D.6 ships through IPanelRenderer/ImGui first" rule above is superseded for screen-space retail HUD windows. The production retained toolkit now lives in src/AcDream.App/UI; imported LayoutDesc windows bind through dedicated gm*UI controllers and App-layer snapshot providers. ImGui remains devtools only.

gmRadarUI is the reference pattern: Core owns exact colors/shapes/projection/ compass/coordinates, RadarSnapshotProvider joins the existing WorldEntity + ClientObjectTable two-table model, RadarController binds LayoutDesc 0x21000074, and UiRadar only draws/handles retained interaction. GameWindow contains mount and delegate wiring, not the feature body. See docs/research/2026-07-10-retail-radar-pseudocode.md.

2026-07-10 retained window ownership

Every production retained window is registered through RetailWindowManager and owned by a typed RetailWindowHandle. Controllers implement IRetainedPanelController; windows with multiple focused controllers use RetainedPanelControllerGroup, which forwards lifecycle in construction order and disposes in reverse order exactly once. A controller created after a hidden mount (inventory) is attached through RetailWindowManager.AttachController.

Controller event subscriptions must use removable named handlers or paired subscribe/unsubscribe delegates. UiHost.Dispose first removes Silk mouse/keyboard subscriptions, then disposes the manager/controllers, then the GL text renderer. GameWindow.OnClosing disposes layout persistence and UiHost before live session or game-state sources. This ordering is the recreate/relogin double-subscription guard; do not reintroduce anonymous lifetime-bound event lambdas.

2026-07-11 composition boundary

RetailUiRuntime now owns production retained composition. Its focused binding records receive state/action delegates plus DAT/font/sprite resolvers; GameWindow.OnLoad retains only resolver/service creation and one declarative RetailUiRuntime.Mount call. All LayoutDesc imports, controller construction, window mounts, toolbar digit and inventory-cell asset lookup, plugin mounts, cursor feedback, persistence, automation, and retained tick/draw/restore/dispose paths live behind the runtime. The live Debug gate passed across two graceful restart cycles. Do not add panel mount bodies back to GameWindow.