acdream/docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md
Erik 2644d1d527 fix(ui): port exact selected health policy
Replace the toolbar's PWD-bit approximation with retail ObjectIsAttackable composed with the exact player and pet short-circuits. Carry second-header PetOwner through CreateObject, session, and ClientObject state so self, pet, and Free-PK cases match retail while friendly NPCs and attackable non-creatures remain name-only.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 11:42:04 +02:00

47 KiB

D.2b / D.5 Retail UI Fidelity Completion Implementation Plan

Execution model: implement one wave at a time. Every behavioral slice starts from named-retail/DAT evidence, lands with automated conformance, and stops at its own live visual gate. Do not combine waves into a big-bang rewrite.

Goal: Bring the existing in-world retained GUI to retail behavioral parity, close every confirmed audit gap, and establish an architecture that future retail panels can extend without adding feature bodies to GameWindow.cs or duplicating window/widget policy.

Scope: the retained UiRoot tree, LayoutDesc importer, widget semantics, window management, vitals, chat, toolbar/selected object, inventory, paperdoll, character sheet, cursors, radar social markers, and the missing in-world retail panels/HUD named in the current D.5/D.6 roadmap. Login, character selection, and character creation remain a separate frontend milestone.

Retail oracle: docs/research/named-retail/acclient_2013_pseudo_c.txt, docs/research/named-retail/acclient.h, production LayoutDesc/Font/RenderSurface DAT objects, and user-confirmed retail/live screenshots. Ghidra is fallback only when the named decomp lacks a body.


1. Non-negotiable architecture and code-style rules

  1. Modern code, retail behavior. Port retail control flow, constants, state precedence, and boundary conditions. Modernize ownership and representation, not observable behavior.
  2. No new feature bodies in GameWindow.cs. It may construct typed binding records and mount one RetailUiRuntime. Panel logic belongs in controllers; pure AC rules belong in Core; GL-only viewport/cursor presentation remains in App/Rendering.
  3. One owner per state. Selection, target-use mode, shortcuts, open-container state, window lifecycle, and pending optimistic actions each get one owner and one event stream. Widgets display state; they do not create competing truth.
  4. Generic policy stays generic. Text roles, button states, layout reflow, focus, timers, window lifecycle, and persistence are widget/window services. Never patch them separately in ChatWindowController, InventoryController, or another panel.
  5. Bind existing DAT nodes. Controllers use required/optional role binding by element id. They do not synthesize a second data-bearing widget when the DAT already contains the retail node. Runtime-created nodes are allowed only when retail creates them dynamically and the code cites that method.
  6. Typed behavior, not stringly behavior. State names remain DAT data, but C# policy uses typed capabilities/records (independent text Editable and Selectable flags, button phase, cursor role, shortcut kind, window event). Controllers must not implement precedence by scattered state-name string lists.
  7. Controllers have lifetimes. Every subscribing controller implements IDisposable; subscriptions use removable named handlers or a small SubscriptionBag. RetailUiRuntime.Dispose tears down the complete tree.
  8. Required bindings fail loudly. Add a LayoutBinding helper with Require<T>(id, role) and Optional<T>(id). Missing required roles disable the affected window with one diagnostic instead of silently producing a partial UI.
  9. No raw game-state writes. All mutations go through Core-owned eventful APIs and, where retail predicts locally, a pending/confirm/rollback ledger.
  10. Every divergence is auditable. Add, amend, retire, or renumber the divergence row in the same commit as the behavior change.

Layer ownership

Layer Owns
AcDream.Core Selection state, shortcut model/plans, item-use and wield legality, social/title state, pure retail math/state machines
AcDream.Core.Net Wire parsers/builders, sequence handling, authoritative event delivery
AcDream.UI.Abstractions Backend-neutral VMs, commands, settings DTOs/persistence contracts
AcDream.App/UI Retained widgets, layout/window runtime, panel controllers, snapshot adapters
AcDream.App/Rendering DAT texture/font/cursor resolution and paperdoll/world-HUD GL rendering
GameWindow Construction, dependency delegates, render/tick calls, disposal only

2. Target runtime shape

Add a RetailUiRuntime composition owner under AcDream.App/UI. It receives the UiHost, DAT resolvers, and small per-subsystem binding records such as VitalsRuntimeBindings, ChatRuntimeBindings, and InventoryRuntimeBindings. It owns:

  • RetailWindowManager -- named handles, show/hide/raise, global lock, lifecycle events, clamping, and persistence;
  • all panel controllers and their disposal;
  • the retained input/cursor action router;
  • window mount factories which all use RetailWindowFrame;
  • the only App-layer subscriptions from game state into retained UI.

RetailWindowHandle contains the outer frame, imported content root, controller, and typed lifecycle methods. Controllers resize/move the handle's outer frame, never an arbitrary imported child. UiRoot continues to own hit testing, capture, focus, and drawing; it does not read settings or send game actions.

The importer carries a typed UiPropertyBag which distinguishes an absent property from an explicit false or 0, merges base properties before derived properties, and preserves numeric state ids alongside names/media. No widget or shared runtime contains element-id exceptions; those stay in window descriptors and controllers.

Complex panels use immutable snapshots at their draw seam, following the radar pattern. Simple widgets may use scalar providers. A controller must not rebuild a large domain model through dozens of unrelated per-frame closures.


3. Wave 0 -- oracle, fixtures, and ledger repair

This wave changes no runtime behavior.

0.1 Consolidate missing retail pseudocode

  • Write focused pseudocode notes for:
    • UIElement_Text interaction/property modes;
    • UIElement_Button::UpdateState_, mouse transitions, enabled/toggle behavior;
    • UIElement::UpdateForParentSizeChange raw edge modes 0-4;
    • UI manager tooltip deadlines, global time delivery, and focus notifications;
    • gmToolbarUI global messages, drop branches, mana/stack behavior;
    • ItemHolder::UseObject / AttemptPlaceIn3D decision trees;
    • chat maximize, alpha, tabs, talk-focus and squelch;
    • paperdoll priority, Aetheria, hit testing and race updates;
    • character titles, luminance, and raise acknowledgement.
  • Correct the roadmap premise around shortcuts: ShortCutData is index_, objectID_, spellID_, while PlayerModule::favorite_spells_[8] is a separate subsystem. This retail toolbar restores only nonzero objectID_; it must not grow invented spell-slot behavior.
  • Pin every method address and DAT property id before implementation.

0.2 Expand committed DAT conformance fixtures

  • Add skip-by-default generators and committed ElementInfo JSON fixtures for toolbar 0x21000016, inventory 0x21000023 plus mounted sublayouts, paperdoll 0x21000024, and character 0x2100002E.
  • Include state media, cursor media, font ids, raw edge modes, default state, min/max attributes, numeric state ids, and runtime-relevant scalar/property arrays.
  • Add inheritance cases proving derived explicit false and 0 override base values rather than being mistaken for absence.
  • Add fixture assertions for every required controller role.

0.3 Repair the audit trail

  • Retire stale AP-38 (chat wrapping) and record UI issue #198 as closed by 769ebef3 (the original #162 collided with a later movement issue).
  • Rewrite AP-37, AP-40, IA-15, and AP-66 to match current code.
  • Renumber the overwritten paperdoll AP-67/AP-68 rows and duplicate AP ids.
  • Add rows/issues for every confirmed unregistered gap from this plan.
  • Mark each row with its retirement wave below.

Gate: docs/fixtures review, App tests green, no production-code diff.


4. Wave 1 -- exact retained-widget foundation

1.1 Give Type-12 text independent retail capabilities

  • Default factory-imported DAT text to display-only behavior. Model Editable 0x16 and Selectable 0x27 as independent capabilities, matching retail; do not collapse them into an exclusive interaction-mode enum.
  • Import retail Editable 0x16, OneLine 0x20, and Selectable 0x27 properties through the typed property bag where the DAT supplies them.
  • Make focus, key capture, selection, clipboard, and pointer-drag behavior derive exclusively from those capabilities.
  • Set the chat transcript to Selectable; keep the chat input explicitly Editable; all labels remain Display.
  • Bind the imported vitals labels 0x100000EB/ED/EF directly and delete the synthesized duplicates.
  • Add tests proving display labels cannot steal focus, capture, or window drag.

Oracle: UIElement_Text::OnSetAttribute @ 0x0046A640, mouse/key methods 0x00469370..0x0046A4A0, gmVitalsUI::PostInit @ 0x004BFCE0.

1.2 Port the button state machine

  • Extract a GL-free UiButtonStateMachine over enabled/hovered/pressed/ captured/toggled state and available DAT states.
  • Port retail transition precedence exactly; controller code supplies semantic click/toggle callbacks only.
  • Preserve retail state ids 1/2/3, selected 6/7/8, and ghosted 13; if the requested state is absent, retain the current custom semantic state rather than overwriting states such as Maximized, Minimized, or radar lock.
  • Make cursor media follow the active button state.
  • Cover normal, rollover, pressed, released-outside, disabled, toggle-on/off, and capture-loss tests using real fixture state names.

Oracle: UIElement_Button::UpdateState_ @ 0x00471CF0, MouseDown @ 0x00471FF0, MouseUp @ 0x004720E0, MouseOverTop @ 0x004721F0.

1.3 Replace lossy anchors with the retail layout policy

  • Introduce UiLayoutPolicy containing the five raw edge modes 0..4, original child rect, and original parent rect. Preserve raw modes through ElementReader.
  • Port UpdateForParentSizeChange as a pure function, including right-track, center mode 3, proportional mode 4, and inclusive-pixel behavior for all four edges.
  • Keep AnchorEdges only as a compatibility adapter for programmatic widgets; imported DAT elements use UiLayoutPolicy directly.
  • Test every raw mode against hand-translated decomp vectors at 800x600, widescreen, parent grow/shrink, and nested layouts.

Oracle: UIElement::UpdateForParentSizeChange @ 0x00462640.

1.4 Finish UI-manager timing/focus behavior

  • Poll the hovered element's tooltip deadline in UiRoot.Tick, then broadcast retail global time message 3 to listeners in deterministic tree order.
  • Tear down focus, capture, modal ownership, hover, drag, default input, and window registration before a removed subtree can receive another time/input pulse.
  • Add typed focus/capture transition behavior; eliminate polling where retail reacts to a transition.
  • Pin tooltip-before-global-time, capture-loss, modal, and subtree-removal ordering tests. Keep double-click coverage with the existing input suite.

1.5 Add a narrow DAT named-state bridge

  • Bind imported named states such as ShowDetail, HideDetail, enabled/ disabled button roles, and empty-cell art without globally re-registering Type 3 elements or teaching controllers how the importer stores states.
  • Prove state changes preserve the imported rect, anchors, dimensions, and media using production LayoutDesc fixtures.

Gate: vitals/chat/radar/toolbar UI Studio fixtures unchanged visually; buttons show retail hover/press; vitals bars drag the window instead of selecting invisible text; full suite green; user visual gate.


5. Wave 2 -- retained window runtime, lifecycle, and persistence

2.1 Introduce RetailWindowManager and typed handles

  • Add RetailWindowHandle and lifecycle events: Shown, Hidden, Moved, Resized, Closed, LockChanged.
  • Move named-window registry/raise policy behind RetailWindowManager; retain thin UiHost forwarding APIs for callers.
  • Define IRetainedPanelController : IDisposable with idempotent OnShown, OnHidden, and optional descendant-focus notification, without coupling UiRoot to game/network semantics.
  • Make UiRoot emit visibility, descendant-focus, and pointer-capture changes; the handle owns outer-frame geometry/opacity and the controller owns panel-specific reactions.
  • On hide/unregister, clear descendant focus, capture, modal ownership, drag state, and default text input. Remove the max/min button from any shared close-id convention; each controller binds close and maximize explicitly.

2.2 Use one mount path for every production window

  • Extend RetailWindowFrame.Mount to return a handle and accept exact resize, chrome, opacity, visibility and constraint policy.
  • Support both imported roots that already own retail chrome and shared nine-slice wrappers, with independent X/Y resize policy, so migrations cannot double-frame or double-inset content.
  • Migrate vitals, chat, toolbar, inventory, radar, character, and Studio one at a time. Delete all inline UiNineSlicePanel mount recipes from GameWindow and MockupDesktop.
  • Make chat maximize operate on its handle's outer frame and import retail min/max attributes and button states; remove hardcoded 320f.
  • Correct chat scrollbar art from the actual inherited DAT roles, not literals.

Acceptance (2026-07-10): full suite green (4,538 passed, 5 skipped); live Debug gate passed for shared chrome, resize/collapse, lock/raise, chat maximize, scrollbar buttons, track/thumb dragging, wheel scrolling, and bottom-pin behavior.

2.3 Persist the complete UI layout

  • Replace radar-only persistence with UiWindowLayout containing position, size, visibility, collapsed/maximized state, and any retail-persisted panel state.
  • Key layouts by character and resolution, matching retail saveui/loadui semantics; add migration from existing radar entries.
  • Restore only after the character context and screen size are known; clamp safely on resolution changes.
  • Save move/resize on interaction completion, visibility on transition, and never let the temporary default login key overwrite character-specific state.
  • Apply global LockUI through the window manager to every window.

Acceptance (2026-07-10): schema-v2 per-character/resolution restore passed a live Debug restart with every retained window persisted. The gate also caught and closed hidden-inventory baseline capture (expanded main-pack rows) and chat's clamped maximize jump (lower edge now remains fixed). Full suite green: 4,545 passed, 5 skipped.

2.4 Enforce controller teardown

  • Apply the same IRetainedPanelController : IDisposable contract; allow plain IDisposable only for controllers that genuinely have no visibility/focus reaction.
  • Convert inline event lambdas to removable handlers/subscription tokens.
  • Dispose controllers on runtime shutdown/rebind and test that relog/recreate does not double-subscribe.
  • Dispose controller subscriptions before session/data sources and before the UI host; make every disposal path idempotent.

Acceptance (2026-07-10): every subscribing retained controller is owned by its typed window handle (toolbar/inventory use reverse-order controller groups), late attachment is lifecycle-safe, Silk input handlers are removable, and shutdown disposes the retained runtime before session sources. Recreate/disposal regression tests pass; full suite green: 4,549 passed, 5 skipped.

2.5 Extract the composition block

  • Create RetailUiRuntime and focused binding records.
  • Move DAT import, window creation, controller construction, registry, and persistence wiring out of GameWindow.OnLoad.
  • Keep GL resolver construction and delegate supply in GameWindow; target a short, declarative mount call and one tick/draw/dispose path.

Acceptance (2026-07-11): the Debug live-world gate passed after two graceful restart cycles. Vitals, radar, chat, toolbar, character, inventory/paperdoll, saved layouts, cursor/input, and window controls remained responsive with no double dispatch. GameWindow now supplies one typed mount graph and delegates retained tick/draw/restore/dispose to RetailUiRuntime. Full suite green: 4,549 passed, 5 skipped.

Gate: move/resize/toggle every window, restart at two resolutions, switch characters, verify exact restore and global lock; chat maximize/minimize must resize chrome and content together; full suite and user visual gate.


6. Wave 3 -- single-owner selection, item interaction, and cursors

3.1 Centralize selection

  • Add Core SelectionState with selected guid, previous guid, reason/source, and one change event.
  • Route world pick, radar click, inventory selection, paperdoll selection, toolbar selected strip, target indicator, examine/use, and plugins through it.
  • Delete panel-local competing selection state except transient hover/row focus.
  • Add one App interaction-state owner for None, Use, Examine, and UseItemOnTarget(sourceGuid); Core selection remains session truth while target mode remains UI orchestration state.

