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

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 @ 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:

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

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.