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(); } }