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.
This commit is contained in:
parent
0c699240e0
commit
f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions
|
|
@ -337,44 +337,18 @@ nothing wrong" surprise, matching the register's existing framing of the
|
|||
|
||||
### B.2 — Double-click
|
||||
|
||||
**No dedicated double-click-to-buy mechanism was found for vendor shop
|
||||
items.** Evidence, not absence-of-search:
|
||||
**Corrected 2026-08-26:** the original symbol-name search missed the real
|
||||
mechanism. Retail handles this inside the general
|
||||
`gmVendorUI::HandleMousePresses @ 0x004C40D0`; it does not require a separately
|
||||
named `CheckForDoubleClick` function. In the Items-list branch, the retail
|
||||
double-click condition directly calls `gmVendorUI::BuySingleItem` for the
|
||||
clicked row. The same function also owns staged Buying/Selling removal and
|
||||
their `ClientLocal` feedback.
|
||||
|
||||
- `gmVendorUI::ListenToElementMessage` (`pc:204260-204309`, full function
|
||||
read) dispatches on message id 1 (button click →
|
||||
`HandleButtonClicks`), 7 (dropdown selection change), `0x2c` (page
|
||||
change), `0x15` (drop release), and `0x1c` (routes to
|
||||
`HandleMousePresses` only when `m_itemsUI != 0`) — there is no distinct
|
||||
"double-click" message id handled at the panel level.
|
||||
- The base list class `UIElement_ItemList` (every method enumerated via
|
||||
`docs/research/named-retail/symbols.json`, ~50 symbols) has
|
||||
`HandleSingleSelection`, `HandleTargetedUseLeftClick`,
|
||||
`ItemList_SetSelectedItem`, `ItemList_OpenContainer` (for double-clicking
|
||||
a CONTAINER item specifically — opening it, not buying), but **no
|
||||
generic double-click handler** and no vendor-specific one either.
|
||||
- Other retail panels DO have an explicit, separately-named double-click
|
||||
handler when the mechanism exists — e.g. `gmContractsUI::CheckForDoubleClick`
|
||||
(`0x00497A10`), `gmPageListUI::CheckForDoubleClick` (`0x00493140`). No
|
||||
`gmVendorUI::CheckForDoubleClick` or `VendorItemsUI::CheckForDoubleClick`
|
||||
symbol exists in the 18,366-function named table.
|
||||
|
||||
**Conclusion:** retail's confirmed vendor-item interaction model is
|
||||
single-click-to-select (→ drives the global `ACCWeenieObject::selectedID`,
|
||||
B.3 below) plus an explicit Buy/Add button press. There is no evidence
|
||||
retail supports double-click-to-buy on the shop list. The user's
|
||||
expectation likely carries over from inventory-panel muscle memory
|
||||
(double-click = use/equip elsewhere in retail) — but the vendor "Items"
|
||||
list is not that panel. **This is flagged as an open question for the
|
||||
contract, not resolved unilaterally**: per the project's
|
||||
no-invented-mechanisms discipline, do not silently add a double-click-buy
|
||||
shortcut and call it retail-faithful. The retail-faithful, fully-evidenced
|
||||
fix for "double-click does nothing" is: (a) make single-click meaningfully
|
||||
select (today it only sets a private field with no visible effect — see
|
||||
B.3), and (b) make the Buy button actually work. If the user still wants a
|
||||
double-click shortcut after seeing single-click+Buy work, that is a
|
||||
deliberate, flagged acdream UX addition on top of retail, not a retail port
|
||||
— call it out explicitly in the commit/register the way AP-116
|
||||
(Particle Range) or similar user-directed deviations are recorded.
|
||||
**Conclusion:** browse-row double-click-to-buy is verbatim retail behavior.
|
||||
The acdream binding is a port, not an optional modernization. The previous
|
||||
absence-of-symbol inference was false and is superseded by the direct function
|
||||
body.
|
||||
|
||||
### B.3 — The quantity slider
|
||||
|
||||
|
|
@ -735,10 +709,9 @@ concretely unblocked by the one before it; skipping ahead reproduces the
|
|||
polish, not correctness — the server is authoritative either way) and
|
||||
file it as a fast follow-up if the user notices the round-trip lag on a
|
||||
refused purchase.
|
||||
2. **Double-click** — no retail mechanism found (B.2). Ask the user
|
||||
directly whether they want a deliberate acdream-only double-click
|
||||
shortcut once single-click-select + Buy-button-works is verified live,
|
||||
rather than assuming yes and inventing behavior.
|
||||
2. **Double-click — RESOLVED 2026-08-26.** Retail's
|
||||
`gmVendorUI::HandleMousePresses @ 0x004C40D0` directly buys a browse row on
|
||||
double-click. Keep this behavior and its staged-row siblings.
|
||||
3. **Where does the vendor-owned split-exempt-mask predicate live** — C.1's
|
||||
design question: fold into `SelectedObjectController` directly (it
|
||||
already owns the seeding logic, would need a `Func<uint,bool>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ Research lane D of the settings-track campaign
|
|||
questions **Q5** (Configure Keyboard: retail's keymap UI + storage) and
|
||||
**Q6** (what every Gameplay Options tab button does).
|
||||
|
||||
> **2026-08-26 implementation addendum:** the report below describes the
|
||||
> pre-OP8 state and its design choices at that date. acdream has now shipped
|
||||
> Option C end to end: all 306 installed-DAT rows, retail conflicts/capture,
|
||||
> and real named `.keymap` Load File / Save As/startup/shutdown persistence.
|
||||
> See `docs/research/2026-08-26-retail-keyboard-routing-audit.md`; AP-202 is
|
||||
> retired.
|
||||
|
||||
**Report only.** No repo code was changed. Every retail claim below carries
|
||||
a named symbol + address from the Sept 2013 EoR PDB-paired build. The
|
||||
PDB/binary pairing was verified first:
|
||||
|
|
|
|||
131
docs/research/2026-08-26-combined-client-parity-gate.md
Normal file
131
docs/research/2026-08-26-combined-client-parity-gate.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# Combined client parity owner gate
|
||||
|
||||
**Date prepared:** 2026-08-26
|
||||
|
||||
**Run:** OWNER-COMPLETE 2026-08-26 on one exact Release binary.
|
||||
|
||||
**Scope:** #443–#450 plus the complete inventory/vendor interaction audit.
|
||||
|
||||
**Outcome:** Sections A–C and E–F passed, including the mid-drag cursor-icon
|
||||
re-test added during the round. Issues #444–#450 are owner-accepted. Section D
|
||||
failed: the private paperdoll remained missing, so #443 stays open as the only
|
||||
surviving defect. Tested executable SHA-256:
|
||||
`173989F3C85C05C0746D628CDF9C6194F6A5E3EFD4597806BF83417483C37B42`.
|
||||
|
||||
Keep the client log for the whole run and take a screenshot for any visual or
|
||||
text mismatch. For every refused item action, record the cursor color, exact
|
||||
SpewBox line, and whether the item visibly moved before the refusal.
|
||||
|
||||
## A. Keyboard, camera, combat input, and relog — #446/#450
|
||||
|
||||
1. In Configure Keyboard, bind Move Forward to bare Shift while Toggle
|
||||
Walk/Run already owns it. Verify the retail conflict dialog appears and the
|
||||
chosen resolution is honored. Repeat with a known allowed shared chord.
|
||||
2. Apply a changed binding, close/reopen Options, then restart the client.
|
||||
Verify it survives. Test Revert, Defaults, Cancel, and Load/Save `.keymap`.
|
||||
3. Press regular Enter: chat input must focus. Press keypad Enter outside chat:
|
||||
it must perform only its configured camera action and must not focus chat.
|
||||
4. Press bare Escape through the retail ladder: cancel target/focus/selection
|
||||
first, then toggle Gameplay Options. It must never enter a developer orbit
|
||||
or bird's-eye mode. Preserve the approved mouse-wheel zoom range.
|
||||
5. Hold End, Page Down, or Delete in melee mode. The bar must charge while the
|
||||
key is held and attack only on release, using the selected height.
|
||||
6. Shift+Escape to character selection, immediately re-enter, and verify the
|
||||
destination loads and exits portal space. Repeat twice in one process.
|
||||
|
||||
## B. Chat and combat text — #447/#448
|
||||
|
||||
1. Run `@acecommands`. Every non-empty server line must be visible; an
|
||||
oversized response must retain the newest complete lines, not blank the
|
||||
transcript.
|
||||
2. Run `@acehelp acecommands` and send ordinary chat afterward. Verify normal
|
||||
text, filtering, and scrolling remain intact.
|
||||
3. Land ordinary and critical melee hits. Outgoing lines must match retail
|
||||
wording and punctuation and contain no acdream-added percentage.
|
||||
|
||||
## C. Selection, use, containers, movement, and splitting — #445/#449
|
||||
|
||||
Use a normal item, unusable item, wearable, weapon, two mergeable stacks, a
|
||||
Pyreal stack, one side pack, a full main pack, a full side pack, an open
|
||||
external container, and a creature/player target.
|
||||
|
||||
1. Single-click and right-click inventory, side-pack, external-container, and
|
||||
paperdoll items. Verify one global selection, stable highlight, status text,
|
||||
and right-click examination.
|
||||
2. Select an owned Pyreal stack. The toolbar must read
|
||||
`<stack> <appropriate name> (of <total carried Pyreals>)`, with no comma
|
||||
insertion added by acdream.
|
||||
3. Single- and double-click a carried side pack. It must open on press, issue
|
||||
no generic item-use request, and remain stably selected.
|
||||
4. Double-click usable, unusable, wearable, and wieldable items. Verify one
|
||||
action. Any local refusal must appear once in SpewBox, not in normal chat.
|
||||
5. Move a full item between main pack and side pack. Before acknowledgement,
|
||||
the source must remain canonical with retail's waiting/ghost presentation;
|
||||
after success it appears only at the destination. Force one rejection and
|
||||
verify no duplicate, disappearance, or speculative capacity change.
|
||||
While holding the item under the cursor, move across the inventory and wait
|
||||
through several ordinary object updates: the cursor icon must remain visible
|
||||
until release. This re-gates the mid-drag procedural-refresh fix found during
|
||||
the first 2026-08-26 owner pass.
|
||||
6. Fill the main pack, observe a rejected move, drop one loose item, then move
|
||||
an item from a side pack into the freed slot immediately. It must accept
|
||||
without reopening the inventory window (#449).
|
||||
7. From a stack of 10, select 2 and split into an empty main-pack slot, an open
|
||||
side pack, and an external container. Each successful result must be 8+2.
|
||||
8. Merge full and partial stacks. Verify selected quantity, target selection,
|
||||
target-cap clamp, source remainder, and exact refusal text for a full target.
|
||||
9. Drop full and partial stacks to the world, then pick them up. Verify pending
|
||||
visuals, authoritative commit, failure cleanup, and no duplicate object.
|
||||
10. Give a full and partial stack to a creature; drag onto another player and
|
||||
verify secure-trade routing. Hover rejection must stay silent; release
|
||||
rejection must print the exact ClientLocal reason.
|
||||
11. Equip once by double-click and once by paperdoll drag. Test a clothing
|
||||
conflict and weapon replacement. Canonical inventory/paperdoll ownership
|
||||
must not change before the authoritative response.
|
||||
|
||||
## D. Private paperdoll viewport — #443
|
||||
|
||||
1. From a fresh process, open inventory and assess one monster and one player.
|
||||
2. The animated doll must be present on first open, not appear only after a
|
||||
delay. Close/reopen each view several times and change equipment once.
|
||||
3. Record #443 independently if the viewport is late or missing even when the
|
||||
underlying equip/inventory transaction is correct.
|
||||
|
||||
## E. Vendor parity and alternate currency — #444/#445
|
||||
|
||||
1. On the browse list, single-click selects, right-click examines, and
|
||||
double-click buys exactly one/current-slider unit through the normal retail
|
||||
purchase path.
|
||||
2. Add a stack quantity greater than one to Buying. Double-click its staged
|
||||
row: remove exactly one unit and print
|
||||
`Removing <name> from shopping list` once in SpewBox.
|
||||
3. Stage an owned item in Selling. Right-click examines it; double-click
|
||||
removes the whole staged entry and prints the same removal form.
|
||||
4. Drag a staged Selling row: it must unstage. Repeat after selecting only part
|
||||
of its stack: SpewBox must print
|
||||
`You cannot split items from this panel` and the slider must reset to max.
|
||||
5. From an owned stack of 10, select 2 and drag into Selling. Verify the
|
||||
temporary row resolves to the new authoritative stack of 2, Sell All sells
|
||||
exactly 2, and 8 remain (#445).
|
||||
6. At an alternate-currency vendor, note holdings in the Items cost sentence
|
||||
and Buying/Selling purse lines. Buy once: every visible holding must decrease
|
||||
immediately and remain correct after authoritative inventory refresh,
|
||||
tab changes, and vendor reopen (#444).
|
||||
7. Buy All once with enough currency and once without enough. The first uses
|
||||
the refreshed balance; the second prints retail's insufficient-funds notice
|
||||
and sends no purchase.
|
||||
|
||||
## F. Re-entrant and lifecycle stress
|
||||
|
||||
1. Change selection during a pending split, close an external container during
|
||||
a pending move, and retry immediately after a refusal.
|
||||
2. Attempt a second inventory operation while one request is pending. Verify a
|
||||
clean retail refusal/no-op, never duplicated wire action or stuck busy state.
|
||||
3. Log out or portal with a recently completed interaction, re-enter, and
|
||||
verify pending projections and the request ledger converge to zero.
|
||||
|
||||
## Pass rule
|
||||
|
||||
The pass rule was satisfied for #444–#450 on the exact binary recorded above.
|
||||
#443 remains open by itself because the transaction rows passed and the failure
|
||||
was confined to private viewport residency.
|
||||
43
docs/research/2026-08-26-issues-444-445-447-test-script.md
Normal file
43
docs/research/2026-08-26-issues-444-445-447-test-script.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Issues #444, #445, #447 — consolidated owner gate
|
||||
|
||||
Run these checks together on the next connected test build. They deliberately
|
||||
require no special probes; record the client log and one screenshot per
|
||||
section. If a split fails, also record the exact visible error text.
|
||||
|
||||
## #444 — alternate-currency vendor balance
|
||||
|
||||
1. Open a vendor that accepts an alternate currency and note the amount shown
|
||||
in both the Items cost sentence and the Buying-tab purse line.
|
||||
2. Buy one item.
|
||||
3. Confirm both visible amounts decrease immediately and remain correct after
|
||||
the server refresh. Close/reopen the tab and vendor and confirm the amount
|
||||
does not bounce back to the old snapshot.
|
||||
4. Buy All once with enough currency, then once without enough. Confirm the
|
||||
first uses the refreshed holding and the second shows retail's insufficient-
|
||||
money notice without sending a purchase.
|
||||
|
||||
## #445 — inventory and vendor partial stack splits
|
||||
|
||||
1. In the main pack, select a stack of 10, set the slider to 2, and drag it to
|
||||
an empty main-pack slot while at least one side bag is equipped.
|
||||
2. Confirm the source becomes 8 and a new stack of exactly 2 appears at the
|
||||
chosen loose-item position; no error should appear.
|
||||
3. Repeat into an open side bag and confirm the same 8+2 result.
|
||||
4. Reset to a stack of 10, select 2, and drag it onto the vendor Selling list.
|
||||
Confirm the client prints `Splitting the <name> before selling them`, then
|
||||
the staged row resolves to the new stack of 2 rather than the source stack.
|
||||
5. Press Sell All and confirm exactly 2 are sold and 8 remain.
|
||||
|
||||
## #447 — `@acecommands` multiline response
|
||||
|
||||
1. Run `@acecommands` on the test account.
|
||||
2. Confirm command text is visible, consecutive server lines are readable,
|
||||
and there is no screen of blank chat rows.
|
||||
3. Scroll through the retained result. A response beyond retail's 10,000-
|
||||
character transcript cap should retain the newest complete command lines
|
||||
instead of blanking the entire response.
|
||||
4. Run `@acehelp acecommands` and one ordinary chat command afterward; confirm
|
||||
their text and normal chat presentation remain intact.
|
||||
|
||||
Pass all three sections on one exact binary, then mark #444/#445/#447
|
||||
owner-accepted together.
|
||||
745
docs/research/2026-08-26-retail-inventory-interaction-audit.md
Normal file
745
docs/research/2026-08-26-retail-inventory-interaction-audit.md
Normal file
|
|
@ -0,0 +1,745 @@
|
|||
# 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.
|
||||
115
docs/research/2026-08-26-retail-keyboard-routing-audit.md
Normal file
115
docs/research/2026-08-26-retail-keyboard-routing-audit.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Retail keyboard defaults and routing audit — 2026-08-26
|
||||
|
||||
## Verdict
|
||||
|
||||
The code gate for #446 now covers all 306 user-bindable rows in the installed
|
||||
Sept-2013 EoR ActionMap. Each row has a distinct `InputAction`, appears enabled
|
||||
in Configure Keyboard, persists through retail-compatible named `.keymap`
|
||||
profiles, and reaches
|
||||
a concrete subsystem consumer. The exact installed-DAT default chord set has
|
||||
zero exceptions. The remaining gate is a connected visual/behavior pass and a
|
||||
fresh-process persistence check.
|
||||
|
||||
The approved acdream extension is deliberately retained: mouse-wheel chase
|
||||
zoom may pull back to 40 m. It does not change the retail keyboard defaults or
|
||||
the keypad camera actions.
|
||||
|
||||
## Oracles
|
||||
|
||||
- `docs/research/named-retail/retail-default.keymap.txt` and installed
|
||||
`client_portal.dat` ActionMap DID `0x26000000`: the 306 rows, default chords,
|
||||
activation types, input contexts, and `ConflictingMaps` relationships.
|
||||
- Installed MasterInputMaps `0x14000000` and `0x14000002`: non-bindable system
|
||||
and mouse commands.
|
||||
- `ClientUISystem::OnAction @0x00564B90`: Escape priority.
|
||||
- `CInputManager_WIN32::GetNameFromKey_Internal @0x00687800` and
|
||||
`ControlNameMapper::LoadSemantics`: displayed keyboard/mouse names.
|
||||
- `ACCmdInterp::InitializeEmoteInputActionHash @0x0058B510`: all 87 emote
|
||||
action-to-motion mappings.
|
||||
- `CPlayerSystem::SelectNext @0x0055F9A0`: selection-cycle filtering and
|
||||
opened-corpse behavior.
|
||||
|
||||
## Exact ActionMap coverage
|
||||
|
||||
| Retail input map | Rows | Consumer |
|
||||
|---|---:|---|
|
||||
| Movement | 14 | Runtime movement owner, including four postures |
|
||||
| Camera + alternate camera | 22 | Held camera input, presets, alternate-scope modifier, instant mouse look |
|
||||
| Combat + melee + missile + magic | 32 | Runtime combat attack owner and spellcasting controller, including spell slots 1–12 |
|
||||
| Emotes | 87 | Exact retail raw-motion table and Runtime `ExecuteMotion` |
|
||||
| Item selection | 26 | Selection controller/query and canonical inventory interaction state |
|
||||
| UI | 42 | Retained panels, screenshot, help/plugin result, logout, and selection commands |
|
||||
| Chat + chat-entry toggle | 7 | Retained chat entry/reply/command routes |
|
||||
| Quickslots | 28 | Toolbar use/select/create routes, including slots 10–18 |
|
||||
| Character settings | 48 | Exact `CharacterOptionId` bit toggle through Runtime |
|
||||
| **Total** | **306** | **306 distinct live identities** |
|
||||
|
||||
The low MasterInputMap entries such as bare Escape and raw mouse event
|
||||
commands are intentionally not Configure Keyboard rows in retail and are not
|
||||
counted among the 306. Unknown rows from a future DAT can still round-trip in
|
||||
the compatibility sibling store, but the installed EoR DAT has no such row and
|
||||
shows no dimmed/store-only keyboard entry.
|
||||
|
||||
## Behavior completed
|
||||
|
||||
- Defaults are an exact installed-DAT transcription, including bare
|
||||
`LeftShift`; device, modifier, activation, and scope all match.
|
||||
- Primary and alternate camera maps remain distinct rebind targets even where
|
||||
retail reuses an action id. The alternate modifier changes the active camera
|
||||
scope without aliasing saved bindings.
|
||||
- Same physical chord may fire each distinct retail action allowed by the
|
||||
ActionMap. In particular, the authored Alt+1..4 chat/UI and quickslot rows
|
||||
multicast instead of one silently replacing the other.
|
||||
- Rebind conflicts use the DAT `ConflictingMaps` table. The shared melee,
|
||||
missile, and magic key cluster remains legal; true conflicts still prompt.
|
||||
- Capture accepts keyboard keys, modifier-only bindings, and mouse buttons.
|
||||
Physical modifier self-bits are normalized, so binding LeftShift does not
|
||||
accidentally become Shift+LeftShift. Mouse button names use retail's
|
||||
`DIMOFS_BUTTON0..7` semantics table. Unsupported joystick and left/right
|
||||
mouse inputs keep the instruction dialog open and re-arm capture.
|
||||
- Setting the chord already present on the same row is a no-op. New chords use
|
||||
retail's dense two-slot insertion rule, and conflicts use priority dialogs
|
||||
with the exact installed-DAT singular/plural and non-bindable text.
|
||||
- Apply/OK, Revert, Defaults, Cancel, explicit unbinding, schema migration,
|
||||
and startup persistence are covered. Revert is enabled only while dirty;
|
||||
OK avoids rewriting an unchanged file.
|
||||
- Load File and Save As use retail's type-7 menu/type-5 text-entry dialogs,
|
||||
PFile bracket-text grammar, filename normalization, overwrite/read-only
|
||||
handling, `Documents\Asheron's Call\*.keymap` directory, selected-profile
|
||||
preference, startup load, and graceful-shutdown rewrite. The portable JSON
|
||||
file remains only as an acdream-host-command compatibility mirror.
|
||||
- Escape follows retail's priority: finish jump charge, release focused UI,
|
||||
stop movement/repeat attack, cancel target mode, clear selection, then
|
||||
toggle the authored Gameplay Options page. It never exits player mode or
|
||||
exposes the orbit/developer camera. Shift+Escape reaches the normal logout
|
||||
gate.
|
||||
- Selection cycling applies retail's containment, cloaking, radar, attackable,
|
||||
fellow, vendor, environment, combat-mode, and opened-corpse rules.
|
||||
Opened-corpse history lives for the session and retires on object deletion.
|
||||
- Screenshot, help, and plugin actions are consumed. Missing separately
|
||||
shipped retail help/plugin surfaces report an honest chat/system result
|
||||
rather than doing nothing.
|
||||
|
||||
## Automated verification
|
||||
|
||||
- App: 6,413/6,413 passed.
|
||||
- Core: 4,713/4,713 passed.
|
||||
- Runtime: 1,849/1,849 passed.
|
||||
- UI.Abstractions: 879/879 passed.
|
||||
- Installed-DAT identity/default conformance and the authored Configure
|
||||
Keyboard mount pin all 306 rows.
|
||||
|
||||
The `.keymap` codec parses the committed real retail file and round-trips all
|
||||
306 user-bindable identities, including low-bit Shift/Ctrl/Alt/Win modifiers,
|
||||
DirectInput controls, fixed Escape/system/edit/pointer maps, and the 48
|
||||
CharacterOption action names. AP-202 is retired. The connected gate could not
|
||||
be run because no local ACE endpoint was listening on UDP port 9000.
|
||||
|
||||
## Connected acceptance gate
|
||||
|
||||
Use the installed EoR DATs and the normal owner-gate pak. In Configure
|
||||
Keyboard, verify that all rows are enabled and that a key, modifier-only chord,
|
||||
and mouse button can each be rebound. Exercise representative movement,
|
||||
camera, melee/missile/magic, emote, selection, panel, chat, quickslot, and
|
||||
character-option actions. Verify conflict prompt, Cancel, Revert, Defaults,
|
||||
Apply, and OK, then restart the process and confirm the applied bindings remain.
|
||||
Loading…
Add table
Add a link
Reference in a new issue