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

@ -446,6 +446,57 @@ state through `CPhysicsObj::set_state` `0x00514DD0`. Separately,
`CPhysicsObj::UpdatePositionInternal` `0x00512C30` skips `CPartArray::Update`
while Hidden, so ordinary animation never advances behind the portal viewport.
### Destination simulation resumes before the portal viewport exits
The retail destination has two different release edges. They must not share
one acdream gate:
```text
SmartBox::DoSetState(player, visible):
set_state(...)
waiting_for_teleport = false
SmartBox::UseTime(next frame):
if not cell_manager.blocking_for_cells:
position_update_complete = true
CObjectMaint::UseTime()
CPhysics::UseTime()
gmSmartBoxUI::UseTime:
if SmartBox::teleport_in_progress() changed from true to false:
EndTeleportAnimation()
gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0:
teleportAnimState = TAS_TUNNEL_CONTINUE
teleportTransitionStartTime = Timer::cur_time
clear temporary FOV override
gmSmartBoxUI::UseTime, at least two seconds later:
TAS_TUNNEL_CONTINUE -> TAS_TUNNEL_FADE_OUT
...
TAS_TUNNEL_FADE_OUT -> TAS_WORLD_FADE_IN
hide portal viewport
show SmartBox world viewport
```
Therefore `TAS_TUNNEL_CONTINUE` is not a continued cell-loading freeze. The
destination world is already ticking behind the private portal viewport for at
least two seconds. That hidden interval is when the player pose, queued
Hidden/UnHide PhysicsScripts, attached emitters, and destination presentation
settle. Only the later `TUNNEL_FADE_OUT -> WORLD_FADE_IN` edge changes which
viewport is drawn.
Holding acdream's world-generation quiescence until the viewport swap is a
behavioral error: the first destination physics/effect tick occurs on the
first visible world frame, producing a character pop plus leftover recall or
purple materialization effects. The faithful split is:
- end world simulation/audio quiescence when the accepted destination is
materialized and `TunnelContinue` begins;
- retain the destination streaming/resource reservation and keep drawing only
the portal viewport until the later viewport-swap edge;
- keep LoginComplete on the final one-second `WorldFadeIn -> Off` edge.
ACE schedules lifestone recall teleport after the MotionTable-reported action
length. With the installed human DATs, the boundary is about 15.06024 seconds;
floating-point conversion can leave acdream at frame 149.999998 of the

View file

@ -55,11 +55,11 @@ both pass `IsUseable`.
For a direct-use item, `ItemHolder::UseObject` then sends
`CM_Inventory::Event_UseEvent`, increments the UI busy count, and calls
`CPlayerSystem::UsingItem`. That latter presentation function handles local
pickup/wield/container state; it does not require a carried source to have a
world position. Therefore an owned inventory object must cross the wire
directly. Only world objects participate in acdream's registered AD-27 local
approach compatibility seam.
`CPlayerSystem::UsingItem`. `Event_UseEvent` itself is the network send
(`0xF7B1/0x36`); the latter function handles local pickup/wield/container
presentation. Therefore neither an owned item nor a world object may be held
behind a client-side movement completion. ACE owns the authoritative approach
chain.
```text
AutoWearIsLegal(itemId, out alreadyWorn, silent):
@ -113,9 +113,10 @@ AutoWear(itemId, out alreadyWorn, silent):
ordinary Use request directly from its inventory identity; it is never sent
through the world-approach query. ACE remains authoritative for its spell
and consumption outcome.
- `SelectionInteractionController` keeps the AD-27 turn/approach seam only for
non-owned spatial objects. Owned self, equipped, and nested-container sources
have no live world pose and send immediately.
- `SelectionInteractionController` sends accepted ordinary Use immediately for
both owned and spatial objects. It does not install speculative local
TurnToObject/MoveToObject movement; ACE's authoritative movement packets use
the shared retail `MoveToManager`.
- `AutoWieldController` remains the shared inventory/paperdoll equip owner. It
resolves AutoWear blockers from `ClientObjectTable`'s retail-ordered
equipment projection and emits the retail literal through system chat.

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`.

View file

@ -17,12 +17,16 @@ retained-source re-gate is tracked separately below.
## Retail and protocol anchors
- `gmSmartBoxUI::UseTime @ 0x004D6E30`: the
- `SmartBox::DoSetState` clears `waiting_for_teleport` when the player becomes
visible. The next `SmartBox::UseTime` resumes `CObjectMaint` and `CPhysics`
while the portal viewport may still be active.
- `gmSmartBoxUI::UseTime @ 0x004D6E30`: the later
`TunnelFadeOut -> WorldFadeIn` edge hides the portal viewport, shows the
world viewport, releases cell blocking, and starts the final one-second
WorldFadeIn state.
- The later WorldFadeIn completion sends LoginComplete and ends logical
teleport state. Viewport release and protocol completion are distinct.
world viewport, releases the destination render reservation, and starts the
final one-second WorldFadeIn state.
- WorldFadeIn completion sends LoginComplete and ends logical teleport state.
Materialization, viewport release, and protocol completion are three
distinct edges.
- `gmPaperDollUI::RedressCreature @ 0x004A3BC0` creates the private inventory
object only when absent and a SmartBox player is available; temporary player
absence does not delete an existing private object.