Resolve and sample retail's authored nine-color paperdoll click map, preserve local click coordinates, and select the stable highest-priority worn item with the player fallback. Keep targeted-use body clicks routed to self and pin both synthetic and live-DAT conformance. Co-Authored-By: Codex <codex@openai.com>
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
- Modern code, retail behavior. Port retail control flow, constants, state precedence, and boundary conditions. Modernize ownership and representation, not observable behavior.
- No new feature bodies in
GameWindow.cs. It may construct typed binding records and mount oneRetailUiRuntime. Panel logic belongs in controllers; pure AC rules belong in Core; GL-only viewport/cursor presentation remains in App/Rendering. - 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.
- 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. - 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.
- Typed behavior, not stringly behavior. State names remain DAT data, but C#
policy uses typed capabilities/records (independent text
EditableandSelectableflags, button phase, cursor role, shortcut kind, window event). Controllers must not implement precedence by scattered state-name string lists. - Controllers have lifetimes. Every subscribing controller implements
IDisposable; subscriptions use removable named handlers or a smallSubscriptionBag.RetailUiRuntime.Disposetears down the complete tree. - Required bindings fail loudly. Add a
LayoutBindinghelper withRequire<T>(id, role)andOptional<T>(id). Missing required roles disable the affected window with one diagnostic instead of silently producing a partial UI. - No raw game-state writes. All mutations go through Core-owned eventful APIs and, where retail predicts locally, a pending/confirm/rollback ledger.
- 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_Textinteraction/property modes;UIElement_Button::UpdateState_, mouse transitions, enabled/toggle behavior;UIElement::UpdateForParentSizeChangeraw edge modes 0-4;- UI manager tooltip deadlines, global time delivery, and focus notifications;
gmToolbarUIglobal messages, drop branches, mana/stack behavior;ItemHolder::UseObject/AttemptPlaceIn3Ddecision 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:
ShortCutDataisindex_,objectID_,spellID_, whilePlayerModule::favorite_spells_[8]is a separate subsystem. This retail toolbar restores only nonzeroobjectID_; 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
ElementInfoJSON fixtures for toolbar0x21000016, inventory0x21000023plus mounted sublayouts, paperdoll0x21000024, and character0x2100002E. - 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
falseand0override 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
0x16and Selectable0x27as independent capabilities, matching retail; do not collapse them into an exclusive interaction-mode enum. - Import retail Editable
0x16, OneLine0x20, and Selectable0x27properties 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 explicitlyEditable; all labels remainDisplay. - Bind the imported vitals labels
0x100000EB/ED/EFdirectly 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
UiButtonStateMachineover 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, selected6/7/8, and ghosted13; if the requested state is absent, retain the current custom semantic state rather than overwriting states such asMaximized,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
UiLayoutPolicycontaining the five raw edge modes0..4, original child rect, and original parent rect. Preserve raw modes throughElementReader. - Port
UpdateForParentSizeChangeas a pure function, including right-track, center mode3, proportional mode4, and inclusive-pixel behavior for all four edges. - Keep
AnchorEdgesonly as a compatibility adapter for programmatic widgets; imported DAT elements useUiLayoutPolicydirectly. - 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 message3to 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
RetailWindowHandleand lifecycle events:Shown,Hidden,Moved,Resized,Closed,LockChanged. - Move named-window registry/raise policy behind
RetailWindowManager; retain thinUiHostforwarding APIs for callers. - Define
IRetainedPanelController : IDisposablewith idempotentOnShown,OnHidden, and optional descendant-focus notification, without couplingUiRootto game/network semantics. - Make
UiRootemit 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.Mountto 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
UiNineSlicePanelmount recipes fromGameWindowandMockupDesktop. - 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
UiWindowLayoutcontaining position, size, visibility, collapsed/maximized state, and any retail-persisted panel state. - Key layouts by character and resolution, matching retail
saveui/loaduisemantics; 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
defaultlogin 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 : IDisposablecontract; allow plainIDisposableonly 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
RetailUiRuntimeand 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
SelectionStatewith 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, andUseItemOnTarget(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, andAttemptPlaceIn3Dto pure/testable Core policy records. - Include ready/busy, ownership, vendor/trade, PK/rare confirmation, stack, ground/drop, and target-type gates.
- Keep App
ItemInteractionControlleras 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 enums0x27/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, andCreateShortcuttoToolbarControllermethods. - Port use-vs-select semantics from
gmToolbarUI::ListenToGlobalMessage. - Keep
GameWindow.OnInputActionto one delegation seam.
4.2 Preserve the exact shortcut model; keep the toolbar object-only
- Model the 18 raw
ShortCutDataentries exactly as signedindex_, unsignedobjectID_, and unsignedspellID_; 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 @ 0x004BF810restores only entries with a nonzeroobjectID_. - Keep the main toolbar item/object-only. Feed
PlayerModule::favorite_spells_[8]and the already parsedHotbarSpellsinto 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
ShortcutDropPlannerkeyed 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
ShortCutDataentry.
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_SplitStackactivation behavior, andHandleSelectionChangedstack 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
0x100004A9and port rootHideDetail/ShowDetailtoggling 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..0x10000525from 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.groundObjectlifecycle sendsNoLongerViewingContentsexactly 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/597and exact equip masks. - Drive Aetheria visibility from authoritative unlock/player state, not an unconditional layout toggle.
- Port
GetUpperInvObjpriority via CoreInventoryPlacementrules so layered equipment is deterministic and retail-correct. - Port
AutoWieldIsLegaland the dual-wield shield-slot special before accepting a drop; share legality with double-click auto-wield. - Implement exact DAT doll body-part hit testing, worn-item lookup, normal selection/player fallback, and target-mode self dispatch.
- Complete double-click examine, drag-icon preparation, and part-selection lighting on the doll body.
- 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
UpdateForRaceand retain the cdb-confirmed Horan default; keep the RTT/FBO renderer as an explicit equivalent adaptation. - Resolve an immutable
PaperdollPresentationfrom 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.Bindwith an instanceCharacterWindowControllerorchestrating focusedAttributesTabController,SkillsTabController,TitlesTabController, andCharacterFooterController. - 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.
- Main panel host, combat and powerbar -- retail panel switching, combat state,
attack height and power timing using layouts
0x21000073/72. - 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. - Social and examine residuals -- Friends/Squelch plus shared Fellowship/ Allegiance state, complete selected-target/examine data and actions.
- Floating chat -- reuse the same provider-backed
ChatVM, shared log, filters, availability and squelch state; do not create another chat model. - Quests, map, vitae, options and smartbox -- retail layouts, state and actions, including key-binding capture and settings apply/revert without an ImGui runtime dependency.
- Vendor, trade, salvage and tinkering -- only after their authoritative gameplay models exist; dual inventories, price/value, quantities, confirmation and authoritative transaction reconciliation.
- 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, legality, dual wield, examine/drag/selection lighting | 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:
- Read/write the pseudocode and cite the retail address.
- Add a failing pure/widget/controller test.
- Implement the smallest coherent layer change.
- Run the focused project tests,
dotnet build AcDream.slnx --no-restore, thendotnet test AcDream.slnx --no-build --no-restore. - Update the divergence register and relevant issue in the same commit.
- Render the production-DAT UI Studio fixture.
- Launch with
ACDREAM_RETAIL_UI=1and stop at the wave's user visual gate. - 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.