fix(runtime): restore interaction completion ownership
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>
This commit is contained in:
parent
5c955c36ec
commit
f9736ece6c
26 changed files with 675 additions and 68 deletions
117
docs/research/2026-07-23-retail-use-busy-ownership-pseudocode.md
Normal file
117
docs/research/2026-07-23-retail-use-busy-ownership-pseudocode.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# 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`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue