acdream/memory/project_ui_architecture.md
Erik 05f6222865 fix(ui): complete live targeted healing flow
Route ACE server commands through the existing chat path, bind the retail paperdoll hit mask instead of its obscured viewport, and prefer authoritative private health vitals. Record the user-confirmed live gate and pin the production DAT widget type in tests.
2026-07-11 07:58:59 +02:00

207 lines
12 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).
The 2026-07-11 non-debug Release gate passed ordinary use/container activation,
equip synchronization, grounded world drop, and targeted healing. ACE `/ci 632`
provisioned a peerless kit and `/harmself` created a real health deficit; main-pack
and paperdoll self-targets both healed successfully.
## 2026-07-11 primary-click ownership
Every primary click that can name an object must first call
`ItemInteractionController.OfferPrimaryClick`. Its typed result is the contract:
`NotActive` permits the surface's ordinary select/open/use fallback;
`ConsumedSuccess` and `ConsumedRejected` are both terminal. Never let a rejected
target click fall through. Inventory cells and bags, main pack, paperdoll slots
and body, toolbar, radar, and world picking share this router; widgets continue
to expose callbacks without importing controllers. The router retains a consumed
target through the toolkit's 500 ms double-click window; otherwise UiRoot's
second Click plus DoubleClick could run ordinary activation after the first click
cleared target mode.
The 2026-07-11 non-debug Release fallback gate passed for inventory,
paperdoll, toolbar, radar, and world primary clicks with target mode inactive,
and the active healing-kit target flow passed through the main pack and 3-D doll.
The production DAT resolves `0x100001D6`, the full-height paperdoll hit/drag mask,
as a `UiButton` over `UiViewport 0x100001D5`. Bind target behavior to the mask,
not only the obscured viewport, and hide/show both together with the Slots toggle.
For local vitals, `PrivateUpdateVital`/`PrivateUpdateVitalCurrent` in
`LocalPlayerState` are authoritative. `VitalsVM.HealthPercent` must prefer that
private health snapshot over the world-facing `CombatState` percentage, using
the latter only before private state arrives. ACE `/harmself` exposed the stale
owner because it sends private vital deltas without changing that combat cache.
## ACE command forwarding
`ChatCommandRouter` is the single submit path for retained and developer chat.
Known client/chat verbs resolve locally; unknown letter verbs are server
commands. They are always published as `ChatChannelKind.Say`, regardless of the
selected channel, because commands must ride `GameAction Talk (0x0015)`.
`ChatInputParser` preserves `@verb` and canonicalizes `/verb` to `@verb` on the
wire: ACE's `GameActionTalk` checks only the at-sign form before invoking its
`CommandManager`. Do not add a parallel admin-command transport.
For live test-item provisioning, ACE `@ci` accepts a weenie class id/classname
plus optional stack size, palette, and shade. Healing kit WCIDs are 628 crude,
629 plain, 630 good, 631 excellent, and 632 peerless. The `+Acdream` developer
character can therefore create the best kit with either `/ci 632` or `@ci 632`;
both forms must produce canonical `@ci 632` Talk text.