refactor(streaming): complete landblock presentation cutover

Remove the legacy GameWindow apply path and make the concrete render, physics, and static publishers the only production owner graph. Serialize full-window retirement with shared-origin teleport and session boundaries so old coordinate-frame resources cannot survive into a new world or login.
This commit is contained in:
Erik 2026-07-21 22:47:30 +02:00
parent 801d8a189c
commit c79d0a49da
12 changed files with 1373 additions and 402 deletions

View file

@ -129,8 +129,8 @@ public sealed class LandblockBuildFactory
// Hydrate the stabs: same logic as the old OnLoad preload. Each stab
// entity from LandblockLoader carries a SourceGfxObjOrSetupId that we
// expand into per-part MeshRefs via SetupMesh.Flatten / GfxObjMesh.Build.
// GPU upload (EnsureUploaded) happens on the render thread in
// ApplyLoadedTerrain — NOT here.
// GPU upload happens later through the render-thread presentation
// pipeline, never in this worker-side build.
var hydrated = new List<AcDream.Core.World.WorldEntity>(baseLoaded.Entities.Count);
foreach (var e in baseLoaded.Entities)
{
@ -364,7 +364,8 @@ public sealed class LandblockBuildFactory
if (gfx is not null)
{
_physicsDataCache.CacheGfxObj(spawn.ObjectId, gfx);
// Sub-meshes pre-built CPU-side; upload deferred to ApplyLoadedTerrain.
// Sub-meshes are CPU-built here; the presentation pipeline
// defers upload to the render thread.
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
if (pb is not null) sceneryBounds.Add(scaleMat, pb.Value);
@ -417,9 +418,8 @@ public sealed class LandblockBuildFactory
// _physicsEngine.SampleTerrainZ(worldPx) query was structurally racy: at
// build time this landblock is NOT registered in physics yet, so that query
// could only return null (→ this same own-heightmap) or a STALE neighbor's
// height — the previous location's terrain, still registered after a teleport
// recenter (which drops only the single stale CENTER landblock, GameWindow
// :5444) until streaming unloads it — planting scenery at the old location's
// height — the previous location's terrain before the full old-window
// recenter retirement converges — planting scenery at the old location's
// altitude (trees-in-sky, deltaZ up to +500m; confirmed via the
// [scenery-z-stale] probe 2026-06-22). Own-heightmap is correct in every
// case, so the global query is removed (also drops its per-spawn cost).
@ -526,7 +526,7 @@ public sealed class LandblockBuildFactory
/// Portal cells and drawable shell placements are accumulated in the
/// transaction-local <paramref name="envCellBuild"/>. The render thread
/// cannot observe this landblock until the streaming completion carries the
/// finished transaction to ApplyLoadedTerrain.
/// finished transaction to <c>LandblockPresentationPipeline</c>.
///
/// Ported from pre-streaming preload lines 407-565.
/// </summary>
@ -683,8 +683,8 @@ public sealed class LandblockBuildFactory
// Setup carries real Lights — a "light attach point" fixture (e.g. the Town
// Network fountain room's ceiling light, Setup 0x02000365). Track the dat
// Setup's Lights.Count here so the meshRefs==0 gate below doesn't also drop
// the entity that would otherwise carry those lights to the registration
// pass (GameWindow.cs ~7900).
// the entity that otherwise carries those lights to the static
// presentation publisher.
int stabLightCount = 0;
if ((stab.Id & 0xFF000000u) == 0x01000000u)
{

View file

@ -326,9 +326,9 @@ public sealed class LandblockPhysicsPublisher
/// <summary>
/// Publishes ordinary static collision and refloods after the caller has
/// completed the render-owned building/EnvCell suffix. The callback keeps
/// the shipped per-entity light-before-collision order until checkpoint E
/// moves static presentation into the transaction coordinator.
/// completed the render-owned building/EnvCell suffix. The concrete
/// presentation pipeline supplies the static-presentation owner callback
/// that preserves per-entity light-before-collision order.
/// </summary>
public void CompletePublication(
LandblockPhysicsPublication publication,

View file

@ -4,14 +4,23 @@ using AcDream.Core.World;
namespace AcDream.App.Streaming;
/// <summary>
/// Read-only presentation state needed by render traversal and diagnostics.
/// Keeping this projection on the pipeline prevents the composition root from
/// retaining each concrete publication owner independently.
/// </summary>
public readonly record struct LandblockPresentationDiagnostics(
LandblockRenderPublisherDiagnostics Render,
LandblockPhysicsPublisherDiagnostics Physics,
LandblockStaticPresentationDiagnostics Statics);
/// <summary>
/// Render-thread transaction coordinator for one accepted landblock streaming
/// result. Streaming policy (desired tier, generation, priority, and apply
/// budget) remains in <see cref="StreamingController"/>; spatial buckets remain
/// in <see cref="GpuWorldState"/>. This owner fixes the presentation ordering
/// independently of those policies so the concrete render, physics, and static
/// publishers can be extracted behind it without returning callbacks to the
/// game window.
/// independently of those policies. The concrete render, physics, and static
/// publishers remain private implementation participants behind this boundary.
/// </summary>
/// <remarks>
/// Retail establishes cells/buildings before static objects in
@ -19,9 +28,9 @@ namespace AcDream.App.Streaming;
/// <c>CLandBlock::release_all @ 0x0052FCF0</c> (objects and visible cells) from
/// later destruction in <c>CLandBlock::Destroy @ 0x0052FAA0</c> (static objects
/// and buildings). Acdream's detach-first retry ledger is the existing
/// asynchronous lifetime adaptation. The injected publication operation
/// currently contains the already-shipped production transaction; later
/// Slice-5 checkpoints replace it with focused publishers.
/// asynchronous lifetime adaptation. Production always uses the focused owner
/// graph; the internal delegate constructor exists only for hermetic policy and
/// retry characterization tests.
/// </remarks>
public sealed class LandblockPresentationPipeline
{
@ -63,7 +72,7 @@ public sealed class LandblockPresentationPipeline
private readonly Dictionary<LandblockStreamResult, PublicationTransaction> _publications =
new(ReferenceEqualityComparer.Instance);
public LandblockPresentationPipeline(
internal LandblockPresentationPipeline(
Action<LandblockBuild, LandblockMeshData> publishBeforeSpatialCommit,
GpuWorldState state,
Action<uint>? onLandblockLoaded = null,
@ -137,6 +146,21 @@ public sealed class LandblockPresentationPipeline
retirementOwner.Advance);
}
/// <summary>
/// Building visibility registries published by the render owner. Legacy
/// test pipelines have no render owner and therefore expose an empty view.
/// </summary>
public IReadOnlyCollection<BuildingRegistry> BuildingRegistries =>
_renderPublisher?.BuildingRegistries ?? Array.Empty<BuildingRegistry>();
/// <summary>
/// Cumulative diagnostics from the three concrete presentation owners.
/// </summary>
public LandblockPresentationDiagnostics Diagnostics => new(
_renderPublisher?.Diagnostics ?? default,
_physicsPublisher?.Diagnostics ?? default,
_staticPublisher?.Diagnostics ?? default);
public int PendingRetirementCount => _retirements.PendingCount;
public int PendingPublicationCount => _publications.Count;

View file

@ -18,6 +18,21 @@ namespace AcDream.App.Streaming;
/// </summary>
public sealed class StreamingController
{
private sealed class OriginRecenterRetirement
{
public bool RadiiConverged;
public bool GenerationAdvanced;
public bool PendingLoadsCleared;
public bool DeferredApplyCleared;
public bool RegionCleared;
public List<uint>? ResidentIds;
public int RetirementCursor;
public bool PreparationCommitted;
public (int X, int Y, bool IsSealedDungeon)? Destination;
public bool DestinationConfigured;
public bool DestinationLoadEnqueued;
}
private readonly Action<uint, LandblockStreamJobKind, ulong> _enqueueLoad;
private readonly Action<uint, ulong> _enqueueUnload;
private readonly Func<int, IReadOnlyList<LandblockStreamResult>> _drainCompletions;
@ -29,6 +44,8 @@ public sealed class StreamingController
private RadiiReconfiguration? _pendingRadiiReconfiguration;
private bool _advancingRadiiReconfiguration;
private (int NearRadius, int FarRadius)? _deferredRadiiRequest;
private OriginRecenterRetirement? _originRecenterRetirement;
private bool _advancingOriginRecenter;
// Hard streaming boundaries advance this token. Worker completions carry
// the generation captured at enqueue time, so an old overlapping load or
// unload cannot mutate the replacement window after a portal recenter.
@ -116,6 +133,7 @@ public sealed class StreamingController
// GPU-upload tail draining at MaxCompletionsPerFrame. Removable probe surface.
public int DeferredApplyBacklog => _deferredApply.Count;
public int PendingRetirementCount => _presentation.PendingRetirementCount;
internal bool IsCollapsedToDungeon => _collapsed;
/// <summary>
/// True once every in-bounds landblock in the requested Chebyshev ring has
@ -154,15 +172,21 @@ public sealed class StreamingController
return true;
}
// [FRAME-DIAG]: how many times ForceReloadWindow has fired (each one drops +
// re-uploads the WHOLE window) and the landblock count it dropped last time.
public int ForceReloadCount { get; private set; }
public int LastForceReloadDropCount { get; private set; }
// [FRAME-DIAG]: full-window presentation retirements include explicit
// reloads and shared-origin recenter barriers. Both retire and later
// re-upload the complete resident window.
public int FullWindowRetirementCount { get; private set; }
public int LastFullWindowRetirementLandblockCount { get; private set; }
// Completions that were drained past a priority item get buffered here
// so they still apply over subsequent frames without loss.
private readonly List<LandblockStreamResult> _deferredApply = new();
public StreamingController(
/// <summary>
/// Internal compatibility seam for hermetic controller policy tests. Live
/// composition cannot supply presentation callbacks; it must use the
/// concrete pipeline constructor below.
/// </summary>
internal StreamingController(
Action<uint, LandblockStreamJobKind, ulong> enqueueLoad,
Action<uint, ulong> enqueueUnload,
Func<int, IReadOnlyList<LandblockStreamResult>> drainCompletions,
@ -245,6 +269,15 @@ public sealed class StreamingController
nameof(farRadius),
"Far radius must be greater than or equal to near radius.");
// A radius transaction may not admit work after an origin-recenter
// snapshot has been captured. Retain only the latest requested radii;
// the first Tick after origin commit applies them to the new frame.
if (_originRecenterRetirement is not null)
{
_deferredRadiiRequest = (nearRadius, farRadius);
return;
}
// Queue callbacks are injected seams and can synchronously reenter the
// controller. Last-request-wins deferral keeps the active mutation
// cursor stable; the request is applied after the current transaction
@ -427,6 +460,12 @@ public sealed class StreamingController
/// </summary>
public void Tick(int observerCx, int observerCy, bool insideDungeon = false)
{
if (_originRecenterRetirement is not null)
{
_presentation.AdvanceRetirements();
return;
}
if (_pendingRadiiReconfiguration is not null)
AdvanceRadiiReconfiguration();
else if (_deferredRadiiRequest is { } deferred)
@ -762,18 +801,262 @@ public sealed class StreamingController
}
/// <summary>
/// 2026-06-22: an OUTDOOR teleport moved the render origin (<c>_liveCenter</c>). Every
/// resident terrain block was baked relative to the OLD origin, so any block that survives
/// an INCREMENTAL recenter — which happens on a NEARBY jump where the old and new windows
/// overlap, e.g. (170,168)→(169,180) — renders shifted by the jump distance: the confirmed
/// "terrain in the sky" arcs (the stale slots were all offset by exactly deltaLB×192).
/// SYNCHRONOUSLY drop every resident landblock (render slot + physics + state) so none
/// survives into the next frame stale, and no async unload can race a re-bake, then null
/// the region so the next <see cref="NormalTick"/> re-bootstraps the WHOLE window fresh at
/// the new origin (the near ring is priority-applied during portal travel; the rest streams in).
/// A sealed-dungeon destination uses <see cref="PreCollapseToDungeon"/> instead.
/// Starts a reload of the current streaming window. Logical residency is
/// detached immediately; render, physics, and static-lighting owners then
/// converge through the retained presentation-retirement ledger before
/// <see cref="NormalTick"/> bootstraps the window again.
/// Shared-origin teleports use <see cref="BeginOriginRecenter"/> instead so
/// every old-window owner retires while the old coordinate frame is active.
/// </summary>
public void ForceReloadWindow()
{
BeginFullWindowRetirement();
}
/// <summary>
/// Starts the old-window half of a shared-origin recenter. All accepted
/// worker work is invalidated and every resident landblock is detached
/// through the retryable presentation ledger while the old coordinate
/// frame is still active. <see cref="Tick"/> remains blocked until
/// <see cref="TryCommitOriginRecenter"/> succeeds.
/// </summary>
internal void BeginOriginRecenter()
{
_originRecenterRetirement ??= new OriginRecenterRetirement();
}
/// <summary>
/// Advances every retained old-window teardown and reports whether the
/// composition root may safely change the shared world origin.
/// </summary>
internal bool IsOriginRecenterRetirementComplete()
{
if (_originRecenterRetirement is null)
throw new InvalidOperationException(
"No streaming-origin recenter transaction is pending.");
if (!TryAdvanceOriginRecenterPreparation())
return false;
_presentation.AdvanceRetirements();
return _presentation.PendingRetirementCount == 0;
}
/// <summary>
/// Releases the streaming bootstrap gate after the composition root has
/// committed the new shared origin. No old-window presentation owner may
/// still be pending at this edge.
/// </summary>
internal bool TryCommitOriginRecenter(
int destinationX,
int destinationY,
bool isSealedDungeon)
{
if (_advancingOriginRecenter)
return false;
_advancingOriginRecenter = true;
try
{
return TryCommitOriginRecenterCore(
destinationX,
destinationY,
isSealedDungeon);
}
finally
{
_advancingOriginRecenter = false;
}
}
private bool TryCommitOriginRecenterCore(
int destinationX,
int destinationY,
bool isSealedDungeon)
{
OriginRecenterRetirement transaction = _originRecenterRetirement
?? throw new InvalidOperationException(
"No streaming-origin recenter transaction is pending.");
if (!transaction.PreparationCommitted)
throw new InvalidOperationException(
"Streaming-origin retirement preparation has not completed.");
if (_presentation.PendingRetirementCount != 0)
throw new InvalidOperationException(
"The streaming origin cannot change while old-window presentation retirement is pending.");
var destination = (destinationX, destinationY, isSealedDungeon);
if (transaction.Destination is { } retained && retained != destination)
{
throw new InvalidOperationException(
"A recenter transaction cannot commit two different destinations.");
}
transaction.Destination ??= destination;
if (!transaction.DestinationConfigured)
{
_collapsed = isSealedDungeon;
_collapsedCenter = isSealedDungeon
? StreamingRegion.EncodeLandblockId(destinationX, destinationY)
: 0u;
if (isSealedDungeon)
{
_region = new StreamingRegion(
destinationX,
destinationY,
nearRadius: 0,
farRadius: 0);
_region.MarkResidentFromBootstrap();
}
else
{
_region = null;
}
transaction.DestinationConfigured = true;
}
if (isSealedDungeon && !transaction.DestinationLoadEnqueued)
{
try
{
EnqueueLoad(
StreamingRegion.EncodeLandblockId(destinationX, destinationY),
LandblockStreamJobKind.LoadNear);
transaction.DestinationLoadEnqueued = true;
}
catch (StreamingMutationException error) when (error.MutationCommitted)
{
transaction.DestinationLoadEnqueued = true;
Console.WriteLine(
$"streaming: committed dungeon recenter enqueue reported failure: {error}");
return false;
}
catch (Exception error)
{
Console.WriteLine(
$"streaming: dungeon recenter enqueue will resume: {error}");
return false;
}
}
_originRecenterRetirement = null;
return true;
}
/// <summary>
/// Releases a fully retired origin transaction at a session boundary
/// without bootstrapping a destination from the ending session.
/// </summary>
internal bool TryCancelOriginRecenter()
{
if (_originRecenterRetirement is not { PreparationCommitted: true })
return false;
if (_presentation.PendingRetirementCount != 0)
return false;
_collapsed = false;
_collapsedCenter = 0u;
_region = null;
_originRecenterRetirement = null;
return true;
}
private bool TryAdvanceOriginRecenterPreparation()
{
OriginRecenterRetirement transaction = _originRecenterRetirement
?? throw new InvalidOperationException(
"No streaming-origin recenter transaction is pending.");
if (transaction.PreparationCommitted)
return true;
if (_advancingOriginRecenter)
return false;
_advancingOriginRecenter = true;
try
{
if (!transaction.RadiiConverged)
{
if (_pendingRadiiReconfiguration is not null)
AdvanceRadiiReconfiguration();
if (_pendingRadiiReconfiguration is not null)
return false;
transaction.RadiiConverged = true;
}
if (!transaction.GenerationAdvanced)
{
AdvanceGeneration();
transaction.GenerationAdvanced = true;
}
if (!transaction.PendingLoadsCleared)
{
try
{
_clearPendingLoads?.Invoke();
transaction.PendingLoadsCleared = true;
}
catch (StreamingMutationException error) when (error.MutationCommitted)
{
transaction.PendingLoadsCleared = true;
Console.WriteLine(
$"streaming: committed pending-load clear reported failure: {error}");
return false;
}
}
if (!transaction.DeferredApplyCleared)
{
_deferredApply.Clear();
transaction.DeferredApplyCleared = true;
}
if (!transaction.RegionCleared)
{
_collapsed = false;
_region = null;
transaction.RegionCleared = true;
}
if (transaction.ResidentIds is null)
{
transaction.ResidentIds = new List<uint>(_state.LoadedLandblockIds);
FullWindowRetirementCount++;
LastFullWindowRetirementLandblockCount = transaction.ResidentIds.Count;
}
while (transaction.RetirementCursor < transaction.ResidentIds.Count)
{
uint id = transaction.ResidentIds[transaction.RetirementCursor];
try
{
_presentation.BeginFullRetirement(id);
transaction.RetirementCursor++;
}
catch (Exception error)
{
// A delivered visibility-observer failure may surface after
// detachment has committed. Advance that exact cursor only
// when world state proves the old resident is unreachable;
// otherwise retain it for the next frame.
if (!_state.IsLoaded(id))
transaction.RetirementCursor++;
Console.WriteLine(
$"streaming: origin-recenter retirement for 0x{id:X8} " +
$"will resume: {error}");
return false;
}
}
transaction.PreparationCommitted = true;
return true;
}
catch (Exception error)
{
Console.WriteLine(
$"streaming: origin-recenter preparation will resume: {error}");
return false;
}
finally
{
_advancingOriginRecenter = false;
}
}
private void BeginFullWindowRetirement()
{
AdvanceGeneration();
_collapsed = false;
@ -785,8 +1068,8 @@ public sealed class StreamingController
_region = null;
// Snapshot — RemoveLandblock mutates the loaded set we're iterating.
var ids = new List<uint>(_state.LoadedLandblockIds);
ForceReloadCount++; // [FRAME-DIAG] churn counter
LastForceReloadDropCount = ids.Count; // = upcoming re-upload volume
FullWindowRetirementCount++; // [FRAME-DIAG] churn counter
LastFullWindowRetirementLandblockCount = ids.Count; // upcoming re-upload volume
foreach (var id in ids)
_presentation.BeginFullRetirement(id);
}
@ -838,14 +1121,13 @@ public sealed class StreamingController
}
catch
{
// The presentation pipeline retains its exact committed
// stage after the coarse presentation callback commits.
// Preserve it and every untouched completion from this
// already-drained chunk in original FIFO order. A
// presentation-stage failure is deliberately fail-fast
// until the focused publishers replace that callback;
// its result is not retryable, but the untouched suffix
// must still survive.
// The concrete presentation pipeline retains the exact
// unfinished stage. Preserve it and every untouched
// completion from this already-drained chunk in
// original FIFO order. Internal compatibility tests may
// use a callback pipeline that cannot retain a receipt;
// its failed result is consumed, but the untouched
// suffix must still survive.
if (_presentation.HasPendingPublication(result))
_deferredApply.Add(result);
for (int suffix = chunkIndex + 1; suffix < chunk.Count; suffix++)
@ -890,12 +1172,12 @@ public sealed class StreamingController
}
catch
{
// A suffix-stage failure keeps its pipeline receipt and
// must remain at this exact FIFO position. The still-
// monolithic presentation callback deliberately removes
// its receipt on failure because its internal committed
// prefix is unknown; consume only that failed result so a
// later frame cannot replay it automatically.
// A concrete suffix-stage failure keeps its pipeline
// receipt and must remain at this exact FIFO position. The
// internal compatibility callback cannot report its exact
// committed prefix and therefore removes its receipt;
// consume only that failed result so a later frame cannot
// replay it automatically.
if (!_presentation.HasPendingPublication(result))
read++;
throw;

View file

@ -0,0 +1,192 @@
using AcDream.App.World;
namespace AcDream.App.Streaming;
/// <summary>
/// Coordinates streaming-origin lifetime boundaries. Presentation must retire
/// the complete old window first; a teleport then changes
/// <see cref="LiveWorldOriginState"/> before destination streaming resumes,
/// while a session boundary releases the controller without recentering. The
/// transaction remains pending across frames when an owner teardown needs retry.
/// </summary>
internal sealed class StreamingOriginRecenterCoordinator
{
private readonly record struct Request(
int DestinationX,
int DestinationY,
bool IsSealedDungeon,
bool CancelAtSessionBoundary = false);
private readonly StreamingController _streaming;
private readonly LiveWorldOriginState _origin;
private Request? _pending;
private Request? _replacement;
private bool _originCommitted;
private bool _acceptReplacement;
private bool _sourceWasSealedDungeon;
private bool _advancing;
public StreamingOriginRecenterCoordinator(
StreamingController streaming,
LiveWorldOriginState origin)
{
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
}
public bool IsPending => _pending is not null;
/// <summary>
/// Begins a new recenter transaction and performs its first retirement
/// attempt immediately. A teleport replacement must call <see cref="Reset"/>
/// before supplying a different destination.
/// </summary>
public bool Begin(int destinationX, int destinationY, bool isSealedDungeon)
{
var request = new Request(destinationX, destinationY, isSealedDungeon);
if (_pending is { } pending && pending != request)
{
if (!_acceptReplacement)
{
throw new InvalidOperationException(
"A different streaming-origin recenter is already pending.");
}
_acceptReplacement = false;
if (_originCommitted)
_replacement = request;
else
_pending = request;
}
if (_pending is null)
{
_pending = request;
_acceptReplacement = false;
_sourceWasSealedDungeon = _streaming.IsCollapsedToDungeon;
_streaming.BeginOriginRecenter();
}
return Advance();
}
/// <summary>
/// Advances retained presentation teardown. Returns true only after the
/// new origin is committed and destination streaming has been unblocked.
/// </summary>
public bool Advance()
{
if (_advancing)
return false;
_advancing = true;
try
{
while (_pending is { } request)
{
if (!_originCommitted)
{
if (!_streaming.IsOriginRecenterRetirementComplete())
return false;
if (_pending is not { } current || current != request)
continue;
// A session-boundary cancellation only releases the old
// controller gate. Session identity owns resetting and
// initializing the next live origin, so an asynchronously
// converging teardown must never overwrite it.
if (!request.CancelAtSessionBoundary)
_origin.Recenter(request.DestinationX, request.DestinationY);
_originCommitted = true;
}
bool destinationCommitted = request.CancelAtSessionBoundary
? _streaming.TryCancelOriginRecenter()
: _streaming.TryCommitOriginRecenter(
request.DestinationX,
request.DestinationY,
request.IsSealedDungeon);
if (!destinationCommitted)
return false;
if (_pending is not { } committed || committed != request)
{
_originCommitted = false;
_streaming.BeginOriginRecenter();
continue;
}
_pending = null;
_originCommitted = false;
_acceptReplacement = false;
if (_replacement is not { } replacement)
return true;
_replacement = null;
_pending = replacement;
_sourceWasSealedDungeon = _streaming.IsCollapsedToDungeon;
_streaming.BeginOriginRecenter();
}
return true;
}
finally
{
_advancing = false;
}
}
/// <summary>
/// Converts an uncommitted request into a same-origin convergence request
/// at a teleport-lifetime boundary. This cannot orphan the controller's
/// bootstrap gate: a replacement may reuse the retained old-window barrier,
/// while cancellation/session reset completes it at the current origin.
/// </summary>
public bool Reset(bool sessionEnding = false)
{
if (_pending is null)
{
if (!sessionEnding)
return true;
// Every character-session boundary invalidates geometry baked in
// the old shared coordinate frame, even when no teleport was in
// flight. Retire the complete window before session identity may
// allow the next player spawn to initialize a different origin.
_pending = new Request(
_origin.CenterX,
_origin.CenterY,
IsSealedDungeon: false,
CancelAtSessionBoundary: true);
_replacement = null;
_originCommitted = false;
_acceptReplacement = false;
_sourceWasSealedDungeon = _streaming.IsCollapsedToDungeon;
_streaming.BeginOriginRecenter();
return Advance();
}
_replacement = null;
_acceptReplacement = true;
if (!_originCommitted)
{
_pending = new Request(
_origin.CenterX,
_origin.CenterY,
IsSealedDungeon: !sessionEnding && _sourceWasSealedDungeon,
CancelAtSessionBoundary: sessionEnding);
}
else if (sessionEnding)
{
_pending = new Request(
_origin.CenterX,
_origin.CenterY,
IsSealedDungeon: false,
CancelAtSessionBoundary: true);
}
return Advance();
}
}