acdream/docs/research/2026-08-26-retail-inventory-interaction-audit.md
Erik f6fe0f2a4f
All checks were successful
CI / linux-portable (push) Successful in 3m27s
CI / windows-gate (push) Successful in 6m42s
CI / release (push) Successful in 2m12s
fix(client): restore retail interaction parity
Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
2026-08-26 20:45:11 +02:00

36 KiB
Raw Blame History

Retail inventory interaction audit

Date: 2026-08-26

Scope: Selection, status text, single/double/right click, drag/drop, container movement, ground pickup/drop, equipping, stack splitting, vendor staging, failure feedback, and SpewBox routing.

Change policy: Audit followed by implementation in the same worktree.

Source snapshot: 0c699240, plus the already-present working-tree fixes for #444, #445, #446, #447, and #449. Those fixes are assessed as found; this report does not claim that they have been committed or user-accepted.

Implementation closeout — 2026-08-26

Slices 14 below are implemented and automated-test covered. Gameplay refusals now use the ClientLocal SpewBox route; move/wield failure kinds are complete; full move/drop/wield are request-first with pending projections; owned-container and vendor-row mouse behavior follows the named retail handlers; hover and release share one side-effect-free legality policy; and local item-policy wording is composed from retail's exact literals.

The last toolbar uncertainty is also resolved. Raw retail bytes at gmToolbarUI::HandleSelectionChanged @ 0x004BF4EF push format literal 0x007B4748, which decodes to %d %hs (of %d). The owned Pyreal-stack branch now renders that exact stack/name/total shape. The final AutoWield fallback was also corrected: retail does not print the invented “That slot is already in use”; with automatic unblocking enabled it moves the preferred occupied-slot item to the backpack, waits for the authoritative move, then retries the wield. Slice 5 remains deliberately deferred as the single combined connected owner gate.

Audited root causes (now fixed)

The inventory implementation was not missing one isolated rule. Most individual operations existed and used the correct wire messages, but three seams made the whole experience feel intermittent:

  1. Some retail-local refusal text was routed to a dead production callback. ItemInteractionController and AutoWieldController used a toast callback for a substantial class of local rejections while GameWindow supplied null. Retail sends these messages to the ClientLocal channel, which is the SpewBox in acdream. The result was a real silent-failure class, not merely different wording.
  2. Full moves, world drops, and wield operations mutated canonical inventory state before the server accepted them. Retail normally leaves the source canonical object in place, adds a waiting/ghost projection at the intended destination, and commits only after the authoritative object update. The old optimistic mutation was reversible, but selection, capacity, paperdoll, vendor, and other observers could see a transient state that never existed on the server. This was the largest structural flakiness risk.
  3. Several list-specific mouse behaviors did not match retail. In particular, staged vendor rows could not be double-clicked or dragged to remove them, staged rows lacked right-click examine, and owned side-pack double-click/open ordering differed from retail.

The wire builders, global selection/split model, merge-first rule, request gate, most right-click examine paths, normal item double-click use/equip, external container pickup, paperdoll placement validation, and the newly repaired vendor-split/main-pack-capacity paths are broadly aligned with retail.

The implementation was executed in this order:

  1. Route every local item refusal through ClientLocal/SpewBox.
  2. Replace canonical optimistic movement with retail-style pending projections.
  3. Close the vendor staged-row and owned-container input differences.
  4. Deepen hover/drop legality and finish exact status/failure text parity.
  5. Run one connected interaction matrix across inventory, paperdoll, ground, external containers, and vendors.

Method and evidence standard

This audit used four evidence layers:

  • The September 2013 named retail pseudo-C under docs/research/named-retail/acclient_2013_pseudo_c.txt, searched by named class and method before relying on older address-only material.
  • Existing focused retail notes under docs/research/, especially the item, drag, give, world-drop, use/autowear, and vendor investigations.
  • The current production controllers, Runtime owners, UI input dispatch, wire request builders, and communication routing.
  • Existing focused tests, used to distinguish implemented intent from behavior that is not currently protected.

Verdicts in this report mean:

  • Match: the important retail behavior and ownership rule are present.
  • Partial: the common path matches, but a retail branch, presentation rule, or failure path is absent.
  • Mismatch: direct retail evidence contradicts the current behavior.
  • Risk: the mechanism differs in a way likely to produce transient or race defects, but this audit does not assert a particular live symptom without a connected reproduction.
  • Gate pending: a code fix exists in the working tree and has automated coverage, but the owner has not yet accepted the live behavior.

Retail reference model

One selected object and one split quantity

Retail has a client-global selected object. Clicking an item selects it; right-click first selects it and then examines it; beginning a drag selects it if it was not already selected. The toolbar observes that global selection and shows the name, stack quantity, and split controls.

The split quantity is also global and applies only when the dragged/requested object is the selected object. An unselected stack always means the full stack. Changing selection resets/reseeds the split amount. Vendor-owned selected stacks use a different initial amount from normal owned stacks.

Primary anchors:

  • UIElement_ItemList::ListenToElementMessage at 0x004E4D50
  • UIElement_ItemList::BeginDrag at 0x004E32D0
  • gmToolbarUI::HandleSelectionChanged at 0x004BF380
  • ItemHolder::GetObjectSplitSize in the named retail pseudo-C

Mouse-down establishes intent; click completion performs list action

For a retail item-list entry, left press first gives target mode a chance to consume the object. Otherwise it selects the object. A container-list entry also opens that child container and updates its open indicator in this same item-list message path.

Right press selects and examines. Double-click invokes generic UseObject for ordinary list items, but the generic double-use path is suppressed for an owned containerList entry. The ground/external root is explicitly allowed.

This distinction matters: a side pack is opened as a container, not opened and then generically used as an ordinary item on the second click.

Dragging is a request with pending presentation

Beginning a physical-item drag produces a source waiting/ghost state. Vendor, salvage, and shortcut lists are special list types and do not use the same physical-source waiting ghost.

Hover is advisory and silent. Release reruns legality with feedback enabled. For a normal container move, retail retains the canonical source ownership and adds a pending destination projection. The server's authoritative object update commits the move. Rejection removes the pending projection and prints the local failure. This same general principle appears in world placement and split-to- world handling.

Primary anchors:

  • UIElement_ItemList::BeginDrag at 0x004E32D0
  • UIElement_ItemList::DragOver at 0x004E3400
  • UIElement_ItemList::AcceptDragObject at 0x004E4250
  • UIElement_ItemList::HandleDropRelease at 0x004E4790
  • ItemHolder::AttemptToPlaceInContainer_IsItemLegal at 0x005870C0
  • ItemHolder::AttemptToPlaceInContainer_IsContainerLegal at 0x005879B0
  • ItemHolder::WillItemFitInContainer at 0x00587D60
  • ItemHolder::IsDragIntoContainerAttemptLegal at 0x00587E90

Drop target dispatch is ordered

Retail's three-dimensional drop/give dispatcher follows this practical order:

  1. Require an owned, movable source that is not currently in trade.
  2. Dropping on self means the main backpack.
  3. Target zero means ground placement or split-to-world.
  4. Try stack merge before treating the target as a container.
  5. A player target opens/routes through secure trade.
  6. A creature target uses give-item behavior.
  7. A container target must be open, unlocked, and legal.
  8. Vendor lists use their own staging rules.
  9. Otherwise resolve as a ground placement or refuse it.

AttemptMerge uses the selected split amount, clamps to target capacity, sends the merge request, and selects the target stack. Give-item is request-only; it does not optimistically remove the source from canonical inventory.

Primary anchors:

  • ItemHolder::AttemptMerge at 0x005878F0
  • ItemHolder::AttemptPlaceIn3D at 0x00588600
  • docs/research/2026-07-13-retail-give-item-pseudocode.md
  • docs/research/2026-07-26-retail-inventory-placement-and-world-drop-pseudocode.md

Use and equip

Generic double-click use passes through ItemHolder::DetermineUseResult and ItemHolder::UseObject, with a short use throttle. The item is classified as direct-use, targeted-use, pickup, equip/autowear, trade, salvage, or game use. Retail locally refuses invalid states and prints a ClientLocal message.

Paperdoll 3D clicks and discrete equipment-slot lists share the same global selection/examine model. Dropping on a paperdoll location validates the exact location, then chooses auto-wear or auto-wield behavior. Clothing overlap can be rejected locally; weapon replacement has different rules.

Primary anchors:

  • ItemHolder::DetermineUseResult at 0x00588460
  • ItemHolder::UseObject at 0x00588A80
  • CPlayerSystem::UsingItem at 0x00562F70
  • gmPaperDollUI::ListenToElementMessage at 0x004A5C30
  • gmPaperDollUI::AcceptDragObject at 0x004A3B10
  • gmPaperDollUI::AcceptPaperDollDragObject at 0x004A4A70
  • docs/research/2026-07-23-retail-item-use-and-autowear-pseudocode.md

Vendor rows are active item lists

Direct named-retail evidence establishes these behaviors:

  • Double-clicking a vendor browse row buys one item.
  • Double-clicking a staged buying row removes it and prints “Removing %s from shopping list” through ClientLocal.
  • Double-clicking a staged selling row removes it, clears its sell state, and prints the same form of message.
  • Dragging an already-staged selling row removes it from the staged list.
  • If a partial split is selected while dragging a staged selling row, retail refuses to split that row, prints “You cannot split items from this panel”, and resets the split control to the stack maximum.
  • A new partial-stack drag into the Selling list sends a split request, creates a temporary staged row, and replaces that row when the new matching object arrives.
  • Hover rejection is silent; release rejection prints to ClientLocal.

Primary anchors:

  • gmVendorUI::HandleMousePresses at 0x004C40D0
  • gmVendorUI::RecvNotice_ItemListBeginDrag at 0x004C4380
  • VendorSellUI::DragItemAcceptable at 0x004C20C0
  • VendorSellUI::AcceptDragObject at 0x004C4F00
  • VendorSellUI::ItemAttributesChanged at 0x004C3FD0

This corrects an older project research conclusion: browse-row double-click buy is retail behavior. It is not an acdream modernization.

Feedback destination

Retail item-policy and request-failure messages are sent on the local client text channel. In acdream, RuntimeCommunicationState.AddText maps ClientLocal (0x1A) to the SpewBox only: it does not add the line to the chat transcript and does not apply a chat timestamp.

Hover failures are normally silent. Release/action failures are not. Server request failures are composed by ACCWeenieObject::ServerSaysAttemptFailed at 0x0058EAE0, including move and wield failures.

Current acdream ownership and routing

The relevant production flow is:

UiRoot / UiItemSlot
  -> InventoryController | ExternalContainerController | PaperdollController
     | VendorUiController | SelectedObjectController
  -> ItemInteractionController / AutoWieldController
  -> RuntimeInventoryState + RuntimeActionState transactions
  -> ClientObjectTable (canonical object ownership)
  -> outbound request builder
  -> authoritative object update / request failure
  -> RuntimeCommunicationState.ClientLocal -> SpewBox

Important owners:

  • SelectionState is the sole selected-object owner shared by inventory, paperdoll, vendor, world selection, and toolbar status.
  • RuntimeInventoryState owns external-container state, item-use transaction state, shared busy/request state, split/pending placement state, and borrows the canonical ClientObjectTable.
  • SelectedObjectController projects selection into the authored toolbar and owns the split-slider presentation.
  • ItemInteractionController classifies use/drop/give/move operations and sends requests.
  • InventoryController, ExternalContainerController, PaperdollController, and VendorUiController own their list-specific input and projections.

This ownership shape aligns with the architecture document. The central issue is not duplicate state; it is which state is mutated before acknowledgement.

Behavior matrix

