4.7 KiB
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 @ 0x00588A80ClientUISystem::Handle_Item__UseDone @ 0x00564900CM_Item::DispatchUI_UseDone @ 0x006A8510ClientUISystem::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:
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
GameActionUseItemandPlayer_Use.TryUseItemconfirm that the Use request owns the server-side operation and that every completed/rejected operation returnsUseDone. - ACE
Player_Move.CreateMoveToChainconfirms that the server may approach a distant target before executing that operation. - holtburger
inventory/actions.rsandinventory/events.rsindependently 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
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:
[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.