fix(world): restore portal and distant use lifecycles

This commit is contained in:
Erik 2026-07-25 09:59:25 +02:00
parent c730632075
commit 1a14812c44
18 changed files with 401 additions and 196 deletions

View file

@ -3,8 +3,8 @@
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.
`You can only move or use one item at a time` gate from an accepted object/NPC
Use through its authoritative completion.
## Retail oracle
@ -44,10 +44,26 @@ There is no retail timeout and movement cancellation is not a substitute for
- 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.
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
@ -62,37 +78,15 @@ RequestUse(object, reservation):
reservation.CancelBeforeDispatch()
return
if object is carried or equipped by the player:
send Use immediately
transfer reservation to UseDone
return
if world identity/approach is invalid:
if a non-owned world object is not usable:
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
@ -117,10 +111,16 @@ 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`.
The 2026-07-25 Alcott trace captured the failure before correction:
```text
[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`.