acdream/memory/project_ui_architecture.md

160 lines
8.7 KiB
Markdown

# 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`](../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.
## Links
- 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`.
## 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).