fix(streaming): #145 — teleport re-use via server-authoritative placement

Portals only worked once per session: teleporting OUT of a dungeon
mis-rooted the player into the SOURCE dungeon's coordinate frame, so every
move was sent dungeon-framed and ACE rejected it ("failed transition") —
the player couldn't move, never reached a portal, and the world wouldn't
re-render (only skybox).

Root cause: acdream's streaming-relative frame recenters on teleport, but
resident physics landblocks keep their load-time world-offset. After
recentering onto the outdoor destination, the collapsed source dungeon
(offset 0,0 as the prior center) and the destination (offset 0,0 as the
new center) overlap, and the Z-agnostic outdoor cell-snap returns the
dungeon for both the arrival placement and every per-frame resolve.

Fix (server-authoritative teleport placement):
- Drop the stale source center landblock from physics at the teleport
  recenter (GameWindow.OnLivePositionUpdated) so the resolve falls through
  to the server position (Resolve NO-LANDBLOCK verbatim) until the
  destination streams in.
- Place outdoor teleports immediately (TeleportArrivalRules) — holding is
  futile because streaming does not progress during a PortalSpace hold.
- Clear a dangling CellGraph.CurrCell when its landblock is removed
  (PhysicsEngine.RemoveLandblock) — otherwise the dungeon-streaming gate
  keeps streaming collapsed onto the gone dungeon (only skybox renders).

Keeps DungeonStreamingGate (gate suppression during the hold). Indoor
(dungeon-entry) placement is unchanged (cell-keyed, IsSpawnCellReady).

User-verified: in->out->re-enter works repeatedly, no ACE errors, world
renders. Remaining facets (server objects + own avatar not rendering after
a teleport-out) are entity render/lifecycle — split to #138.

Registers AP-36 + AD-2 updated. New: DungeonStreamingGate (+4 tests),
TeleportArrivalRules (+4 tests). Build + 2727 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-20 21:38:00 +02:00
parent f6a576af8e
commit a15bd3b56d
9 changed files with 290 additions and 32 deletions

View file

@ -81,6 +81,19 @@ public sealed class PhysicsEngine
_landblocks.Remove(landblockId);
ShadowObjects.RemoveLandblock(landblockId);
// #145: if the player's current cell belonged to the landblock being removed (a teleport
// drops the stale source center via OnLivePositionUpdated), clear it. Otherwise CurrCell
// dangles on an orphaned cell and the dungeon-streaming gate — keyed on CurrCell — keeps
// streaming collapsed onto the gone landblock, so the destination never streams in and
// only the skybox renders. Clearing it lets the gate read "not in a dungeon" → the
// controller ExitDungeonExpands to the destination, and CurrCell re-acquires once the
// destination landblock hydrates and the per-frame resolve roots into it.
if (DataCache?.CellGraph is { } cg && cg.CurrCell is { } cur
&& (cur.Id & 0xFFFF0000u) == (landblockId & 0xFFFF0000u))
{
cg.CurrCell = null;
}
// UCG Stage 1: mirror removal into the unified graph (inert this stage).
DataCache?.CellGraph.RemoveLandblock(landblockId);
}