acdream/memory/project_ui_architecture.md
Erik 0f2d98c501 fix(combat): synchronize auto-target presentation
Make the retained toolbar consume retail's payload-free selection notice from canonical live state so a reentrant Auto Target replacement cannot be erased by the outer clear. Restrict automatic acquisition to the requested hostile non-player monster policy and record the intentional PK edge divergence.

Co-Authored-By: Codex <codex@openai.com>
2026-07-12 20:26:00 +02:00

12 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.

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.