Surface/action Retail Current acdream Verdict
Inventory left press Target-mode consume, otherwise select PrimaryItemPressed does the same Match
Ordinary item single click Select; no generic use Mouse-down selects Match
Ordinary item double-click Generic use/equip DoubleClicked = ActivateItem Match
Owned side-pack single press Select and open in the item-list handler Selects and opens on mouse-down Match, implemented
Owned side-pack double-click Open behavior; generic item double-use suppressed Opens once; generic activation is suppressed Match, implemented
Inventory right-click Select, then examine Select and examine Match
Drag lift Select if needed; source ghost Selects and ghosts Match
Drag hover Silent, legality-aware green/red Silent and shares the release legality decision Match, implemented
Full internal move Request plus pending destination projection; canonical source waits for server Request-first pending projection; authoritative update commits Match, implemented
Merge stacks Merge before container placement; selected split amount; select target Same broad behavior Match
Partial move to container Split request; wait for authoritative object Request-only Match
Drop to ground Request/pending presentation; source remains canonical until response Request-first; canonical source waits for response Match, implemented
Split to ground Global pending split; select arriving matching object; timeout Request/pending path exists Broad match
Pick up from ground Pending destination projection; authoritative commit Pending destination path Match
Open external container Root/nested list-specific behavior Root double-click, nested open behavior Broad match
Move to external container Request-only, open/unlocked legality, server commit Request-only with shared hover/release legality Match, implemented
Give to creature Request-only; selected split amount Request-only Match
Give/drop to player Secure-trade routing Secure-trade routing exists Broad match
Paperdoll click/right-click Global select/examine Global select/examine Match
Paperdoll drag equip Exact location validation; auto-wear/wield Same broad split Broad match
Full wield Authoritative request model Request-first; canonical ownership waits for response Match, implemented
Invalid item use/equip ClientLocal text in SpewBox Shared ReportClientLocal route Match, implemented
Selected status Normal name or {quantity} name; owned coin is %d %hs (of %d) Both branches implemented Match, implemented
Split applicability Only selected stack uses global quantity Same Match
Vendor browse single/right Select; right-click examine Select and right-click examine Match
Vendor browse double Buy one Buy one Match
Drag inventory to Selling Stage full or selected partial quantity Present; partial temp-row replacement present Match, #445 gate pending
Vendor hover refusal Silent Silent Match
Vendor release refusal ClientLocal/SpewBox System message/SpewBox path Match
Staged Buying double-click Remove one + SpewBox line Same Match, implemented
Staged Selling double-click Remove row, clear state + SpewBox line Same Match, implemented
Staged Selling drag Remove row; partial selection warns and resets split Same, exact refusal + reset Match, implemented
Staged row right-click Generic select/examine item-list behavior Select and examine on every vendor list role Match, implemented
Main-pack capacity Items and carried containers counted separately Separate loose-item count now present Match, #449 gate pending
Server move/wield failure text Exact ClientLocal move/wield compositions Both request kinds and compositions present Match, implemented

Findings

F1 — local inventory refusals can be completely silent

Resolution: CLOSED IN CODE — one ReportClientLocal route now selects interface text, system text, or the test fallback in that order.

Priority: P0

Confidence: Confirmed by production composition

ItemInteractionController uses two different presentation routes:

  • _systemMessage / _interfaceText, which are wired to RuntimeCommunicationState.AddText(..., ClientLocal) and reach SpewBox.
  • _toast, used by many local policy refusals.

InteractionRetainedUiComposition forwards its toast dependency, but GameWindow currently sets the production composition toast to null after the developer-toast surface was removed. Consequently, the local rejection still aborts the action, but the user receives no explanation.

Affected classes include invalid item use, missing use target, trade/wield requirements, locked or unsuitable targets, invalid move/give/drop states, midair/drop refusal, and paperdoll slot-in-use refusal. Exact membership should be frozen in a focused message-routing test before changing it.

Retail evidence is unambiguous: these are local client text messages and belong in SpewBox, not a transient developer toast.