Implementation (2026-07-11): Core SelectionState ports ACCWeenieObject::SetSelectedObject @ 0x0058C2E0, including deduplication, previous/previous-valid tracking, object-removal clear, and plugin callbacks. World pick, radar, combat targeting, inventory, paperdoll slots, the selected toolbar strip, target indicator, use/pickup, PreviousSelection, and the new IPluginHost.Selection contract share that owner. InteractionState separately owns None/Use/Examine/UseItemOnTarget; ItemInteractionController now projects its pending source from that state. AP-58 retired.

Acceptance (2026-07-11): Release live-world gate passed. World clicks, radar clicks, inventory items, and equipped paperdoll items stayed synchronized across toolbar name, target brackets, radar highlight, and selection squares; PreviousSelection restored the prior target. Movement, doors, armor removal, and retained window controls remained unregressed. Full suite green: 4,560 passed, 5 skipped.

3.2 Port item-use/drop legality into Core

  • Translate ItemHolder::UseObject, DetermineUseResult, and AttemptPlaceIn3D to pure/testable Core policy records.
  • Include ready/busy, ownership, vendor/trade, PK/rare confirmation, stack, ground/drop, and target-type gates.
  • Keep App ItemInteractionController as orchestration: ask policy, show modal/ toast, send the selected wire action, and manage pending state.

Implementation (2026-07-11): Core ItemInteractionPolicy is the pure port of all three named-retail decision trees and returns ordered typed actions (plus the deliberately independent placement return value). Matching-executable x86 resolved the two damaged masks as BF_REQUIRES_PACKSLOT (0x00800000) and BF_VENDOR (0x00000200). CreateObject now retains PWD flags and CombatUse; the App snapshot resolves component packs from portal.dat. The controller owns the 200 ms pre-legality throttle, ready/busy lifecycle (released by UseDone), target state, policy action dispatch, confirmation/auxiliary action seams, and ground-drop optimistic state. Vendor/trade/salvage/confirmation retained panels consume the typed action seam when those surfaces land; until then the policy refuses unsafe wire sends rather than bypassing their gates.

Acceptance (2026-07-11): Non-debug Release live-world gate passed for ordinary item/container activation, equip/inventory synchronization, grounded world drop, and targeted healing. ACE command forwarding provisioned WCID 632 with /ci 632; /harmself then proved the main-pack and paperdoll self-target paths plus authoritative health-bar drain/recovery. Full suite green at the final gate: 4,583 passed, 5 skipped.

3.3 Add one item primary-click chokepoint

  • Define a controller-level activation router: target-mode acquisition first, then window-specific fallback (select/open/use).
  • Return a typed result (NotActive, ConsumedSuccess, ConsumedRejected) so both valid and rejected target attempts are consumed and cannot accidentally fall through to selection, double-click use, or container opening.
  • Route toolbar, inventory grid, bags, paperdoll slots/body, main pack and world objects through it. Widgets expose events; they do not depend on game controllers.
  • Add end-to-end tests for health-kit target use on self, inventory item, worn item, and world entity, including invalid-target behavior.

Implementation (2026-07-11): ItemInteractionController.OfferPrimaryClick is the sole typed target-mode offer. ConsumedRejected is intentionally terminal: invalid clicks clear retail target mode but never fall through to selection, container opening, or ordinary activation. Inventory contents/bags/main pack, paperdoll equipment/body, toolbar items, radar blips, and world picks all use the router; retained widgets remain controller-agnostic event sources. End-to-end retained tests pin self targeting and rejected inventory/worn targets without selection drift, while controller tests pin valid world/self and invalid target results. The consumed target is retained for retail's 500 ms double-click window, so the second Click/DoubleClick pair cannot reactivate or equip the rejected object after target mode clears. Issue #197 and AP-107 are retired.

Acceptance (2026-07-11): User-confirmed the non-debug Release fallback matrix for inactive target mode across inventory, paperdoll, toolbar, radar, and world clicks, then the active targeted-heal flow through both the main pack and 3-D paperdoll. The production DAT resolves the authored doll hit mask 0x100001D6 as a UiButton; binding the viewport beneath it cannot receive a live click. The mask now owns self-target clicks and hides with the viewport in slot view. Full suite green at the final gate: 4,583 passed, 5 skipped.

3.4 Complete retail cursor precedence and catalog

  • Port every reachable branch of ClientUISystem::UpdateCursorState, not only target enums 0x27/28/29.
  • Separate global world-interaction cursors from widget-local cursor media; preserve retail's event order: a global state change reasserts the default, while a later captured/hovered widget cursor transition may override it.
  • Cover default/combat/use/examine/busy/target states with golden EnumIDMap tests, and cover drag accept/reject, resize/move/text as widget-local semantic states without inventing global enum meanings for unreachable 0x07..0x09.
  • Keep OS cursor fallback only as an explicit registered adaptation with a visible diagnostic.

Implementation (2026-07-11): RetailGlobalCursorKind now models all 15 reachable enum branches from ClientUISystem::UpdateCursorState @ 0x00564630. Production DAT fixtures pin 0x01..0x06, 0x0A..0x0F, and 0x27..0x29 to their exact RenderSurface DIDs. CursorFeedbackController owns the pure global decision tree and reports only the active captured/hovered widget cursor; RetailCursorManager applies global-change then widget-change transitions in the same default/local layering used by UIElementManager::CheckCursor @ 0x0045ABF0. The old semantic-state alias search (TargetValid borrowing Drag_rollover_accept, etc.) is removed. AP-100 is retired; AP-72 remains the single explicit DAT-failure adaptation and now emits one visible diagnostic per failed global kind.

