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:
parent
f6a576af8e
commit
a15bd3b56d
9 changed files with 290 additions and 32 deletions
51
src/AcDream.App/Streaming/DungeonStreamingGate.cs
Normal file
51
src/AcDream.App/Streaming/DungeonStreamingGate.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
namespace AcDream.App.Streaming;
|
||||
|
||||
/// <summary>Result of the per-frame dungeon streaming-gate decision.</summary>
|
||||
/// <param name="InsideDungeon">Passed to <see cref="StreamingController.Tick"/> — collapse
|
||||
/// streaming to the single dungeon landblock.</param>
|
||||
/// <param name="ObserverLandblockKey">When non-null, override the streaming observer to this
|
||||
/// landblock key (the cell id's high 16 bits, 0xXXYY). Null leaves the caller's observer as-is.</param>
|
||||
public readonly record struct DungeonGateResult(bool InsideDungeon, uint? ObserverLandblockKey);
|
||||
|
||||
/// <summary>
|
||||
/// AP-36: the dungeon streaming gate (#133 FPS). When the player stands in a SEALED
|
||||
/// EnvCell (an indoor cell that doesn't see outside), streaming collapses to the single
|
||||
/// dungeon landblock — AC dungeons have no adjacent landblocks, so the normal 25×25
|
||||
/// window would pull in ~129 unrelated ocean-grid dungeons. The trigger is the player's
|
||||
/// CURRENT cell (<c>CellGraph.CurrCell</c>, set the moment the player is placed), and the
|
||||
/// observer is pinned to that cell's OWN landblock (the cell id high 16 bits) because a
|
||||
/// dungeon's EnvCells sit at arbitrary ocean-grid world coords with negative local offsets.
|
||||
///
|
||||
/// <para>Extracted from <c>GameWindow.OnUpdate</c> as a pure function so the
|
||||
/// teleport-hold rule (below) is unit-testable without the GL/dat/network stack.</para>
|
||||
/// </summary>
|
||||
public static class DungeonStreamingGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Decide the streaming gate from the player's current cell.
|
||||
/// </summary>
|
||||
/// <param name="isTeleportHold">True while a teleport arrival is held (the controller is in
|
||||
/// PortalSpace): the player is NOT yet placed, so <paramref name="currCellId"/> is the frozen
|
||||
/// SOURCE cell, not where the player is going.</param>
|
||||
/// <param name="currCellIsSealedDungeon"><c>CurrCell is EnvCell && !SeenOutside</c>.</param>
|
||||
/// <param name="currCellId">The current cell id (0xXXYYNNNN).</param>
|
||||
public static DungeonGateResult Compute(
|
||||
bool isTeleportHold, bool currCellIsSealedDungeon, uint currCellId)
|
||||
{
|
||||
// #145/#138: during a teleport hold the player is NOT yet placed, so CurrCell is
|
||||
// the frozen SOURCE cell — where the player IS, not where they're going. Streaming
|
||||
// must follow the DESTINATION, which the PortalSpace observer pin already does, so
|
||||
// the source-cell gate is suppressed. Otherwise a teleport OUT of a dungeon keeps
|
||||
// streaming collapsed on the source dungeon (CurrCell still sealed) → the outdoor
|
||||
// destination never hydrates → TeleportArrivalReadiness holds 600 frames → force-
|
||||
// snap to ocean. A teleport INTO a dungeon is handled explicitly upstream by
|
||||
// StreamingController.PreCollapseToDungeon (and the controller's _collapsed latch
|
||||
// holds it through the hold), so suppressing the gate here doesn't regress it.
|
||||
if (isTeleportHold)
|
||||
return new DungeonGateResult(false, null);
|
||||
|
||||
if (currCellIsSealedDungeon)
|
||||
return new DungeonGateResult(true, currCellId >> 16);
|
||||
return new DungeonGateResult(false, null);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue