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

745 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```text
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.