Gate: live use/drop/target matrix with retail cursor art and selection updates; no click path bypasses the router; full suite and user visual gate.

Acceptance (2026-07-11): User-confirmed the complete non-debug Release cursor matrix. A follow-up gate exposed that the four stance indicators were display-only; wiring their exact retail click branch passed immediately. Mouse click and quick key now both toggle peace/combat, update the visible indicator, and drive the corresponding DAT cursor state. Final suite: 4,618 passed, 5 skipped.


7. Wave 4 -- toolbar and selected-object completion

4.1 Wire retail quick-slot input

  • Add a focused retained-UI input action controller which maps UseQuickSlot_1..9, UseQuickSlot_14..18, and CreateShortcut to ToolbarController methods.
  • Port use-vs-select semantics from gmToolbarUI::ListenToGlobalMessage.
  • Keep GameWindow.OnInputAction to one delegation seam.

4.2 Preserve the exact shortcut model; keep the toolbar object-only

  • Model the 18 raw ShortCutData entries exactly as signed index_, unsigned objectID_, and unsigned spellID_; verify packing against the retail packer and protocol captures before changing the existing builder.
  • Preserve all three fields through parse, session storage, mutation, and wire serialization even though gmToolbarUI::UpdateFromPlayerDesc @ 0x004BF810 restores only entries with a nonzero objectID_.
  • Keep the main toolbar item/object-only. Feed PlayerModule::favorite_spells_[8] and the already parsed HotbarSpells into the separate spell-bar subsystem in Wave 10.

Wave 4.2 implementation (2026-07-11): Core-owned ShortcutEntry now models the exact signed index, object id, and raw 32-bit spell word. PlayerDescription parsing, the 18-slot nullable store, retained-cell drag snapshots, displaced-item reindexing, runtime delegates, WorldSession, and AddShortcut serialization carry the same lossless value. The visible toolbar continues to bind only entries with nonzero object ids. AP-103 is retired; automated conformance is green and the live relog/persistence gate passed: ACE restored the mutated shortcut in the same slot after a clean reconnect.

4.3 Port every drag/drop branch

  • Add a pure ShortcutDropPlanner keyed by source kind: toolbar reorder, inventory fresh add, occupied target, inventory-button target, off-bar removal, and full-stack merge replacement.
  • For an inventory-to-occupied-slot drop, place the displaced shortcut at retail's first-empty-to-the-right slot, never at the inventory grid index.
  • Validate the complete mutation before changing state, apply it atomically, and emit exact Remove/Add wire ordering. Use pending/confirm/rollback only where the protocol actually exposes rejection; never leave a partially mutated bar.
  • On full-stack merge, rekey the matching object shortcut and preserve the non-object fields of its raw ShortCutData entry.

Wave 4.3 partial implementation (2026-07-11): drag-over feedback is now an explicit neutral/accept/reject result instead of a Boolean. Inventory and paperdoll enforce retail's (DropItemFlags & 0xE) == 0 physical-item gate, so a toolbar alias released on either surface only leaves its remove-on-lift shortcut removal in place; it cannot move, unwield, or wield the underlying object. The raw-field preservation landed in Wave 4.2. Live gate passed with an equipped helmet: dropping its toolbar alias onto inventory removed only the shortcut and left the helmet worn.

Wave 4.3b implementation (2026-07-11): a pure Core ShortcutDropPlanner now plans the complete post-lift mutation before state changes. Fresh inventory drops use the exact cyclic-right search; aliases use only the vacated toolbar source; duplicate-object removal, wraparound, a full bar, visually empty non-object records, exact wire order, and raw full-stack rekeying have conformance fixtures. ToolbarController applies the validated local transaction atomically, then emits its ordered Remove/Add events. AP-102 is retired; 4,646 tests pass. The live inventory-to-occupied displacement plus toolbar-reorder gate passed 2026-07-11. Wave 4.3c completes the umbrella: retained buttons can own item-drop callbacks; the toolbar inventory button uses retail's fresh-item gate and exact green-arrow overlay, while aliases stay neutral. Inventory now tries the pure named-retail stack-merge legality/amount planner before ordinary insertion, sends 0x0054, broadcasts FullMergingItem immediately from the attempt owner, rekeys the toolbar with existing-destination removal, and selects the target. The notice is not a server confirmation despite its name. Release build is warning-free and 4,660 tests pass. The compatible-stack live gate passed 2026-07-11 and exposed the expected Wave 4.4/AP-101 selected-stack gap. Wave 4.4a now implements that gap: exact count-first name formatting, authored entry/slider media, one Core split-quantity owner, live stack-size refresh, and selected-source item-holder amounts. Controls/polish passed the live gate. Partial container/ground transfers now send retail 0x0055/0x0056; their live action gate remains pending. The inventory-button gate remains pending.

4.4 Finish toolbar controls

  • Bind all seven retail panel buttons by their DAT panel-id attribute plus combat, Use, and Examine actions; dispatch panel buttons through the window registry, ghosting unavailable panels rather than constructing placeholders.
  • Make open/closed states event-driven from RetailWindowManager.
  • Complete selected-object item mana and port RecvNotice_UpdateItemMana.
  • Complete formatted stack count, stack entry and split slider, the applicable RecvNotice_SplitStack activation behavior, and HandleSelectionChanged stack precedence. Vendor-specific initial quantity remains naturally dormant until a vendor panel owns an active vendor id.
  • Replace the PWD-only health gate with the exact attackable/player/pet policy.
  • Add item-mana query/cancel state and health query cancellation.
  • Add one shared split-quantity owner for the entry, slider, and selected-source item-holder merge, container-split, and ground-split payloads.

