using AcDream.App.World;
namespace AcDream.App.Streaming;
///
/// Coordinates streaming-origin lifetime boundaries. Spatial ownership must
/// detach the complete old window first; a teleport then changes
/// before destination streaming resumes,
/// while deferred resource receipts continue under the shared frame budget.
/// A session boundary releases the controller without recentering.
///
internal sealed class StreamingOriginRecenterCoordinator : IStreamingOriginConvergence
{
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;
///
/// Begins a new recenter transaction. Its retained preparation and
/// retirement cursors advance only from
/// under that frame's shared work meter. A teleport replacement must call
/// before supplying a different destination.
///
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();
}
///
/// Observes old-window spatial detachment and commits the new origin once
/// every detached owner has an exact retirement receipt. Returns true only
/// after the destination streaming gate has been released.
///
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;
}
}
///
/// 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.
///
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();
}
}