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.
36 KiB
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 1–4 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:
- Some retail-local refusal text was routed to a dead production callback.
ItemInteractionControllerandAutoWieldControllerused atoastcallback for a substantial class of local rejections whileGameWindowsuppliednull. Retail sends these messages to theClientLocalchannel, which is the SpewBox in acdream. The result was a real silent-failure class, not merely different wording. - 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.
- 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:
- Route every local item refusal through
ClientLocal/SpewBox. - Replace canonical optimistic movement with retail-style pending projections.
- Close the vendor staged-row and owned-container input differences.
- Deepen hover/drop legality and finish exact status/failure text parity.
- 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::ListenToElementMessageat0x004E4D50UIElement_ItemList::BeginDragat0x004E32D0gmToolbarUI::HandleSelectionChangedat0x004BF380ItemHolder::GetObjectSplitSizein 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::BeginDragat0x004E32D0UIElement_ItemList::DragOverat0x004E3400UIElement_ItemList::AcceptDragObjectat0x004E4250UIElement_ItemList::HandleDropReleaseat0x004E4790ItemHolder::AttemptToPlaceInContainer_IsItemLegalat0x005870C0ItemHolder::AttemptToPlaceInContainer_IsContainerLegalat0x005879B0ItemHolder::WillItemFitInContainerat0x00587D60ItemHolder::IsDragIntoContainerAttemptLegalat0x00587E90
Drop target dispatch is ordered
Retail's three-dimensional drop/give dispatcher follows this practical order:
- Require an owned, movable source that is not currently in trade.
- Dropping on self means the main backpack.
- Target zero means ground placement or split-to-world.
- Try stack merge before treating the target as a container.
- A player target opens/routes through secure trade.
- A creature target uses give-item behavior.
- A container target must be open, unlocked, and legal.
- Vendor lists use their own staging rules.
- 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::AttemptMergeat0x005878F0ItemHolder::AttemptPlaceIn3Dat0x00588600docs/research/2026-07-13-retail-give-item-pseudocode.mddocs/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::DetermineUseResultat0x00588460ItemHolder::UseObjectat0x00588A80CPlayerSystem::UsingItemat0x00562F70gmPaperDollUI::ListenToElementMessageat0x004A5C30gmPaperDollUI::AcceptDragObjectat0x004A3B10gmPaperDollUI::AcceptPaperDollDragObjectat0x004A4A70docs/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::HandleMousePressesat0x004C40D0gmVendorUI::RecvNotice_ItemListBeginDragat0x004C4380VendorSellUI::DragItemAcceptableat0x004C20C0VendorSellUI::AcceptDragObjectat0x004C4F00VendorSellUI::ItemAttributesChangedat0x004C3FD0
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:
SelectionStateis the sole selected-object owner shared by inventory, paperdoll, vendor, world selection, and toolbar status.RuntimeInventoryStateowns external-container state, item-use transaction state, shared busy/request state, split/pending placement state, and borrows the canonicalClientObjectTable.SelectedObjectControllerprojects selection into the authored toolbar and owns the split-slider presentation.ItemInteractionControllerclassifies use/drop/give/move operations and sends requests.InventoryController,ExternalContainerController,PaperdollController, andVendorUiControllerown 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 toRuntimeCommunicationState.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:
- A production-composition test proving every local policy rejection reaches
ClientLocal/SpewBox and no gameplay failure depends on a toast callback. - Owned side-pack single/double-click tests proving one open action and no generic use request, including the second-click event order.
- Vendor staged Buying and Selling double-click removal tests with exact selection, totals, state cleanup, and message assertions.
- Vendor staged Selling drag-to-remove and selected-partial split-reset tests.
- Staged vendor-row right-click select/examine tests.
- 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.
- Owned coinstack toolbar-status parity using the byte-resolved exact format.
- Move and wield authoritative failure-composition tests.
- Transaction-observer tests proving canonical ownership does not change before acknowledgement while selection, capacity, vendor, and paperdoll borrow the same state.
- 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
toastrefusal 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.
- Single-click each item/list type; verify selection border and exact status.
- Right-click inventory, side-pack, external-container, paperdoll, browse, Buying, and Selling rows; verify selection and examine.
- Double-click ordinary usable, wearable, wieldable, and unusable items; verify one request and correct SpewBox refusal where applicable.
- Single- and double-click a carried side pack; verify one open action, no redundant generic use, and stable selection.
- Drag a full item between main pack and side pack; observe source/destination before response, after success, and after forced rejection.
- Fill a side pack, reject a move, free one slot, and retry immediately.
- Fill the main pack with loose items while carrying side packs; verify item and container capacities independently (#449).
- Merge full and partial stacks; verify selected split amount, target selection, source remainder, and full-target refusal text.
- Split to an inventory container, external container, creature, ground, and vendor; change selection while the request is pending.
- Drop full and partial stacks to ground; verify ghost/pending behavior, selected arriving object, rejection cleanup, and no duplicate item.
- Pick up from ground into a nearly full destination, then retry after freeing capacity.
- Equip by double-click and by paperdoll drag; test clothing conflict and weapon replacement. Verify source/paperdoll state before acknowledgement.
- Drag full and partial stacks to vendor Selling; verify exact quantities, temp-row replacement, totals, and #445 behavior.
- Double-click staged Buying and Selling rows to remove them; verify SpewBox text and state cleanup.
- Drag a staged Selling row to remove it; repeat with a partial split selected and verify refusal plus slider reset.
- Complete/cancel transactions in normal and alternate currency; verify currency balance refresh (#444) and selection/status stability.
- Repeat representative actions while another inventory request is pending, immediately after rejection, and immediately after reopening a container.
- 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.mddocs/research/2026-07-13-retail-item-drag-visuals-pseudocode.mddocs/research/2026-07-13-retail-give-item-pseudocode.mddocs/research/2026-07-23-retail-item-use-and-autowear-pseudocode.mddocs/research/2026-07-26-retail-inventory-placement-and-world-drop-pseudocode.mddocs/research/2026-08-08-slice6-vendor-transactions-research.mddocs/research/named-retail/acclient_2013_pseudo_c.txt
Primary current implementation surfaces:
src/AcDream.App/UI/UiRoot.cssrc/AcDream.App/UI/UiItemSlot.cssrc/AcDream.App/UI/ItemInteractionController.cssrc/AcDream.App/UI/Layout/InventoryController.cssrc/AcDream.App/UI/Layout/ExternalContainerController.cssrc/AcDream.App/UI/Layout/PaperdollController.cssrc/AcDream.App/UI/Layout/SelectedObjectController.cssrc/AcDream.App/UI/Layout/VendorUiController.cssrc/AcDream.App/UI/AutoWieldController.cssrc/AcDream.App/Composition/InteractionRetainedUiComposition.cssrc/AcDream.App/Rendering/GameWindow.cssrc/AcDream.Core/Items/ItemInteractionPolicy.cssrc/AcDream.Runtime/Gameplay/RuntimeInventoryState.cssrc/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.