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

@ -206,6 +206,7 @@ src/
LiveEntityRuntime.cs -> canonical identity/state/body/projectile/spatial ownership
LiveEntityPresentationController.cs -> Hidden/NoDraw/effect/collision presentation
LiveEntityTeardown.cs -> failure-isolated multi-owner lifecycle drain
RetailLiveFrameCoordinator.cs -> object/physics-before-network frame barrier
Rendering/
GameWindow.cs -> still owns too much runtime wiring
TerrainModernRenderer.cs -> mandatory bindless+MDI terrain path
@ -642,7 +643,19 @@ canonical local physics controller and chase-camera handoff.
The destination residency gate supplies retail's `EndTeleportAnimation` edge
once asynchronous streaming is ready. Player placement remains authoritative
and separate from presentation; arrival resets camera viewer state but does
not reset the character animation sequence. See
not reset the character animation sequence. `RetailLiveFrameCoordinator`
matches `SmartBox::UseTime @ 0x00455410`: it advances local and remote object/
projectile runtime, composes final animated/equipped poses, drains animation
hooks, then advances particles before owner scripts and inbound network dispatch.
`RetailLocalPlayerFrameController` owns the player's exactly-once tick and sends
input-originated movement/jump output on the object side of that barrier. After
inbound dispatch, the retail command-interpreter phase evaluates periodic
AutonomousPosition from current authoritative state, then a non-advancing spatial
reconciliation updates equipped children and live effect/light anchors. Consequently a
teleport Hidden transition cannot freeze the last fraction of recall and replay
it after UnHide, and ForcePosition/F751 cannot combine old one-shot input with a new cell.
The draw pass consumes that completed update snapshot and never advances live
animation independently. See
`docs/research/2026-07-15-retail-portal-space-pseudocode.md`.
## Roadmap Model

View file

@ -593,7 +593,7 @@ Research: R9 + R12 + R13.
- **✓ SHIPPED — G.1 — Sky + weather + day-night.** Deterministic client-side from Portal Year time. Sky dome geometry + keyframe gradients + rain/snow particles. See `r12-weather-daynight.md`. Full data + visual stack shipped: Region dat loader, keyframe interp, WeatherSystem with 5-kind PDF + transitions + storm flashes, WorldSession→WorldTimeService sync via ConnectRequest+TimeSync, SkyRenderer with sky-object arcs + UV scroll, rain/snow billboard renderer, F7/F10 debug cycle keys.
- **✓ SHIPPED — G.2 — Dynamic lighting.** 8-light D3D-style fixed pipeline. Hard-cutoff at Range, no attenuation inside. Cell ambient. Shader UBO per frame. See `r13-dynamic-lighting.md`. SceneLightingUbo std140 at binding=1 feeds terrain + mesh + mesh_instanced + sky shaders. LightingHookSink auto-registers Setup.Lights at entity stream-in, flips IsLit on SetLightHook, unregisters on landblock unload.
- **Indoor portal-based cell tracking (follow-up to Indoor walking Phase 1 / issue #87).** Replace `PhysicsDataCache.TryFindContainingCell` AABB containment with retail's `CObjMaint::HandleObjectEnterCell` portal traversal. When the player crosses a cell portal boundary, `CellId` propagates through the `CEnvCell` portal connectivity graph. Prerequisite for wall collision from outside (#85) and the remaining #84 threshold symptom. PDB symbols and `acclient.h` `CCellStructure` refs are in place (see #87). **Unblocks G.3.**
- **✓ SHIPPED — G.3 — Dungeon streaming + portal space.** `EnvCellStreamer`, portal-visibility BFS, `PlayerTeleport (0xF751)` handling with post-transition `LoginComplete`, and the retail DAT-authored portal-space CreatureMode viewport (Setup `0x02000306`, animation `0x030005AC`, exact camera/light/timing/view-plane transitions, forward-axis roll, direct portal→world viewport switch, projection warp, and destination viewer reset; corrective visual gate pending 2026-07-15). Dungeons render, stream, teleport-in, collide, light, and their doors work — see the shipped-table rows (G.3, G.3a, #137 collision, A7.L1 lighting) below and the M1.5 section of `docs/plans/2026-05-12-milestones.md` for current gate status (one recorded end-to-end round-trip user gate outstanding; live residual = far-town teleport-OUT arrival cascade, #145-residual REOPENED). See `r09-dungeon-portal-space.md` and `docs/research/2026-07-15-retail-portal-space-pseudocode.md`.
- **✓ SHIPPED — G.3 — Dungeon streaming + portal space.** `EnvCellStreamer`, portal-visibility BFS, `PlayerTeleport (0xF751)` handling with post-transition `LoginComplete`, and the retail DAT-authored portal-space CreatureMode viewport (Setup `0x02000306`, animation `0x030005AC`, exact camera/light/timing/view-plane transitions, forward-axis roll, direct portal→world viewport switch, projection warp, destination viewer reset, object/physics-before-network frame barrier, pre-network input movement/jump output, post-inbound AutonomousPosition, and non-advancing spatial reconciliation; recall-tail corrective visual gate pending 2026-07-16). Dungeons render, stream, teleport-in, collide, light, and their doors work — see the shipped-table rows (G.3, G.3a, #137 collision, A7.L1 lighting) below and the M1.5 section of `docs/plans/2026-05-12-milestones.md` for current gate status (one recorded end-to-end dungeon round-trip user gate outstanding; live residual = far-town teleport-OUT arrival cascade, #145-residual REOPENED). See `r09-dungeon-portal-space.md` and `docs/research/2026-07-15-retail-portal-space-pseudocode.md`.
**Acceptance:** walk outside at dusk, see the sky gradient + sun moving; enter a torch-lit dungeon via portal; leave back to daylight.

View file

@ -312,6 +312,16 @@ a black alpha fade; both compose below retained UI, so
chat, panels, toolbar, cursor, and input remain live throughout travel. See
`docs/research/2026-07-15-retail-portal-space-pseudocode.md`.
The live frame barrier now also matches `SmartBox::UseTime`: local/remote
object, projectile, animation, final-pose, animation-hook, particle, and
owner-script work completes before the inbound network queue is dispatched.
Local movement/jump output is committed on that same pre-network side, while a
non-advancing post-network pass reconciles children and attached effect/light
anchors after authoritative root moves. This prevents ACE's teleport Hidden state
from freezing the final fraction of recall and replaying it after materialize,
without an arrival reset or Hidden-time animation exception. Automated recall
DAT conformance is green; destination-side visual confirmation remains.
---
#### (historical M1.5 working notes below)
@ -513,9 +523,9 @@ include dungeons.
zero-time recursive chains fail diagnostically while valid timed weather loops
continue. The
live-pose step now publishes exact root/indexed rigid-part transforms after
animation and equipped-child composition, then drains animation hooks,
PhysicsScripts, attached emitters/lights, and particle simulation in fixed
frame order. World-released particles remain at their birth positions while
animation and equipped-child composition, advances existing FP hooks, drains
animation hooks, refreshes attached emitters/lights, advances particles, then
ticks PhysicsScripts in fixed retail frame order. World-released particles remain at their birth positions while
parent-local particles follow; normal/blocking/anonymous logical emitter
semantics are exact; pending projections skip particle updates/draw while
retaining absolute creation times, the same emitter, and its logical ID, so

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.