Wave 4.4a implementation (2026-07-11): static cdb against the matching retail binary pinned stack-name "%d %hs", numeric entry "%d", and the slider's exact 1000-step truncation formula. StackSplitQuantityState is the single Core owner; SelectedObjectController binds authored elements 0x100001A3/A4, and InventoryController consumes the value only when the dragged source is selected. The horizontal scrollbar remains the generic retained UiScrollbar in scalar mode, using the DAT track/thumb rather than panel-local drawing. The warning-free App Release build and 4,697-test suite are green. Gate polish preserves CreateObject PluralName through Core.Net into ClientObject.GetAppropriateName, honors the entry's authored right alignment, and lets the pointer-owned thumb remain at exact minimum/maximum endpoints instead of snapping back from the quantized amount. The controls/polish and partial-transfer live gates passed. ItemHolder::GetObjectSplitSize @ 0x00586F00 now scopes that quantity to the selected object for all transfers. Partial inventory moves send StackableSplitToContainer 0x0055; partial drag-out drops send StackableSplitTo3D 0x0056; neither path moves the original object optimistically because the authoritative destination stack receives a server-assigned guid. The partial inventory and ground transfers were live-confirmed 2026-07-11.

Wave 4.4b implementation (2026-07-11): named retail pins the complete selected-item mana exchange: CM_Item::Event_QueryItemMana @ 0x006A8610 sends 0x0263 + guid, DispatchUI_QueryItemManaResponse @ 0x006A84D0 reads guid/fraction/valid, and gmToolbarUI::RecvNotice_UpdateItemMana @ 0x004BD0C0 reveals the meter only for a valid response. ItemManaState owns the parsed response; SelectedObjectController queries only owned non-stack items, ignores stale-guid notices, cancels invalid/visible queries with guid zero, and applies the same visible-meter cancellation to health. Automated wire, state, and retained-controller conformance is green. The live gate passed 2026-07-11: an empty Mana Stone consumed a source magic item, destroyed that source, transferred its mana into armor, and the selected armor's authored mana bar updated from the authoritative 0x0264 response.

Wave 4.4c implementation (2026-07-11): the former PWD-only approximation is replaced by a pure Core port of ClientCombatSystem::ObjectIsAttackable @ 0x0056A600 composed exactly with toolbar IsPlayer || pet_owner. CreateObject now continues through the complete second header and preserves PetOwner after MaterialType/Cooldown fields, matching PublicWeenieDesc::UnPack @ 0x005AD7AC..0x005AD7F8 and ACE serialization. The policy restores self, pet, and Free-PK creature cases while retaining name-only friendly NPCs and rejecting attackable non-creatures. AP-46 is retired; automated parser/model/policy conformance is green and the live gate remains pending.

Wave 4.1 implementation (2026-07-11): bare 1..9 now use slots 0..8, Ctrl+1..9 selects them, Alt+5..9 uses slots 13..17, and 0 creates a shortcut to the selected owned/eligible object in the first empty slot. A focused ToolbarInputController maps semantic actions; ToolbarController owns the retail target-before-use/select ordering and auto-slot eligibility; and GameWindow.OnInputAction has one retained-runtime delegation seam. Keybinding schema v2 migrates only the exact old default Ctrl+number bindings, preserving custom user bindings. The four stance-specific combat indicator buttons 0x10000192..0x10000195 now all dispatch one injected combat toggle command. The key binding and retained toolbar therefore converge on GameWindow.ToggleLiveCombatMode, matching gmToolbarUI::ListenToElementMessage @ 0x004BEE90; the visible indicator continues to update from authoritative CombatState. Use, Examine, remaining panel buttons, raw shortcut preservation, drag/drop edge cases, and selected-object residuals remain in this wave.

Oracles: gmToolbarUI @ 0x004BD0C0..0x004BF380, UIElement_UIItem::SetShortcutNum, shortcut wire builders.

Gate: bare/Ctrl keys 1-9, Alt+5..9, item shortcuts, occupied-slot replacement, inventory-button drop, peace/war digits, player/monster/item/stack/ mana selections; full suite and user visual gate.


8. Wave 5 -- vitals and chat parity

5.1 Finish vitals state behavior

  • Bind 0x100004A9 and port root HideDetail/ShowDetail toggling through the generic state/timer system.
  • Verify normal/detail dimensions, art, values, resize and hit behavior.

5.2 Use one ChatVM/runtime model

  • Construct one ChatVM with FPS/position providers and share it between retained chat and ImGui devtools.
  • Keep devtools visibility independent; do not duplicate chat state.

5.3 Port chat window mechanics

  • Complete exact maximize/minimize, focus alpha transition, opacity bounds, send/max button states, scrollbar orientation and resizing.
  • Port half-parent-height growth, imported min/max constraints, saved geometry, bottom pinning, and restore behavior on the outer frame.
  • Drive alpha from focus/lifecycle events and retail timing, not a fixed 0.75.

5.4 Confirm the retail chat surface, then port its filters

