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:
parent
801d8a189c
commit
c79d0a49da
12 changed files with 1373 additions and 402 deletions
192
src/AcDream.App/Streaming/StreamingOriginRecenterCoordinator.cs
Normal file
192
src/AcDream.App/Streaming/StreamingOriginRecenterCoordinator.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue