8.7 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.ImGuifor 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 theACDREAM_DEVTOOLS=1overlay. (Pivoted fromHexa.NET.ImGuion 2026-04-25 — Hexa's native OpenGL3 backend resolves GL via GLFW/SDL internally and crashed0xC0000005against 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 inAcDream.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.ImGuiimplementation ofIPanelRenderer+ ImGui bootstrap. Phase D.2a.ImGuiController(from the Silk.NET extension) handles GL backend + input event subscription;ImGuiBootstrapperis a thin IDisposable wrapper;ImGuiPanelRendererwraps the widgetsIPanelRendererneeds.src/AcDream.UI.Retail/(later) — custom retained-mode toolkit using dat assets, sameIPanelRenderercontract. Phase D.2b.
Hard rules
- No panel references a backend namespace. If a panel imports
ImGuiNET/Silk.NET.OpenGL.Extensions.ImGuior a custom-toolkit widget class directly, it's a bug — extendIPanelRendererinstead. - Plugin API targets the abstraction layer only. Plugins define
IPanelinstances; they never see which backend draws them. - 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. - D.3 AcFont / D.4 dat sprites / D.7 cursor manager are D.2b dependencies — they plug into the custom backend only.
- 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
IPanelRendereris 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.
Links
- Full design:
docs/plans/2026-04-24-ui-framework.md - Roadmap entry:
docs/plans/2026-04-11-roadmap.mdPhase 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.
2026-07-11 selection and interaction ownership
AcDream.Core.Selection.SelectionState is the only selected-object truth. It
ports retail ACCWeenieObject::SetSelectedObject: deduplicate the current guid,
remember previous and previous-valid guids, commit before firing one typed
transition, and clear with the removed object retained as previous. World pick,
radar, combat targeting, inventory, paperdoll slots, toolbar selected-object
presentation, target indicator, use/pickup, PreviousSelection, and plugins all
read or write this same owner. Plugins use IPluginHost.Selection; plugin event
failures are isolated from the client and other plugins.
AcDream.App.UI.InteractionState separately owns temporary pointer orchestration:
None, Use, Examine, or UseItemOnTarget(sourceGuid). Item target mode is a
projection of this state, not a second selected-object field. Keep selection in
Core and interaction modes in App when completing Wave 3.
The 2026-07-11 Release gate passed for world/radar/inventory/paperdoll selection synchronization, toolbar/target/radar presentation, PreviousSelection, and the movement/door/armor/window regression tour.
2026-07-11 item-interaction policy boundary
AcDream.Core.Items.ItemInteractionPolicy owns the pure, ordered retail rules
from DetermineUseResult, UseObject, target compatibility, and
AttemptPlaceIn3D. Do not put vendor/trade/confirmation/stack/ground legality
back into widgets or GameWindow. ItemInteractionController snapshots mutable
objects, applies the 200 ms pre-legality throttle, balances busy state against
Core.Net UseDone, manages target mode, and dispatches typed actions. PWD flags
and CombatUse are retained from CreateObject; IsComponentPack is resolved
against portal.dat's SpellComponentTable.
Matching-executable x86 pins the two operands lost by Binary Ninja:
DetermineUseResult @ 0x005884B6 tests BF_REQUIRES_PACKSLOT (0x00800000), and
AttemptPlaceIn3D @ 0x00588752 tests BF_VENDOR (0x00000200). Preserve the
retail asymmetry that UseObject early-classifies results 2..7 but excludes 8,
and preserve placement's action list separately from its boolean return (vendor
sale and secure-trade attempts act while returning false).