273 lines
15 KiB
Markdown
273 lines
15 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.
|
||
|
||
Retail `CM_UI::SendNotice_SelectionChanged @ 0x00479F50` carries no object-id
|
||
payload. Notice consumers such as `gmToolbarUI::HandleSelectionChanged` read the
|
||
current global selection when invoked. Preserve that rule in retained UI: a
|
||
handler registered earlier can retarget reentrantly (Auto Target on a death
|
||
clear), so applying the captured outer transition afterward produces stale UI
|
||
even though `SelectionState` is correct. Selected-object presentation must read
|
||
`SelectionState.SelectedObjectId` at notification time. Issue #205, 2026-07-12.
|
||
|
||
`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.
|
||
|
||
## 2026-07-21 selection extraction and inventory transaction ownership
|
||
|
||
`WorldSelectionQuery` is the read-only App boundary over canonical live state,
|
||
the completed render-selection frame, object metadata, camera/cursor inputs,
|
||
DAT selection bounds, hostile lookup, and Use/PickUp range classification. It
|
||
never mutates `SelectionState`, starts movement, or sends packets.
|
||
|
||
`SelectionInteractionController` is the corresponding intent owner. It owns
|
||
world-click routing, closest/previous selection, the outbound frame-boundary
|
||
queue, Use/PickUp transport, and MoveTo deferral. Queued work captures both the
|
||
live local entity ID and exact `ClientObject` reference where available;
|
||
deferred movement uses cancel-before-arm ordering and a queue epoch prevents
|
||
pre-reset work from crossing session lifetime.
|
||
|
||
`ItemInteractionController` owns the one global inventory transaction, shaped
|
||
after retail `ACCWeenieObject::prevRequest`. Inventory, external-container,
|
||
toolbar, paperdoll, give, merge, split, drop, and deferred keyboard pickup all
|
||
acquire the same owner. A destination waiting projection carries an exact token
|
||
and item identity. Keyboard pickup may remain reserved-but-undispatched while
|
||
MoveTo runs, then promotes that exact token only when the wire request is sent.
|
||
|
||
`ClientObjectMoveOrigin` distinguishes immediate optimistic repaint, server
|
||
rollback repaint, and authoritative response. Only authoritative move/failure/
|
||
remove/stack responses complete the transaction. Completion clears the global
|
||
owner and local waiting projection atomically, publishes the old token-specific
|
||
projection resolution, then advertises readiness. This order is essential:
|
||
reentrant observers may start the next request without the old response tail
|
||
rejecting, hiding, or consuming it. Do not regress to GUID-only cleanup or let
|
||
ordinary `ObjectUpdated` events complete inventory work.
|
||
|
||
`GameWindow` now constructs and forwards narrow lifecycle edges to these
|
||
owners; no selection algorithm or interaction request body belongs there.
|
||
|
||
## 2026-07-27 M4 world-interaction checkpoint
|
||
|
||
The six-slice world-interaction plan lives at
|
||
`docs/plans/2026-07-23-world-interaction-completion.md`. Slices 1–3 are
|
||
user-accepted:
|
||
|
||
- the favorite-spell bar owns its authored overflow controls and stable manual
|
||
offset;
|
||
- status Use/Assess shares canonical interaction and appraisal owners;
|
||
- `gmFloatyExaminationUI` is an independent top-level window, not a shared
|
||
primary-panel page;
|
||
- object assessment owns exact response flags, balanced busy lifetime,
|
||
selection-follow, inscription, complete EoR item/creature/spell reports, and
|
||
the animated private creature preview;
|
||
- retained items and favorite spells select on button-down; right-click uses
|
||
the correct object-appraisal or local spell-report path;
|
||
- component-disabled ACE profiles show the accepted scarab/prismatic formula,
|
||
DAT icon DIDs, authored foreground stack, and 310 x 400 window extent.
|
||
|
||
Resume at Slice 4. It must add equipped-child world picking through the
|
||
existing completed render-selection frame and exact child presentation key;
|
||
do not invent a second selection state or marker anchor. Slices 5–6 then add
|
||
vendor browsing and server-authoritative transactions through the same Runtime
|
||
inventory/action owners.
|