126 lines
4.7 KiB
Markdown
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`.
|