acdream/docs/research/2026-07-23-retail-use-busy-ownership-pseudocode.md

126 lines
4.7 KiB
Markdown

# Retail Use busy-reference ownership
Date: 2026-07-23
Scope: the retained UI busy cursor and the
`You can only move or use one item at a time` gate from an accepted object/NPC
Use through its authoritative completion.
## Retail oracle
Named Sept-2013 retail symbols:
- `ItemHolder::UseObject @ 0x00588A80`
- `ClientUISystem::Handle_Item__UseDone @ 0x00564900`
- `CM_Item::DispatchUI_UseDone @ 0x006A8510`
- `ClientUISystem::UsageCallback @ 0x00565B20`
`ItemHolder::UseObject` checks the 200 ms throttle and
`IsPlayerReadyToMakeInventoryRequest`, emits `CM_Inventory::Event_UseEvent`,
increments `ClientUISystem::m_cBusy`, and then calls
`CPlayerSystem::UsingItem`. The confirmation callback has the same order.
`CM_Item::DispatchUI_UseDone` routes event `0x01C7` to
`Handle_Item__UseDone`, which decrements exactly one busy reference and updates
the cursor when the count reaches zero.
Therefore the invariant is:
```text
one accepted Use request that reached the wire
owns one busy reference
until one authoritative UseDone arrives
```
There is no retail timeout and movement cancellation is not a substitute for
`UseDone` after dispatch.
## ACE and protocol cross-reference
- ACE `GameActionUseItem` and `Player_Use.TryUseItem` confirm that the Use
request owns the server-side operation and that every completed/rejected
operation returns `UseDone`.
- ACE `Player_Move.CreateMoveToChain` confirms that the server may approach a
distant target before executing that operation.
- holtburger `inventory/actions.rs` and `inventory/events.rs` independently
confirm the one Use action / one UseDone event protocol pair.
The connected 2026-07-25 regression trace proved why the old AD-27 Use branch
was incorrect: a pending local Ready animation could starve the speculative
TurnToObject, so no Use packet ever reached ACE. Retail has no such dependency.
`ItemHolder::UseObject` emits `CM_Inventory::Event_UseEvent` first; the event
packs and sends opcode `0x36` in `CM_Inventory::Event_UseEvent @ 0x006AC3B0`.
Only then does retail call `CPlayerSystem::UsingItem`. ACE owns any required
authoritative `CreateMoveToChain`. The Use half of AD-27 is therefore retired;
its remaining client completion adaptation applies only to pickup.
The immediate-send rerun then proved a distinct presentation-owner defect:
ACE sent the correct MoveTo and acdream resolved its target, but a login Ready
node remained at the head of the local `MotionInterpreter`. During login
hydration the player's logical animation runtime remained current while its
spatial projection was temporarily withdrawn. The presenter consumed the
zero-tick PartArray motion but its spatial-only guard withheld the
`MotionDoneTarget`, so the interpreter never popped that node. The correction
binds completion to the exact logical live-record animation owner and drains
the pre-controller PartArray queue before publishing a candidate interpreter.
The connected distant-NPC gate then turned, walked, sent no duplicate Use, and
completed through authoritative UseDone.
## Ported ownership pseudocode
```text
BeginAcceptedUse(object):
reservation = BusyOwner.Reserve()
busyCount += 1
RequestUse(object, reservation)
RequestUse(object, reservation):
if session is invalid:
reservation.CancelBeforeDispatch()
return
if a non-owned world object is not usable:
reservation.CancelBeforeDispatch()
return
if SendUse(object) succeeds:
reservation.MarkDispatched()
else:
reservation.CancelBeforeDispatch()
reservation.MarkDispatched():
// Ownership transfers to the server completion. Keep busyCount unchanged.
make later local cancellation inert
reservation.CancelBeforeDispatch():
// No packet exists, therefore no UseDone can arrive for this reservation.
busyCount -= 1
make duplicate cancellation inert
OnUseDone(error):
busyCount -= 1
if busyCount == 0:
update cursor
OnSessionReset():
invalidate the reservation generation
busyCount = 0
```
The reservation is idempotent and generation-bound. A late callback from a
retired session cannot decrement a new session's busy reference.
## Regression evidence
The 2026-07-25 Alcott trace captured the failure before correction:
```text
[input] UseSelected Press
[autowalk-pending] count=1 nodes=[0x41000003/ctx=0/jump=0x0]
[autowalk-target] object=0x8000D085 status=Ok ...
```
There was no `[B.4b] use`, no `UseDone`, and therefore no possible
authoritative release of the busy reference. Focused tests now pin that close
and far world Use both dispatch immediately, never install speculative local
movement, never duplicate on a later movement callback, and retain busy
ownership until exactly one `UseDone`.