Future fix: remove the semantic split for gameplay failure text. Give item controllers one ClientLocal sink and reserve any visual toast mechanism for non-retail developer/launcher notifications.

F2 — optimistic canonical moves expose impossible intermediate state

Resolution: CLOSED IN CODE — full move, world drop, and wield dispatch requests without mutating canonical ownership; pending source/destination presentation converges on confirmation, failure, and reset.

Priority: P0 architectural correction

Confidence: Confirmed mechanism divergence; symptom linkage requires gates

The full-stack internal move and world-drop paths use optimistic operations against the canonical object table. Full wield uses the same pattern. Failure rollback exists, but all borrowers can observe the speculative state:

  • selection and toolbar status;
  • loose-item and carried-container capacity;
  • paperdoll slots;
  • vendor sell eligibility/staging;
  • external-container views;
  • plugins and Runtime views.

Retail instead keeps source canonical ownership stable and uses waiting/ghost presentation at the intended destination until the server update arrives.

This does not prove that every reported intermittent inventory symptom comes from this seam. It does explain why otherwise-correct controllers can disagree briefly and why a rejection/late response/re-entrant action can make the UI feel flaky.

Future fix: model full move/drop/wield like the existing request-only split, give, ground-pickup, and external-container paths. Store a generation-scoped pending placement intent and presentation ghost, send the request, and let the authoritative update commit canonical ownership. On failure/timeout/reset, remove only the pending presentation.

F3 — vendor staged-row removal behavior is missing

Resolution: CLOSED IN CODE — staged rows implement the retail double-click, right-click, drag-lift, message, and split-reset branches.

Priority: P1

Confidence: Confirmed by direct named-retail functions

Current staged Buying and Selling rows only bind selection. They have no double-click removal. Selling rows also disable drag source behavior.

Retail supports:

  • double-click staged Buying to remove;
  • double-click staged Selling to remove and clear sell state;
  • drag staged Selling to remove;
  • a precise ClientLocal removal line;
  • a partial-split refusal/reset when dragging from the staged Selling list.

Future fix: add list-role-specific actions rather than routing these rows through generic item activation. Protect each action with unit tests that also assert selection, sell-state cleanup, totals, and exact SpewBox routing.

F4 — owned side-pack click/double-click sequencing differs

Resolution: CLOSED IN CODE — carried containers open on press and the generic double-use route is suppressed for that list role.

Priority: P1

Confidence: Confirmed structural mismatch

Retail opens a carried child container in the item-list press handler and suppresses generic double-click use for a containerList item. acdream selects on mouse-down, opens on completed click, and binds generic activation to double click for every inventory cell. UiRoot emits the second click before the double-click event, so a double-click can both open and activate the pack.

This is a plausible source of redundant requests and awkward drag/open interactions. It should be fixed by explicit item-list role, not by a global double-click timing change, because ordinary items and the external-container root intentionally retain double-click use/open behavior.

F5 — hover acceptance is less strict than release/server legality

Resolution: CLOSED IN CODE — InventoryContainerPlacementPolicy is the shared silent-hover/speaking-release decision for owned and external lists.

Priority: P1/P2

Confidence: Confirmed code difference

Inventory-grid hover mostly checks list role, basic object class, and capacity. External-container hover is broader still. Retail's predicates incorporate ownership, trade state, source/destination identity, real carrying-container restrictions, open/locked state, destination capacity type, and other legal conditions.

The practical symptom is a green cursor followed by a refusal or apparent no-op on release. Hover must remain silent, but its boolean should be produced from the same pure legality decision used at release.

Future fix: extract one side-effect-free placement decision that returns a reason code. Hover consumes only allowed/denied; release converts the same reason to exact ClientLocal text.

F6 — selected status lacks retail's owned-coin special case

Resolution: CLOSED IN CODE — the PDB-matched retail executable resolves the literal at 0x007B4748 to %d %hs (of %d); the controller now reads the player's CoinValue and uses that exact branch for owned WCID 273 stacks.

Priority: P2

Confidence: Byte-resolved from the PDB-matched retail executable

