Preserve prepublication local motion completion, require the PartArray enter-world lifecycle port, and balance deferred Use busy ownership across dispatch and cancellation. Reconcile the completed GameWindow connected gates and add regression coverage. Co-authored-by: Codex <codex@openai.com>
117 lines
4 KiB
Markdown
117 lines
4 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 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`.
|