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>
67 lines
2.7 KiB
C#
67 lines
2.7 KiB
C#
using AcDream.App.Streaming;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Streaming;
|
|
|
|
/// <summary>
|
|
/// The GameWindow side of the dungeon streaming gate (AP-36): given the player's
|
|
/// CURRENT cell, decide whether to collapse streaming and where to pin the observer.
|
|
/// The StreamingController's reaction to these inputs is covered by
|
|
/// <see cref="StreamingControllerDungeonGateTests"/>; this fixture covers the
|
|
/// INPUT computation — in particular the teleport-hold rule (#145/#138).
|
|
/// </summary>
|
|
public class DungeonStreamingGateTests
|
|
{
|
|
// A 0x0007 Town Network dungeon cell (landblock 0x0007, cell 0x0145).
|
|
private const uint DungeonCell = 0x00070145u;
|
|
// An outdoor terrain cell near Holtburg (landblock 0xAB34, structure cell 0x0001).
|
|
private const uint OutdoorCell = 0xAB340001u;
|
|
|
|
[Fact]
|
|
public void SealedDungeonCell_NotHold_CollapsesAndPinsObserverToCellLandblock()
|
|
{
|
|
var r = DungeonStreamingGate.Compute(
|
|
isTeleportHold: false, currCellIsSealedDungeon: true, currCellId: DungeonCell);
|
|
|
|
Assert.True(r.InsideDungeon);
|
|
Assert.Equal(0x0007u, r.ObserverLandblockKey); // 0x00070145 >> 16
|
|
}
|
|
|
|
[Fact]
|
|
public void OutdoorCell_NotHold_NoCollapse_NoObserverOverride()
|
|
{
|
|
var r = DungeonStreamingGate.Compute(
|
|
isTeleportHold: false, currCellIsSealedDungeon: false, currCellId: OutdoorCell);
|
|
|
|
Assert.False(r.InsideDungeon);
|
|
Assert.Null(r.ObserverLandblockKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void TeleportHold_StaleSealedDungeonCurrCell_SuppressesGate()
|
|
{
|
|
// #145/#138 — teleport OUT of a dungeon. During the arrival hold the player is
|
|
// unplaced, so CurrCell is still the frozen SOURCE dungeon cell. The gate MUST be
|
|
// suppressed: streaming has to follow the DESTINATION (the PortalSpace observer
|
|
// pin), not re-pin onto the source dungeon. The pre-fix logic returned
|
|
// (true, 0x0007) here, which kept streaming collapsed on the source dungeon → the
|
|
// outdoor destination never hydrated → 600-frame timeout → force-snap to ocean.
|
|
var r = DungeonStreamingGate.Compute(
|
|
isTeleportHold: true, currCellIsSealedDungeon: true, currCellId: DungeonCell);
|
|
|
|
Assert.False(r.InsideDungeon);
|
|
Assert.Null(r.ObserverLandblockKey);
|
|
}
|
|
|
|
[Fact]
|
|
public void TeleportHold_OutdoorCurrCell_AlsoNoCollapse()
|
|
{
|
|
// Sanity: an outdoor→outdoor or building→outdoor hold is already correct, but the
|
|
// suppression rule must not change that.
|
|
var r = DungeonStreamingGate.Compute(
|
|
isTeleportHold: true, currCellIsSealedDungeon: false, currCellId: OutdoorCell);
|
|
|
|
Assert.False(r.InsideDungeon);
|
|
Assert.Null(r.ObserverLandblockKey);
|
|
}
|
|
}
|