Normal current text—name for a singleton and {stackSize} {name} for a stack—matches the main retail branch. Retail has an additional owned-coinstack formatting branch that derives a total/value-aware display and name. The current controller always uses the generic stack prefix.

Binary inspection resolves the apparent vtable-symbol artifact: the raw call site pushes 0x007B4748, %d %hs (of %d), with stack size, appropriate name, and the player's integer CoinValue as its three arguments.

F7 — request failure coverage omits move and wield kinds

Resolution: CLOSED IN CODE — both request kinds are represented and route through the item-aware retail failure composer.

Priority: P2

Confidence: Confirmed enum/composer gap

Retail's ServerSaysAttemptFailed includes move and wield result families. The current request-failure model and InventoryFailureMessages cover merge, split, pickup, put, drop, and give, but do not represent the retail move/wield families. A server-side failure in those operations therefore cannot produce the exact item-aware retail sentence through the common composer.

F8 — current retail-divergence documentation is wrong about vendor double-click

Resolution: CLOSED — the older research is corrected and AP-171 retired.

Priority: Documentation correction before implementation

Confidence: Confirmed by direct named-retail evidence

Older vendor research and AP-171 characterize double-click browse-row purchase as an acdream enhancement. gmVendorUI::HandleMousePresses directly calls BuySingleItem on the retail Items-list double-click. Current browse behavior is correct; the documentation is not. Leaving this claim in the register risks a future parity cleanup deleting a retail feature.

F9 — #445 and #449 need connected acceptance, not more inference

Priority: Gate now

Confidence: Automated fixes present

  • #445 now uses the selected split quantity for vendor selling, creates a temporary staged row, and replaces it when the authoritative split object arrives.
  • #449 now counts loose items separately from carried container objects when deciding whether the main backpack is full.

Both have focused tests in the current working tree. Neither should be marked closed until a live server gate covers success, refusal, repeated action, and selection changes.

F10 — paperdoll disappearance is a separate rendering/residency defect

Priority: Keep separate from transaction fixes

Confidence: Existing issue #443

The intermittent missing paperdoll that heals after a delay is tracked as paperdoll first-open/residency behavior. It can make a correct equip transaction look broken, so it belongs in the combined user gate, but it should not be folded into inventory ownership or input logic without evidence.

SpewBox contract

The following should appear in the SpewBox through ClientLocal when the user commits the action and it is refused or changed:

  • invalid use/equip/wield state;
  • “choose a target” or invalid target;
  • cannot move/drop/give an item;
  • locked, closed, full, or otherwise illegal destination;
  • merge/split/pickup/put/drop/give/move/wield request failure;
  • vendor item cannot be sold or split in that list;
  • removal from a vendor shopping/selling list;
  • automatic removal of conflicting wear items where retail reports it;
  • midair or other locally cancelled placement when retail reports it.

The following should be silent:

  • merely hovering a rejected drop target;
  • moving the pointer away without releasing;
  • ordinary selection changes;
  • beginning a legal drag.

These messages should not be duplicated into the normal chat log and should not gain chat timestamps. That is already how ClientLocal behaves in the communication owner.

Existing automated coverage

The repository already has strong narrow coverage in:

  • InventoryControllerTests: population, selection, open/right-click, drag/ghost, pending pickup, split, merge, capacity, rollback, and #449.
  • ExternalContainerControllerTests: root/nested behavior, selection, right-click, partial split, and pending gates.
  • PaperdollControllerTests: selection, examine, drag, and wield placement.
  • SelectedObjectControllerTests: name, stack status, slider, and vendor split initialization.
  • VendorUiControllerTests: browse, buy quantities, selection/examine, staging, partial vendor split/failure, rejection feedback, and alternate currency.
  • ItemInteractionControllerTests: use/equip, world drop, give, partial-stack behavior, failures, and transaction lifecycle.
  • Runtime inventory tests: request ownership, reset, and lifecycle behavior.

The pre-implementation test suite was strongest at proving controller-local intent. The implementation program below adds the missing transaction and cross-controller coverage.