The named gmMainChatUI build proves independently filtered chat/floaty windows; it does not prove that DAT elements 0x10000522..0x10000525 are an in-window numbered-tab model. Do not implement tab switching from the older inference.

  • Confirm the role of 0x10000522..0x10000525 from live retail behavior or a stronger DAT/call-site trace before assigning tab semantics.
  • Add a Core/VM chat-line classification carrying retail message type/channel.
  • Port the proven per-window display masks, unread state, and persistence only where the PlayerModule/options payload proves it; otherwise register the absence.
  • Route every retained/floating transcript through its filter without duplicating ChatLog.

5.5 Complete talk-focus and moderation actions

  • Enable fellowship/allegiance/patron/vassal/monarch/society options from live social state rather than a hardcoded allow-list.
  • Put availability behind one provider backed by Turbine room membership, Fellowship, Allegiance, and Society/Olthoi state; use it for both menu enablement and outbound-send validation.
  • Implement Tell-to-Selected, selection notices, squelch list/filter, and clickable-name actions.
  • Preserve exact retail word wrapping and color mapping; add rich runs where retail renders mixed styles.

Oracles: gmVitalsUI @ 0x004BFC00, gmMainChatUI @ 0x004CCCC0..0x004CE2A0, UIElement_Text, ChatFilter methods.

Gate: vitals detail toggle; chat maximize/fade; every tab/filter; Say/Tell/ Reply/channel; social enable/disable; squelch and copy/select; full suite and user visual gate.


9. Wave 6 -- inventory completion

  • Split the owned inventory window from the external/ground-container window. The proven ClientUISystem.groundObject lifecycle sends NoLongerViewingContents exactly once when an external view is replaced or closed. Owned side-bag navigation/closing sends nothing unless a live packet trace proves otherwise. Both lifecycles clear their own panel drag/view state; repeated hide is idempotent.
  • Route grid/bag clicks through target mode and global SelectionState.
  • Retire capacity/burden fallbacks only after wire captures prove ContainersCapacity, ItemsCapacity, EncumbranceVal, and augmentation delivery.
  • Port source dimming, closed-full-bag prediction, capacity direction from DAT, selection/open overlays, and full-stack merge reconciliation.
  • Replace procedural constants with imported properties where retail DAT carries them; keep visually confirmed literals only with an explicit registered row.

Oracles: gmInventoryUI, gmBackpackUI, gm3DItemsUI, UIElement_ItemList @ 0x004E16E0..0x004E4EF5, inventory game events.

Gate: main pack + every side bag; close/reopen; full/empty containers; target use; select/selected strip; reorder/merge/drop/unwield; resize/scroll at capacity; full suite and user visual gate.


10. Wave 7 -- paperdoll completion

  • Replace the ad-hoc slot array with a complete retail slot descriptor table, including Aetheria 0x10000595/596/597 and exact equip masks.
  • Drive Aetheria visibility from authoritative unlock/player state, not an unconditional layout toggle.
  • Port GetUpperInvObj priority via Core InventoryPlacement rules so layered equipment is deterministic and retail-correct.
  • Port AutoWieldIsLegal and the dual-wield shield-slot special before accepting a drop; share legality with double-click auto-wield.
  • Implement doll body-part hit testing, worn-item lookup, select/examine, drag-icon preparation, and part-selection lighting.
  • Resolve empty-slot art/visibility separately for doll and slot views from DAT; remove inventory-grid art from equip slots unless retail uses it in that state.
  • Port UpdateForRace and retain the cdb-confirmed Horan default; keep the RTT/FBO renderer as an explicit equivalent adaptation.
  • Resolve an immutable PaperdollPresentation from heritage, gender, and body type, including retail camera, heading, pose, and DAT-selected assets.

Oracles: gmPaperDollUI @ 0x004A3590..0x004A5F90, InventoryPlacement::DetermineHigherPriority, AutoWieldIsLegal.

Gate: armor/non-armor toggle; layered clothing; Aetheria; dual wield; invalid wield; body hover/select/examine/drag/highlight; at least two races; full suite and user visual gate.


11. Wave 8 -- character window completion and decomposition

8.1 Decompose before adding behavior

  • Replace the 1,900-line static CharacterStatController.Bind with an instance CharacterWindowController orchestrating focused AttributesTabController, SkillsTabController, TitlesTabController, and CharacterFooterController.
  • Share typed row/footer view models; no mutable one-element closure arrays.
  • Preserve the current accepted Attributes/Skills output during the refactor.

8.2 Complete missing state

  • Add Core title collection/current-title state and net event delivery.
  • Implement the Titles tab refresh, selection and current-title action.
  • Populate live identity title and level-200+ luminance fields/progress from the exact Int64 properties and CharacterTitleTable.
  • Port every attribute/vital/skill footer state and button availability branch.

8.3 Reconcile stat raises

  • Remove ApplyLocalRaise: named retail sends one request, ghosts the clicked button, and leaves XP/credits/ranks unchanged until the authoritative quality update arrives.
  • Permit only one raise request in flight; clear the awaiting/ghosted state on the matching authoritative quality-change path. Pin the exact rejection cleanup with a live ACE trace before adding any extra timeout or recovery behavior.
  • Test one-in-flight enforcement, authoritative acknowledgement, rejection, caps, and relog/disconnect cleanup without optimistic mutation or retry loops.

Oracles: gmStatManagementUI, gmAttributeUI, gmSkillUI, gmCharacterTitleUI @ 0x0049A610, UpdateExperience @ 0x004F0A70.

Gate: attributes, all skill buckets, train/raise/reject, Titles, live title, luminance at a level-200 fixture, resize/scroll; full suite and user visual gate.


12. Wave 9 -- social state, radar completion, and social panels

  • Add Core fellowship and allegiance state stores driven by the exact full/ incremental game events.
  • Feed fellowship leader/member and allegiance membership into RadarSnapshotProvider.relationshipFor, retiring AP-90.
  • Reuse the same state for chat availability/filtering and retained Fellowship/ Allegiance panels; never infer relationships from names or chat text.
  • Port membership lists, leader state, vitals, tree expansion, and actions from their named retail controllers/DAT layouts.

Gate: join/leave/leader change and allegiance update live; radar shapes/colors, chat menu availability, and both panels update from the same state; full suite and user visual gate.


13. Wave 10 -- missing retained panels and remaining HUD

Each item below gets its own focused pseudocode/spec/implementation plan and visual gate, but must use the completed runtime/widget/window architecture rather than add new infrastructure locally.

  1. Main panel host, combat and powerbar -- retail panel switching, combat state, attack height and power timing using layouts 0x21000073/72.
  2. Spellbook, effects and separate favorite-spell bars -- learned spells, schools/tabs, eight favorite lists, spell-bar drag/reorder, cast targeting, active effects, mana/reagent/error states. Never route these through gmToolbarUI.
  3. Social and examine residuals -- Friends/Squelch plus shared Fellowship/ Allegiance state, complete selected-target/examine data and actions.
  4. Floating chat -- reuse the same provider-backed ChatVM, shared log, filters, availability and squelch state; do not create another chat model.
  5. Quests, map, vitae, options and smartbox -- retail layouts, state and actions, including key-binding capture and settings apply/revert without an ImGui runtime dependency.
  6. Vendor, trade, salvage and tinkering -- only after their authoritative gameplay models exist; dual inventories, price/value, quantities, confirmation and authoritative transaction reconciliation.
  7. Remaining D.6 HUD -- target name/health presentation, damage floaters, world name plates and status text through dedicated Core snapshot + App renderer seams.

Gate per panel: golden DAT fixture, controller tests, complete wire/action scenario, UI Studio fixture, live user visual gate, divergence cleanup.


14. Coverage map for confirmed audit findings

Confirmed gap Retirement wave
Imported display text steals focus/drag; duplicate vitals labels 1
Buttons lack automatic retail states 1
Edge mode 3 recenter and mode 4 policy lost 1
Tooltip/global UI time ordering and removed-subtree cleanup 1
Chat maximize changes content, not outer frame 2 / 5
Only radar persists position; windows lack lifecycle 2
Controller subscriptions are not disposed 2
GameWindow owns repeated mount/composition logic 2
Item interaction is only a subset of retail policy 3
Target cursor precedence/catalog incomplete 3
Inventory target click and panel-local selection 3 / 6
Toolbar hotkeys dead 4
Inventory-to-occupied toolbar drop corrupts placement 4
Toolbar raw spell field discarded / separate favorite spell bars missing 4 / 10
Use/Examine/combat/panel buttons missing 4
Mana/stack selected-object behavior missing 4
Vitals detail state missing 5
Duplicate ChatVM lacks /loc and /framerate providers 5
Chat filter windows, opacity, channel availability, squelch/actions incomplete; numbered-tab premise unproven 5
Chat scrollbar arrows reversed 2 / 5
External-container close lifecycle missing; owned-side-bag 0x0195 premise disproved 2 / 6
Inventory capacity/burden/drag residuals 6
Paperdoll Aetheria, priority, legality, dual wield, body interactions 7
Paperdoll empty art and per-race behavior 7
Character Titles inert, live title/luminance missing 8
Character raises mutate optimistically instead of using proven server-authoritative flow 8
Radar fellowship/allegiance state missing 9
Combat/powerbar/spellbook/effects/social residuals/floating chat/quests/map/vitae/options/smartbox/vendor/trade/salvage/tinkering/world HUD absent 10
Stale/missing/colliding divergence rows 0, then every wave

15. Verification and commit discipline

For every behavioral task:

  1. Read/write the pseudocode and cite the retail address.
  2. Add a failing pure/widget/controller test.
  3. Implement the smallest coherent layer change.
  4. Run the focused project tests, dotnet build AcDream.slnx --no-restore, then dotnet test AcDream.slnx --no-build --no-restore.
  5. Update the divergence register and relevant issue in the same commit.
  6. Render the production-DAT UI Studio fixture.
  7. Launch with ACDREAM_RETAIL_UI=1 and stop at the wave's user visual gate.
  8. Commit one behavior/refactor slice; never mix unrelated cleanup into it.

Safe parallel work: isolated Core models, net parsers, fixture generators, and widget unit tests. Integration, RetailUiRuntime, GameWindow extraction, input arbitration, and GL/paperdoll wiring stay with the lead agent that holds full context.

Final parity seal

  • Every production retained layout has a committed conformance fixture.
  • Every required controller binding is asserted.
  • Every reachable named retail branch is either ported or has a current, uniquely-numbered divergence row with an approved equivalence argument.
  • All windows persist correctly per character and resolution.
  • Full automated suite is green with no new warnings.
  • UI Studio screenshot set is reviewed at reference and widescreen sizes.
  • One live regression tour covers every window, action and cross-panel flow.
  • The user passes the final side-by-side retail visual gate.