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.
745 lines
36 KiB
Markdown
745 lines
36 KiB
Markdown
# 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:
|
||
|
||
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.
|