Automated gates added by the implementation

The implementation adds or updates coverage for the following:

  1. A production-composition test proving every local policy rejection reaches ClientLocal/SpewBox and no gameplay failure depends on a toast callback.
  2. Owned side-pack single/double-click tests proving one open action and no generic use request, including the second-click event order.
  3. Vendor staged Buying and Selling double-click removal tests with exact selection, totals, state cleanup, and message assertions.
  4. Vendor staged Selling drag-to-remove and selected-partial split-reset tests.
  5. Staged vendor-row right-click select/examine tests.
  6. A table-driven pure legality test shared by hover and release for inventory, external container, ground, player, creature, vendor, self, locked container, full item slots, and full container slots.
  7. Owned coinstack toolbar-status parity using the byte-resolved exact format.
  8. Move and wield authoritative failure-composition tests.
  9. Transaction-observer tests proving canonical ownership does not change before acknowledgement while selection, capacity, vendor, and paperdoll borrow the same state.
  10. Re-entrant sequences: drag while a request is pending, selection change during split, rejection after container close, late response after session reset, and repeated action after rollback.

Executed implementation program

Slice 1 — feedback integrity — COMPLETE

  • Replace gameplay toast refusal calls with the shared ClientLocal sink.
  • Add the missing move/wield failure kinds and exact item-aware compositions.
  • Freeze hover-silent versus release-speaks behavior.
  • Correct the vendor double-click documentation claim.

This is small, high-confidence, and immediately turns “nothing happened” into an actionable player explanation.

Slice 2 — authoritative placement ownership — COMPLETE

  • Introduce one generation-scoped pending placement record for full move, world drop, and wield.
  • Preserve canonical source ownership until the authoritative object update.
  • Project source waiting/ghost and destination pending visuals separately.
  • Converge success, refusal, timeout, disconnect, and late-response cleanup.
  • Prove all borrowed observers see either pre-commit or committed state, never a speculative canonical move.

This is the most important solidity work and should receive dual review because it crosses Runtime ownership and retained presentation.

Slice 3 — item-list mouse parity — COMPLETE

  • Make carried-container press/open and double-click suppression explicit.
  • Add staged vendor double-click removal.
  • Add staged Selling drag-to-remove and split reset/refusal.
  • Restore right-click select/examine consistently across vendor list roles.

Slice 4 — shared legality and exact presentation — COMPLETE

  • Unify hover/release placement decisions with reason codes.
  • Add the owned-coinstack toolbar branch after capturing exact retail text.
  • Reconcile hard-coded local item wording with DAT-backed retail strings.

Automated verification — COMPLETE

  • Focused inventory/external-container/paperdoll/vendor/selection/item-use matrix: 328 passed, 0 failed.
  • Cross-controller retained-UI interaction flow: 10 passed, 0 failed.
  • Complete Release build: 0 warnings, 0 errors.
  • Repository hermetic lane (the exact release filter, serial execution): 15,755 passed, 0 skipped, 0 failed across 14 test assemblies.

The repository wrapper's project-consistency preflight explicitly excludes the tracked deployment-only ACE comparison mods under tools/ace-mods/. They compile against a separately installed ACE server and intentionally remain outside AcDream.slnx; the portable product graph still owns every other project under src/, tests/, and tools/.

Slice 5 — connected closure — DEFERRED OWNER GATE

Run the manual matrix below against ACE using an exact built binary and retain logs/screenshots for failures. Close #445 and #449 only after their rows pass. Keep #443 independent unless the evidence links paperdoll rendering to an inventory acknowledgement.

Connected manual matrix

Use one normal item, one wearable item, one wieldable item, two mergeable stacks, one side pack, a full main backpack, a full side pack, an open chest, a locked/closed container if available, a creature/player target, and a vendor with normal and alternate currency.

  1. Single-click each item/list type; verify selection border and exact status.
  2. Right-click inventory, side-pack, external-container, paperdoll, browse, Buying, and Selling rows; verify selection and examine.
  3. Double-click ordinary usable, wearable, wieldable, and unusable items; verify one request and correct SpewBox refusal where applicable.
  4. Single- and double-click a carried side pack; verify one open action, no redundant generic use, and stable selection.
  5. Drag a full item between main pack and side pack; observe source/destination before response, after success, and after forced rejection.
  6. Fill a side pack, reject a move, free one slot, and retry immediately.
  7. Fill the main pack with loose items while carrying side packs; verify item and container capacities independently (#449).
  8. Merge full and partial stacks; verify selected split amount, target selection, source remainder, and full-target refusal text.
  9. Split to an inventory container, external container, creature, ground, and vendor; change selection while the request is pending.
  10. Drop full and partial stacks to ground; verify ghost/pending behavior, selected arriving object, rejection cleanup, and no duplicate item.
  11. Pick up from ground into a nearly full destination, then retry after freeing capacity.
  12. Equip by double-click and by paperdoll drag; test clothing conflict and weapon replacement. Verify source/paperdoll state before acknowledgement.
  13. Drag full and partial stacks to vendor Selling; verify exact quantities, temp-row replacement, totals, and #445 behavior.
  14. Double-click staged Buying and Selling rows to remove them; verify SpewBox text and state cleanup.
  15. Drag a staged Selling row to remove it; repeat with a partial split selected and verify refusal plus slider reset.
  16. Complete/cancel transactions in normal and alternate currency; verify currency balance refresh (#444) and selection/status stability.
  17. Repeat representative actions while another inventory request is pending, immediately after rejection, and immediately after reopening a container.
  18. Log out/portal/re-enter with a pending or recently completed interaction; verify the request ledger and pending projections converge to zero.

For every refused release/action, record whether the cursor was green/red, whether a SpewBox line appeared, the exact line, and whether canonical item ownership changed before the server response.

Evidence index

Retail research already in the tree:

  • docs/research/deepdives/r06-items-inventory.md
  • docs/research/2026-07-13-retail-item-drag-visuals-pseudocode.md
  • docs/research/2026-07-13-retail-give-item-pseudocode.md
  • docs/research/2026-07-23-retail-item-use-and-autowear-pseudocode.md
  • docs/research/2026-07-26-retail-inventory-placement-and-world-drop-pseudocode.md
  • docs/research/2026-08-08-slice6-vendor-transactions-research.md
  • docs/research/named-retail/acclient_2013_pseudo_c.txt

Primary current implementation surfaces:

  • src/AcDream.App/UI/UiRoot.cs
  • src/AcDream.App/UI/UiItemSlot.cs
  • src/AcDream.App/UI/ItemInteractionController.cs
  • src/AcDream.App/UI/Layout/InventoryController.cs
  • src/AcDream.App/UI/Layout/ExternalContainerController.cs
  • src/AcDream.App/UI/Layout/PaperdollController.cs
  • src/AcDream.App/UI/Layout/SelectedObjectController.cs
  • src/AcDream.App/UI/Layout/VendorUiController.cs
  • src/AcDream.App/UI/AutoWieldController.cs
  • src/AcDream.App/Composition/InteractionRetainedUiComposition.cs
  • src/AcDream.App/Rendering/GameWindow.cs
  • src/AcDream.Core/Items/ItemInteractionPolicy.cs
  • src/AcDream.Runtime/Gameplay/RuntimeInventoryState.cs
  • src/AcDream.Runtime/Gameplay/RuntimeActionState.cs

Closure statement

The retail-backed work order is implemented through Slice 4. The code now has one ClientLocal feedback route, request-first authoritative placement, list-role-specific retail mouse behavior, shared placement legality, complete move/wield failure composition, exact local-policy literals, and the exact owned-coinstack status format. Occupied-slot AutoWield now also follows retail's move-confirm-retry transaction instead of emitting an invented refusal. The complete hermetic automated lane is green. No connected acceptance is claimed here; the combined owner gate remains the final closure step, and #443 remains an independent private-viewport residency issue.