# 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 while an object/NPC Use crosses acdream's close-range turn/approach boundary. ## 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 existing close-range compatibility seam is recorded as divergence AD-27: acdream may finish its local turn before emitting Use. That seam creates a real pre-wire interval which retail does not have, so it needs explicit ownership rather than pretending a server completion is already owed. ## Ported ownership pseudocode ```text BeginAcceptedUse(object): reservation = BusyOwner.Reserve() busyCount += 1 RequestUse(object, reservation) RequestUse(object, reservation): if session/identity/approach is invalid: reservation.CancelBeforeDispatch() return if a close-range turn must finish first: pending = (exact object incarnation, approach token, reservation) if movement installation fails: pending.reservation.CancelBeforeDispatch() return if SendUse(object) succeeds: reservation.MarkDispatched() else: reservation.CancelBeforeDispatch() OnNaturalApproachCompletion(pending): if pending incarnation is stale: pending.reservation.CancelBeforeDispatch() else if SendUse(pending.object) succeeds: pending.reservation.MarkDispatched() else: pending.reservation.CancelBeforeDispatch() OnApproachCancelledOrTargetRemoved(pending): pending.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 connected trace showed successful NPC Use packets and matching `UseDone(0)`. A managed heap snapshot after the final completion showed `ItemInteractionController._busyCount == 0`, no pending inventory request, and no pending auto-wield transaction. The permanent failure path was instead the pre-wire close-range branch: it incremented busy before local movement, but cancellation or stale identity discarded the pending send, leaving no packet and therefore no possible `UseDone`.