fix(world): complete recall before teleport hide

Match retail's update ordering so object animation, particles, and scripts advance before inbound teleport state is applied. Separate input-originated movement from post-network autonomous position output, and reconcile presentation without a second physics tick so recall cannot resume after arrival.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-16 06:37:29 +02:00
parent dded9e6b17
commit 75acae02d6
13 changed files with 1068 additions and 332 deletions

View file

@ -844,8 +844,17 @@ Repository links:
4. No client-side impact, explosion, damage, or deletion is synthesized.
5. Presentation frame ordering is fixed: advance animation/root motion,
publish indexed parts, compose equipped children, drain animation hooks,
tick owner PhysicsScripts, refresh attached emitters/lights, then advance
particle simulation and draw.
refresh attached emitters/lights, advance particle simulation, tick owner
PhysicsScripts, dispatch inbound network events, run the post-inbound
command-interpreter position check, reconcile spatial derivatives without
advancing time, then draw. The explicit network boundary ports
`SmartBox::UseTime @ 0x00455410`: `CPhysics::UseTime` updates every object
(including its particle and script managers) before SmartBox drains its
inbound queue, after which `CommandInterpreter::UseTime @ 0x006B3BF0`
evaluates AutonomousPosition. Within each object,
`CPhysicsObj::UpdateObjectInternal @ 0x005156B0` updates
`ParticleManager` before `ScriptManager`, so a particle created by a script
begins simulation on the following object frame.
Retail stages animation hooks inside `CSequence::update`, then executes them
from `CPhysicsObj::process_hooks` before the new root is committed. The same

View file

@ -276,10 +276,45 @@ The viewer reset copies the player's complete position into both `viewer` and
boom outward through the same damped and swept path used after camera
collision. Reusing the source-world viewer is not retail behavior.
## Recall completion versus Hidden
The retail teleport hook does not clear the character's animation sequence.
The arrival presentation is instead hidden by the portal scene and its final
world view-plane transition while the normal motion/update stream settles. Therefore acdream
must not add an arrival-only animation reset.
Recall completion is instead guaranteed by the enclosing frame order:
```text
Client.UseTime:
Timer.update_time()
ClientNet.UseTime()
process packet controller/cache/UI work
SmartBox.UseTime()
SmartBox.Draw()
SmartBox.UseTime:
CObjectMaint.UseTime()
CPhysics.UseTime() // advances PartArray, particles, scripts
GameTime/LScape/Ambient.UseTime()
while inbound queue not empty:
DispatchSmartBoxEvent() // SetState/Hidden lands here
CommandInterpreter.UseTime() // ShouldSendPositionEvent -> AP
```
The exact call sites are `Client::UseTime` `0x00411C40` and
`SmartBox::UseTime` `0x00455410`, and `CommandInterpreter::UseTime`
`0x006B3BF0`. `SmartBox::DoSetState` `0x004520D0`
applies Hidden directly and does not advance or reset animation. Separately,
`CPhysicsObj::UpdatePositionInternal` `0x00512C30` skips `CPartArray::Update`
while Hidden. Therefore the ordering boundary is load-bearing: the current
frame's animation must advance before an inbound Hidden transition can freeze
it.
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
0..149 recall node if it dispatches Hidden before the current object tick. On
UnHide that microscopic remainder becomes a visible destination-side recall
tail. Retail's object-before-network order retires the node first. The faithful
fix is that frame barrier, never an arrival-only Ready/reset command and never
advancing PartArray while Hidden.
## acdream integration translation
@ -303,6 +338,23 @@ must not add an arrival-only animation reset.
strand the transit, while canonical physics retains its own retail gates.
- Destination placement resets the retail chase camera's published and sought
positions to the player before the normal update path resumes.
- `RetailLiveFrameCoordinator` owns the `SmartBox::UseTime` phase barrier:
local/remote object and projectile advancement, final pose/hook publication,
attached emitters/lights, particle simulation, and PhysicsScripts all precede
inbound session dispatch in that retail order; rendering consumes the resulting
coherent snapshot.
- `RetailLocalPlayerFrameController` owns the local player's exactly-once
object tick. Input-originated movement and jump packets are serialized from
its pre-network result; F751 or ForcePosition received later in the frame
cannot relocate or replay those one-shot commands. After inbound dispatch,
`CommandInterpreter::UseTime @ 0x006B3BF0` evaluates AutonomousPosition from
the current controller frame, matching its exact retail slot.
- After inbound dispatch, a non-time-advancing reconciliation composes equipped
children and refreshes emitter/light anchors from the authoritative root. It
does not rerun animation, scripts, particles, or fades. A player first created
by that inbound pass is likewise projected without receiving a late physics
tick; its first normal object tick occurs on the next update.
This is update-loop ordering, not portal-specific animation logic.
- Destination residency remains acdream's asynchronous adaptation. It supplies
the same `EndTeleportAnimation` edge retail receives when its blocking cell
load completes; it does not alter presentation ordering.