refactor(runtime): acknowledge exact world host projections
This commit is contained in:
parent
73d0b54e38
commit
18d17d8bb1
17 changed files with 1677 additions and 72 deletions
|
|
@ -453,6 +453,8 @@ internal sealed class FrameRootCompositionPhase
|
||||||
lifecycleAutomation =
|
lifecycleAutomation =
|
||||||
new WorldLifecycleAutomationController(
|
new WorldLifecycleAutomationController(
|
||||||
() => session.WorldReveal.Snapshot,
|
() => session.WorldReveal.Snapshot,
|
||||||
|
() => d.WorldEnvironment.Runtime.Ownership,
|
||||||
|
() => live.WorldTransit.Ownership,
|
||||||
() => session.WorldReveal.PortalMaterializationCount,
|
() => session.WorldReveal.PortalMaterializationCount,
|
||||||
resourceSnapshots.Capture,
|
resourceSnapshots.Capture,
|
||||||
screenshots,
|
screenshots,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.App.Streaming;
|
||||||
using AcDream.App.UI.Testing;
|
using AcDream.App.UI.Testing;
|
||||||
using AcDream.Core.Physics;
|
using AcDream.Core.Physics;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
|
|
||||||
namespace AcDream.App.Diagnostics;
|
namespace AcDream.App.Diagnostics;
|
||||||
|
|
||||||
|
|
@ -103,6 +104,8 @@ internal sealed record WorldLifecycleCheckpoint(
|
||||||
DateTime TimestampUtc,
|
DateTime TimestampUtc,
|
||||||
int ProcessId,
|
int ProcessId,
|
||||||
RuntimePortalSnapshot Reveal,
|
RuntimePortalSnapshot Reveal,
|
||||||
|
RuntimeWorldEnvironmentOwnershipSnapshot EnvironmentOwnership,
|
||||||
|
RuntimeWorldTransitOwnershipSnapshot TransitOwnership,
|
||||||
RenderFrameOutcome Render,
|
RenderFrameOutcome Render,
|
||||||
WorldLifecycleResourceSnapshot Resources);
|
WorldLifecycleResourceSnapshot Resources);
|
||||||
|
|
||||||
|
|
@ -177,6 +180,10 @@ internal sealed class WorldLifecycleAutomationController :
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly Func<RuntimePortalSnapshot> _getReveal;
|
private readonly Func<RuntimePortalSnapshot> _getReveal;
|
||||||
|
private readonly Func<RuntimeWorldEnvironmentOwnershipSnapshot>
|
||||||
|
_getEnvironmentOwnership;
|
||||||
|
private readonly Func<RuntimeWorldTransitOwnershipSnapshot>
|
||||||
|
_getTransitOwnership;
|
||||||
private readonly Func<int> _getPortalMaterializationCount;
|
private readonly Func<int> _getPortalMaterializationCount;
|
||||||
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
|
private readonly Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot>
|
||||||
_captureResources;
|
_captureResources;
|
||||||
|
|
@ -191,6 +198,9 @@ internal sealed class WorldLifecycleAutomationController :
|
||||||
|
|
||||||
public WorldLifecycleAutomationController(
|
public WorldLifecycleAutomationController(
|
||||||
Func<RuntimePortalSnapshot> getReveal,
|
Func<RuntimePortalSnapshot> getReveal,
|
||||||
|
Func<RuntimeWorldEnvironmentOwnershipSnapshot>
|
||||||
|
getEnvironmentOwnership,
|
||||||
|
Func<RuntimeWorldTransitOwnershipSnapshot> getTransitOwnership,
|
||||||
Func<int> getPortalMaterializationCount,
|
Func<int> getPortalMaterializationCount,
|
||||||
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
|
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> captureResources,
|
||||||
FrameScreenshotController screenshots,
|
FrameScreenshotController screenshots,
|
||||||
|
|
@ -198,6 +208,11 @@ internal sealed class WorldLifecycleAutomationController :
|
||||||
Action<string>? log = null)
|
Action<string>? log = null)
|
||||||
{
|
{
|
||||||
_getReveal = getReveal ?? throw new ArgumentNullException(nameof(getReveal));
|
_getReveal = getReveal ?? throw new ArgumentNullException(nameof(getReveal));
|
||||||
|
_getEnvironmentOwnership = getEnvironmentOwnership
|
||||||
|
?? throw new ArgumentNullException(
|
||||||
|
nameof(getEnvironmentOwnership));
|
||||||
|
_getTransitOwnership = getTransitOwnership
|
||||||
|
?? throw new ArgumentNullException(nameof(getTransitOwnership));
|
||||||
_getPortalMaterializationCount = getPortalMaterializationCount
|
_getPortalMaterializationCount = getPortalMaterializationCount
|
||||||
?? throw new ArgumentNullException(nameof(getPortalMaterializationCount));
|
?? throw new ArgumentNullException(nameof(getPortalMaterializationCount));
|
||||||
_captureResources = captureResources ?? throw new ArgumentNullException(nameof(captureResources));
|
_captureResources = captureResources ?? throw new ArgumentNullException(nameof(captureResources));
|
||||||
|
|
@ -289,6 +304,8 @@ internal sealed class WorldLifecycleAutomationController :
|
||||||
TimestampUtc: DateTime.UtcNow,
|
TimestampUtc: DateTime.UtcNow,
|
||||||
ProcessId: Environment.ProcessId,
|
ProcessId: Environment.ProcessId,
|
||||||
Reveal: _getReveal(),
|
Reveal: _getReveal(),
|
||||||
|
EnvironmentOwnership: _getEnvironmentOwnership(),
|
||||||
|
TransitOwnership: _getTransitOwnership(),
|
||||||
Render: outcome,
|
Render: outcome,
|
||||||
Resources: _captureResources(outcome));
|
Resources: _captureResources(outcome));
|
||||||
string json = JsonSerializer.Serialize(checkpoint, JsonOptions);
|
string json = JsonSerializer.Serialize(checkpoint, JsonOptions);
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,9 @@ internal sealed class CurrentGameRuntimeViewAdapter : IGameRuntimeView
|
||||||
_actionView.Snapshot,
|
_actionView.Snapshot,
|
||||||
_movementView.Snapshot,
|
_movementView.Snapshot,
|
||||||
_environmentView.Snapshot,
|
_environmentView.Snapshot,
|
||||||
_portalView.Snapshot);
|
_environmentView.Ownership,
|
||||||
|
_portalView.Snapshot,
|
||||||
|
_portalView.Ownership);
|
||||||
|
|
||||||
internal void Deactivate() => _active = false;
|
internal void Deactivate() => _active = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,9 @@ internal sealed class WorldGenerationQuiescence
|
||||||
private readonly Func<uint, RuntimeEntityKey?> _resolveProjectionKey;
|
private readonly Func<uint, RuntimeEntityKey?> _resolveProjectionKey;
|
||||||
private readonly IWorldAudioQuiescence? _audio;
|
private readonly IWorldAudioQuiescence? _audio;
|
||||||
private bool _effectsQuiesced;
|
private bool _effectsQuiesced;
|
||||||
|
private bool _selectionEdgeCommitted;
|
||||||
|
private bool _audioSuspended;
|
||||||
|
private bool _audioTransitionActive;
|
||||||
|
|
||||||
public WorldGenerationQuiescence(
|
public WorldGenerationQuiescence(
|
||||||
SelectionState selection,
|
SelectionState selection,
|
||||||
|
|
@ -99,26 +102,62 @@ internal sealed class WorldGenerationQuiescence
|
||||||
|
|
||||||
public void CommitBegin(in WorldGenerationQuiescenceEdge edge)
|
public void CommitBegin(in WorldGenerationQuiescenceEdge edge)
|
||||||
{
|
{
|
||||||
if (!edge.ShouldApply || _effectsQuiesced)
|
if (!edge.ShouldApply)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_effectsQuiesced = true;
|
_effectsQuiesced = true;
|
||||||
|
if (!_selectionEdgeCommitted)
|
||||||
|
{
|
||||||
|
// SelectionState commits its value before notifying observers.
|
||||||
|
// Record the edge first so a re-entrant reveal cannot replay it.
|
||||||
|
_selectionEdgeCommitted = true;
|
||||||
if (edge.ClearWorldSelection)
|
if (edge.ClearWorldSelection)
|
||||||
{
|
{
|
||||||
_selection.Clear(
|
_selection.Clear(
|
||||||
SelectionChangeSource.System,
|
SelectionChangeSource.System,
|
||||||
SelectionChangeReason.Cleared);
|
SelectionChangeReason.Cleared);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_audio?.SuspendWorldAudio();
|
ReconcileAudio();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ObserveReleased()
|
public void ObserveReleased()
|
||||||
{
|
{
|
||||||
if (!_effectsQuiesced)
|
if (!_effectsQuiesced
|
||||||
|
&& !_selectionEdgeCommitted
|
||||||
|
&& !_audioSuspended)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_effectsQuiesced = false;
|
_effectsQuiesced = false;
|
||||||
_audio?.ResumeWorldAudio();
|
ReconcileAudio();
|
||||||
|
if (!_effectsQuiesced && !_audioSuspended)
|
||||||
|
_selectionEdgeCommitted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReconcileAudio()
|
||||||
|
{
|
||||||
|
if (_audio is null || _audioTransitionActive)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_audioTransitionActive = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (_audioSuspended != _effectsQuiesced)
|
||||||
|
{
|
||||||
|
bool suspend = _effectsQuiesced;
|
||||||
|
if (suspend)
|
||||||
|
_audio.SuspendWorldAudio();
|
||||||
|
else
|
||||||
|
_audio.ResumeWorldAudio();
|
||||||
|
_audioSuspended = suspend;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_audioTransitionActive = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,31 @@ internal interface IWorldRevealRenderResourceScheduler
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class WorldRevealCoordinator
|
internal sealed class WorldRevealCoordinator
|
||||||
{
|
{
|
||||||
|
private sealed class HostProjection(
|
||||||
|
RuntimeWorldHostProjectionToken token,
|
||||||
|
int requiredRenderRadius,
|
||||||
|
in WorldGenerationQuiescenceEdge quiescenceEdge)
|
||||||
|
{
|
||||||
|
public RuntimeWorldHostProjectionToken Token { get; } = token;
|
||||||
|
public int RequiredRenderRadius { get; } = requiredRenderRadius;
|
||||||
|
public WorldGenerationQuiescenceEdge QuiescenceEdge { get; } =
|
||||||
|
quiescenceEdge;
|
||||||
|
public bool QuiescenceCommitted { get; set; }
|
||||||
|
public bool StreamingRegistered { get; set; }
|
||||||
|
public bool RenderResourcesRegistered { get; set; }
|
||||||
|
public bool SimulationReleaseProjected { get; set; }
|
||||||
|
public bool StreamingReleased { get; set; }
|
||||||
|
public bool RenderResourcesReleased { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
private readonly RuntimeWorldTransitState _transit;
|
private readonly RuntimeWorldTransitState _transit;
|
||||||
private readonly WorldRevealReadinessBarrier _readiness;
|
private readonly WorldRevealReadinessBarrier _readiness;
|
||||||
private readonly WorldGenerationQuiescence? _quiescence;
|
private readonly WorldGenerationQuiescence? _quiescence;
|
||||||
private readonly IWorldRevealStreamingScheduler? _streaming;
|
private readonly IWorldRevealStreamingScheduler? _streaming;
|
||||||
private readonly IWorldRevealRenderResourceScheduler? _renderResources;
|
private readonly IWorldRevealRenderResourceScheduler? _renderResources;
|
||||||
private long _reservationGeneration;
|
private readonly List<HostProjection> _hostProjections = [];
|
||||||
private bool _worldViewportReleased;
|
private bool _hostRetryActive;
|
||||||
|
private bool _hostRetryRequested;
|
||||||
|
|
||||||
public WorldRevealCoordinator(
|
public WorldRevealCoordinator(
|
||||||
RuntimeWorldTransitState transit,
|
RuntimeWorldTransitState transit,
|
||||||
|
|
@ -76,12 +94,15 @@ internal sealed class WorldRevealCoordinator
|
||||||
public int PortalMaterializationCount =>
|
public int PortalMaterializationCount =>
|
||||||
_transit.Snapshot.PortalMaterializationCount;
|
_transit.Snapshot.PortalMaterializationCount;
|
||||||
public bool WaitCueShown => _transit.Snapshot.WaitCueShown;
|
public bool WaitCueShown => _transit.Snapshot.WaitCueShown;
|
||||||
|
public RuntimeWorldTransitOwnershipSnapshot Ownership =>
|
||||||
|
_transit.Ownership;
|
||||||
|
|
||||||
public long BeginLogin(uint destinationCell)
|
public long BeginLogin(uint destinationCell)
|
||||||
{
|
{
|
||||||
if (destinationCell == 0u)
|
if (destinationCell == 0u)
|
||||||
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
||||||
|
|
||||||
|
WithdrawHostForReplacement();
|
||||||
WorldGenerationQuiescenceEdge quiescenceEdge =
|
WorldGenerationQuiescenceEdge quiescenceEdge =
|
||||||
_quiescence?.CaptureBegin() ?? default;
|
_quiescence?.CaptureBegin() ?? default;
|
||||||
_readiness.Begin();
|
_readiness.Begin();
|
||||||
|
|
@ -100,7 +121,15 @@ internal sealed class WorldRevealCoordinator
|
||||||
{
|
{
|
||||||
if (destinationCell == 0u)
|
if (destinationCell == 0u)
|
||||||
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
throw new ArgumentOutOfRangeException(nameof(destinationCell));
|
||||||
|
if (!_transit.CanBeginPortalReveal(
|
||||||
|
teleportSequence,
|
||||||
|
destinationCell))
|
||||||
|
{
|
||||||
|
generation = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WithdrawHostForReplacement();
|
||||||
WorldGenerationQuiescenceEdge quiescenceEdge =
|
WorldGenerationQuiescenceEdge quiescenceEdge =
|
||||||
_quiescence?.CaptureBegin() ?? default;
|
_quiescence?.CaptureBegin() ?? default;
|
||||||
if (!_transit.TryBeginPortalReveal(
|
if (!_transit.TryBeginPortalReveal(
|
||||||
|
|
@ -124,14 +153,21 @@ internal sealed class WorldRevealCoordinator
|
||||||
uint destinationCell,
|
uint destinationCell,
|
||||||
in WorldGenerationQuiescenceEdge quiescenceEdge)
|
in WorldGenerationQuiescenceEdge quiescenceEdge)
|
||||||
{
|
{
|
||||||
_quiescence?.CommitBegin(quiescenceEdge);
|
if (!_transit.TryRegisterHostProjection(
|
||||||
_reservationGeneration = generation;
|
|
||||||
_worldViewportReleased = false;
|
|
||||||
_streaming?.BeginDestinationReservation(
|
|
||||||
generation,
|
generation,
|
||||||
destinationCell,
|
destinationCell,
|
||||||
WorldRevealReadinessBarrier.RequiredRenderRadius(destinationCell));
|
out RuntimeWorldHostProjectionToken token))
|
||||||
_renderResources?.BeginDestinationReveal(generation);
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Runtime rejected the exact reveal host projection.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_hostProjections.Add(new HostProjection(
|
||||||
|
token,
|
||||||
|
WorldRevealReadinessBarrier.RequiredRenderRadius(
|
||||||
|
destinationCell),
|
||||||
|
quiescenceEdge));
|
||||||
|
RetryPendingHostWork();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -146,6 +182,7 @@ internal sealed class WorldRevealCoordinator
|
||||||
|
|
||||||
public WorldRevealReadinessSnapshot Evaluate(uint destinationCell)
|
public WorldRevealReadinessSnapshot Evaluate(uint destinationCell)
|
||||||
{
|
{
|
||||||
|
RetryPendingHostWork();
|
||||||
WorldRevealReadinessSnapshot snapshot = _readiness.Evaluate(destinationCell);
|
WorldRevealReadinessSnapshot snapshot = _readiness.Evaluate(destinationCell);
|
||||||
RuntimePortalSnapshot portal = _transit.Snapshot;
|
RuntimePortalSnapshot portal = _transit.Snapshot;
|
||||||
if (portal.Generation != 0)
|
if (portal.Generation != 0)
|
||||||
|
|
@ -189,17 +226,17 @@ internal sealed class WorldRevealCoordinator
|
||||||
ushort teleportSequence,
|
ushort teleportSequence,
|
||||||
uint destinationCell)
|
uint destinationCell)
|
||||||
{
|
{
|
||||||
bool wasAvailable = _transit.IsWorldSimulationAvailable;
|
|
||||||
bool acknowledged = _transit.AcknowledgePortalMaterialized(
|
bool acknowledged = _transit.AcknowledgePortalMaterialized(
|
||||||
generation,
|
generation,
|
||||||
teleportSequence,
|
teleportSequence,
|
||||||
destinationCell);
|
destinationCell);
|
||||||
ObserveSimulationRelease(wasAvailable);
|
RetryPendingHostWork();
|
||||||
return acknowledged;
|
return acknowledged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ObserveWorldViewportVisible()
|
public void ObserveWorldViewportVisible()
|
||||||
{
|
{
|
||||||
|
RetryPendingHostWork();
|
||||||
long generation = _transit.Snapshot.Generation;
|
long generation = _transit.Snapshot.Generation;
|
||||||
if (generation != 0)
|
if (generation != 0)
|
||||||
_transit.AcknowledgeWorldViewportVisible(generation);
|
_transit.AcknowledgeWorldViewportVisible(generation);
|
||||||
|
|
@ -220,13 +257,12 @@ internal sealed class WorldRevealCoordinator
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RevealWorldViewport()
|
public void RevealWorldViewport()
|
||||||
{
|
{
|
||||||
long generation = _reservationGeneration;
|
HostProjection? host = FindCurrentHostProjection();
|
||||||
if (generation == 0 || _worldViewportReleased)
|
if (host is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_streaming?.EndDestinationReservation(generation);
|
_transit.RequireDestinationReservationRelease(host.Token);
|
||||||
_renderResources?.EndDestinationReveal(generation);
|
RetryPendingHostWork();
|
||||||
_worldViewportReleased = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Complete()
|
public void Complete()
|
||||||
|
|
@ -235,10 +271,8 @@ internal sealed class WorldRevealCoordinator
|
||||||
if (generation == 0)
|
if (generation == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool wasAvailable = _transit.IsWorldSimulationAvailable;
|
|
||||||
_transit.Complete(generation);
|
_transit.Complete(generation);
|
||||||
ObserveSimulationRelease(wasAvailable);
|
RetryPendingHostWork();
|
||||||
EndHostLifetime();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Cancel()
|
public void Cancel()
|
||||||
|
|
@ -247,37 +281,284 @@ internal sealed class WorldRevealCoordinator
|
||||||
if (generation == 0)
|
if (generation == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool wasAvailable = _transit.IsWorldSimulationAvailable;
|
|
||||||
_transit.Cancel(generation);
|
_transit.Cancel(generation);
|
||||||
ObserveSimulationRelease(wasAvailable);
|
RetryPendingHostWork();
|
||||||
EndHostLifetime();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetSession()
|
public void ResetSession()
|
||||||
{
|
{
|
||||||
bool wasAvailable = _transit.IsWorldSimulationAvailable;
|
|
||||||
long generation = _transit.Snapshot.Generation;
|
long generation = _transit.Snapshot.Generation;
|
||||||
if (generation != 0)
|
if (generation != 0)
|
||||||
_transit.Cancel(generation);
|
_transit.Cancel(generation);
|
||||||
ObserveSimulationRelease(wasAvailable);
|
RetryPendingHostWork();
|
||||||
EndHostLifetime();
|
|
||||||
_transit.ResetSession();
|
_transit.ResetSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EndHostLifetime()
|
/// <summary>
|
||||||
|
/// Resumes the exact unfinished graphical-host suffix. Completed callback
|
||||||
|
/// stages never replay; a thrown callback leaves its Runtime
|
||||||
|
/// acknowledgement pending for the next call.
|
||||||
|
/// </summary>
|
||||||
|
internal void RetryPendingHostWork()
|
||||||
{
|
{
|
||||||
long generation = _reservationGeneration;
|
_hostRetryRequested = true;
|
||||||
if (generation == 0)
|
if (_hostRetryActive)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
RevealWorldViewport();
|
_hostRetryActive = true;
|
||||||
_reservationGeneration = 0;
|
try
|
||||||
_worldViewportReleased = false;
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
_hostRetryRequested = false;
|
||||||
|
int index = 0;
|
||||||
|
while (index < _hostProjections.Count)
|
||||||
|
{
|
||||||
|
HostProjection host = _hostProjections[index];
|
||||||
|
DrainHostProjection(host);
|
||||||
|
if (index < _hostProjections.Count
|
||||||
|
&& ReferenceEquals(
|
||||||
|
_hostProjections[index],
|
||||||
|
host))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (_hostRetryRequested);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_hostRetryActive = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ObserveSimulationRelease(bool wasAvailable)
|
private void DrainHostProjection(HostProjection host)
|
||||||
|
{
|
||||||
|
if (!TryGetHostProjection(host, out var projection))
|
||||||
|
return;
|
||||||
|
|
||||||
|
RuntimeWorldHostAcknowledgementStage pending =
|
||||||
|
projection.PendingAcknowledgements;
|
||||||
|
if ((pending & RuntimeWorldHostAcknowledgementStage
|
||||||
|
.ProjectionRegistered) != 0)
|
||||||
|
{
|
||||||
|
CompleteHostRegistration(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetHostProjection(host, out projection))
|
||||||
|
return;
|
||||||
|
pending = projection.PendingAcknowledgements;
|
||||||
|
if ((pending & RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected) != 0)
|
||||||
|
{
|
||||||
|
CompleteSimulationRelease(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetHostProjection(host, out projection))
|
||||||
|
return;
|
||||||
|
pending = projection.PendingAcknowledgements;
|
||||||
|
if ((pending & RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased) != 0)
|
||||||
|
{
|
||||||
|
CompleteDestinationReservationRelease(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetHostProjection(host, out projection))
|
||||||
|
return;
|
||||||
|
if ((projection.PendingAcknowledgements
|
||||||
|
& RuntimeWorldHostAcknowledgementStage.TerminalProjected)
|
||||||
|
== 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Acknowledge(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.TerminalProjected);
|
||||||
|
RemoveHostProjection(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteHostRegistration(HostProjection host)
|
||||||
|
{
|
||||||
|
if (!host.QuiescenceCommitted)
|
||||||
|
{
|
||||||
|
_quiescence?.CommitBegin(host.QuiescenceEdge);
|
||||||
|
host.QuiescenceCommitted = true;
|
||||||
|
}
|
||||||
|
if (!IsStagePending(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.ProjectionRegistered))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!host.StreamingRegistered)
|
||||||
|
{
|
||||||
|
_streaming?.BeginDestinationReservation(
|
||||||
|
host.Token.Generation,
|
||||||
|
host.Token.DestinationCell,
|
||||||
|
host.RequiredRenderRadius);
|
||||||
|
host.StreamingRegistered = true;
|
||||||
|
}
|
||||||
|
if (!IsStagePending(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.ProjectionRegistered))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!host.RenderResourcesRegistered)
|
||||||
|
{
|
||||||
|
_renderResources?.BeginDestinationReveal(
|
||||||
|
host.Token.Generation);
|
||||||
|
host.RenderResourcesRegistered = true;
|
||||||
|
}
|
||||||
|
if (!IsStagePending(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.ProjectionRegistered))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Acknowledge(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteSimulationRelease(HostProjection host)
|
||||||
|
{
|
||||||
|
if (!host.SimulationReleaseProjected)
|
||||||
{
|
{
|
||||||
if (!wasAvailable && _transit.IsWorldSimulationAvailable)
|
|
||||||
_quiescence?.ObserveReleased();
|
_quiescence?.ObserveReleased();
|
||||||
|
host.SimulationReleaseProjected = true;
|
||||||
|
}
|
||||||
|
if (!IsStagePending(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Acknowledge(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CompleteDestinationReservationRelease(HostProjection host)
|
||||||
|
{
|
||||||
|
if (!host.StreamingReleased)
|
||||||
|
{
|
||||||
|
if (host.StreamingRegistered)
|
||||||
|
{
|
||||||
|
_streaming?.EndDestinationReservation(
|
||||||
|
host.Token.Generation);
|
||||||
|
}
|
||||||
|
host.StreamingReleased = true;
|
||||||
|
}
|
||||||
|
if (!IsStagePending(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!host.RenderResourcesReleased)
|
||||||
|
{
|
||||||
|
if (host.RenderResourcesRegistered)
|
||||||
|
{
|
||||||
|
_renderResources?.EndDestinationReveal(
|
||||||
|
host.Token.Generation);
|
||||||
|
}
|
||||||
|
host.RenderResourcesReleased = true;
|
||||||
|
}
|
||||||
|
if (!IsStagePending(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Acknowledge(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithdrawHostForReplacement()
|
||||||
|
{
|
||||||
|
HostProjection? host = FindCurrentHostProjection();
|
||||||
|
if (host is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_transit.BeginHostProjectionSupersession(host.Token))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Runtime rejected reveal-host supersession.");
|
||||||
|
}
|
||||||
|
RetryPendingHostWork();
|
||||||
|
}
|
||||||
|
|
||||||
|
private HostProjection? FindCurrentHostProjection()
|
||||||
|
{
|
||||||
|
long generation = _transit.Snapshot.Generation;
|
||||||
|
for (int i = 0; i < _hostProjections.Count; i++)
|
||||||
|
{
|
||||||
|
HostProjection candidate = _hostProjections[i];
|
||||||
|
if (candidate.Token.Generation == generation)
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetHostProjection(
|
||||||
|
HostProjection host,
|
||||||
|
out RuntimeWorldHostProjectionSnapshot projection)
|
||||||
|
{
|
||||||
|
if (_transit.TryGetHostProjection(host.Token, out projection))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
RemoveHostProjection(host);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsStagePending(
|
||||||
|
HostProjection host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage stage) =>
|
||||||
|
_transit.TryGetHostProjection(
|
||||||
|
host.Token,
|
||||||
|
out RuntimeWorldHostProjectionSnapshot projection)
|
||||||
|
&& (projection.PendingAcknowledgements & stage) != 0;
|
||||||
|
|
||||||
|
private void RemoveHostProjection(HostProjection host)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _hostProjections.Count; i++)
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(_hostProjections[i], host))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_hostProjections.RemoveAt(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Acknowledge(
|
||||||
|
HostProjection host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage stage)
|
||||||
|
{
|
||||||
|
if (!_transit.AcknowledgeHostProjection(
|
||||||
|
new RuntimeWorldHostAcknowledgement(
|
||||||
|
host.Token,
|
||||||
|
stage)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Runtime rejected reveal host acknowledgement {stage} "
|
||||||
|
+ $"for generation {host.Token.Generation}.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -192,8 +192,19 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
|
||||||
$"{checkpoint.Environment.ActiveDayGroupIndex}:" +
|
$"{checkpoint.Environment.ActiveDayGroupIndex}:" +
|
||||||
$"{(int)checkpoint.Environment.Weather}:" +
|
$"{(int)checkpoint.Environment.Weather}:" +
|
||||||
$"{(int)checkpoint.Environment.EnvironOverride};" +
|
$"{(int)checkpoint.Environment.EnvironOverride};" +
|
||||||
|
$"environment-owner={checkpoint.EnvironmentOwnership.IsInitialized}:" +
|
||||||
|
$"{checkpoint.EnvironmentOwnership.DayGroupDefinitionCount}:" +
|
||||||
|
$"{checkpoint.EnvironmentOwnership.ActiveDayGroupCount};" +
|
||||||
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
|
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
|
||||||
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
|
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady};" +
|
||||||
|
$"transit-owner={checkpoint.TransitOwnership.BufferedTeleportDestinationCount}:" +
|
||||||
|
$"{checkpoint.TransitOwnership.PendingTeleportStartCount}:" +
|
||||||
|
$"{checkpoint.TransitOwnership.ActiveTeleportCount}:" +
|
||||||
|
$"{checkpoint.TransitOwnership.AcceptedTeleportDestinationCount}:" +
|
||||||
|
$"{checkpoint.TransitOwnership.ActiveRevealCount}:" +
|
||||||
|
$"{checkpoint.TransitOwnership.PendingDestinationReadinessCount}:" +
|
||||||
|
$"{checkpoint.TransitOwnership.HostProjectionCount}:" +
|
||||||
|
$"{checkpoint.TransitOwnership.PendingHostAcknowledgementCount}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnLifecycle(in RuntimeLifecycleDelta delta) =>
|
public void OnLifecycle(in RuntimeLifecycleDelta delta) =>
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,55 @@ public readonly record struct RuntimeDestinationReadiness(
|
||||||
&& IsCollisionReady));
|
&& IsCollisionReady));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum RuntimeWorldHostAcknowledgementStage
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
ProjectionRegistered = 1 << 0,
|
||||||
|
SimulationReleaseProjected = 1 << 1,
|
||||||
|
DestinationReservationReleased = 1 << 2,
|
||||||
|
TerminalProjected = 1 << 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exact identity of one graphical or direct host projection attached to a
|
||||||
|
/// Runtime reveal generation. It contains no presentation object.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct RuntimeWorldHostProjectionToken(
|
||||||
|
long Generation,
|
||||||
|
uint DestinationCell)
|
||||||
|
{
|
||||||
|
public bool IsValid => Generation != 0 && DestinationCell != 0u;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RuntimeWorldHostAcknowledgement(
|
||||||
|
RuntimeWorldHostProjectionToken Projection,
|
||||||
|
RuntimeWorldHostAcknowledgementStage Stage);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeWorldHostProjectionSnapshot(
|
||||||
|
RuntimeWorldHostProjectionToken Token,
|
||||||
|
RuntimeWorldHostAcknowledgementStage PendingAcknowledgements,
|
||||||
|
bool ProjectionRegistered,
|
||||||
|
bool SimulationReleaseProjected,
|
||||||
|
bool DestinationReservationReleased,
|
||||||
|
bool IsSuperseding)
|
||||||
|
{
|
||||||
|
public int PendingAcknowledgementCount =>
|
||||||
|
Count(PendingAcknowledgements);
|
||||||
|
|
||||||
|
private static int Count(RuntimeWorldHostAcknowledgementStage stages)
|
||||||
|
{
|
||||||
|
int value = (int)stages;
|
||||||
|
int count = 0;
|
||||||
|
while (value != 0)
|
||||||
|
{
|
||||||
|
value &= value - 1;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public readonly record struct RuntimePortalSnapshot(
|
public readonly record struct RuntimePortalSnapshot(
|
||||||
long Generation,
|
long Generation,
|
||||||
RuntimePortalKind Kind,
|
RuntimePortalKind Kind,
|
||||||
|
|
@ -166,6 +215,7 @@ public readonly record struct RuntimePortalSnapshot(
|
||||||
public interface IRuntimePortalView
|
public interface IRuntimePortalView
|
||||||
{
|
{
|
||||||
RuntimePortalSnapshot Snapshot { get; }
|
RuntimePortalSnapshot Snapshot { get; }
|
||||||
|
RuntimeWorldTransitOwnershipSnapshot Ownership { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly record struct RuntimeStateCheckpoint(
|
public readonly record struct RuntimeStateCheckpoint(
|
||||||
|
|
@ -184,7 +234,9 @@ public readonly record struct RuntimeStateCheckpoint(
|
||||||
RuntimeActionSnapshot Actions,
|
RuntimeActionSnapshot Actions,
|
||||||
RuntimeMovementSnapshot Movement,
|
RuntimeMovementSnapshot Movement,
|
||||||
RuntimeWorldEnvironmentSnapshot Environment,
|
RuntimeWorldEnvironmentSnapshot Environment,
|
||||||
RuntimePortalSnapshot Portal);
|
RuntimeWorldEnvironmentOwnershipSnapshot EnvironmentOwnership,
|
||||||
|
RuntimePortalSnapshot Portal,
|
||||||
|
RuntimeWorldTransitOwnershipSnapshot TransitOwnership);
|
||||||
|
|
||||||
public interface IGameRuntimeView
|
public interface IGameRuntimeView
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -96,9 +96,15 @@ public readonly record struct RuntimeWorldEnvironmentSnapshot(
|
||||||
WeatherKind Weather,
|
WeatherKind Weather,
|
||||||
EnvironOverride EnvironOverride);
|
EnvironOverride EnvironOverride);
|
||||||
|
|
||||||
|
public readonly record struct RuntimeWorldEnvironmentOwnershipSnapshot(
|
||||||
|
bool IsInitialized,
|
||||||
|
int DayGroupDefinitionCount,
|
||||||
|
int ActiveDayGroupCount);
|
||||||
|
|
||||||
public interface IRuntimeWorldEnvironmentView
|
public interface IRuntimeWorldEnvironmentView
|
||||||
{
|
{
|
||||||
RuntimeWorldEnvironmentSnapshot Snapshot { get; }
|
RuntimeWorldEnvironmentSnapshot Snapshot { get; }
|
||||||
|
RuntimeWorldEnvironmentOwnershipSnapshot Ownership { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -152,6 +158,15 @@ public sealed class RuntimeWorldEnvironmentState
|
||||||
Weather.Kind,
|
Weather.Kind,
|
||||||
Weather.Override);
|
Weather.Override);
|
||||||
|
|
||||||
|
public RuntimeWorldEnvironmentOwnershipSnapshot Ownership =>
|
||||||
|
CaptureOwnership();
|
||||||
|
|
||||||
|
public RuntimeWorldEnvironmentOwnershipSnapshot CaptureOwnership() =>
|
||||||
|
new(
|
||||||
|
IsInitialized,
|
||||||
|
_definition?.DayGroups.Count ?? 0,
|
||||||
|
ActiveDayGroupIndex >= 0 ? 1 : 0);
|
||||||
|
|
||||||
public void Initialize(RuntimeWorldEnvironmentDefinition definition)
|
public void Initialize(RuntimeWorldEnvironmentDefinition definition)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(definition);
|
ArgumentNullException.ThrowIfNull(definition);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,27 @@ using System.Globalization;
|
||||||
|
|
||||||
namespace AcDream.Runtime.World;
|
namespace AcDream.Runtime.World;
|
||||||
|
|
||||||
|
public readonly record struct RuntimeWorldTransitOwnershipSnapshot(
|
||||||
|
int BufferedTeleportDestinationCount,
|
||||||
|
int PendingTeleportStartCount,
|
||||||
|
int ActiveTeleportCount,
|
||||||
|
int AcceptedTeleportDestinationCount,
|
||||||
|
int ActiveRevealCount,
|
||||||
|
int PendingDestinationReadinessCount,
|
||||||
|
int HostProjectionCount,
|
||||||
|
int PendingHostAcknowledgementCount)
|
||||||
|
{
|
||||||
|
public bool IsSessionIdle =>
|
||||||
|
BufferedTeleportDestinationCount == 0
|
||||||
|
&& PendingTeleportStartCount == 0
|
||||||
|
&& ActiveTeleportCount == 0
|
||||||
|
&& AcceptedTeleportDestinationCount == 0
|
||||||
|
&& ActiveRevealCount == 0
|
||||||
|
&& PendingDestinationReadinessCount == 0
|
||||||
|
&& HostProjectionCount == 0
|
||||||
|
&& PendingHostAcknowledgementCount == 0;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Canonical presentation-independent owner of local F751/Position
|
/// Canonical presentation-independent owner of local F751/Position
|
||||||
/// correlation and one login/portal reveal generation. The graphical or
|
/// correlation and one login/portal reveal generation. The graphical or
|
||||||
|
|
@ -21,12 +42,34 @@ namespace AcDream.Runtime.World;
|
||||||
public sealed class RuntimeWorldTransitState
|
public sealed class RuntimeWorldTransitState
|
||||||
: IRuntimePortalView
|
: IRuntimePortalView
|
||||||
{
|
{
|
||||||
|
private sealed class HostProjectionRecord(
|
||||||
|
RuntimeWorldHostProjectionToken token)
|
||||||
|
{
|
||||||
|
public RuntimeWorldHostProjectionToken Token { get; } = token;
|
||||||
|
public RuntimeWorldHostAcknowledgementStage Pending { get; set; } =
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered;
|
||||||
|
public bool ProjectionRegistered { get; set; }
|
||||||
|
public bool SimulationReleaseProjected { get; set; }
|
||||||
|
public bool DestinationReservationReleased { get; set; }
|
||||||
|
public bool IsSuperseding { get; set; }
|
||||||
|
|
||||||
|
public RuntimeWorldHostProjectionSnapshot Snapshot => new(
|
||||||
|
Token,
|
||||||
|
Pending,
|
||||||
|
ProjectionRegistered,
|
||||||
|
SimulationReleaseProjected,
|
||||||
|
DestinationReservationReleased,
|
||||||
|
IsSuperseding);
|
||||||
|
}
|
||||||
|
|
||||||
public static readonly TimeSpan RetailWaitCueDelay =
|
public static readonly TimeSpan RetailWaitCueDelay =
|
||||||
TimeSpan.FromSeconds(5);
|
TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
private readonly Action<string> _log;
|
private readonly Action<string> _log;
|
||||||
private readonly Dictionary<ushort, RuntimeTeleportDestination>
|
private readonly Dictionary<ushort, RuntimeTeleportDestination>
|
||||||
_bufferedDestinations = [];
|
_bufferedDestinations = [];
|
||||||
|
private readonly Dictionary<long, HostProjectionRecord>
|
||||||
|
_hostProjections = [];
|
||||||
private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle;
|
private RuntimePortalSnapshot _snapshot = RuntimePortalSnapshot.Idle;
|
||||||
private long _nextGeneration;
|
private long _nextGeneration;
|
||||||
private bool _hasLastTeleportStart;
|
private bool _hasLastTeleportStart;
|
||||||
|
|
@ -45,6 +88,8 @@ public sealed class RuntimeWorldTransitState
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimePortalSnapshot Snapshot => _snapshot;
|
public RuntimePortalSnapshot Snapshot => _snapshot;
|
||||||
|
public RuntimeWorldTransitOwnershipSnapshot Ownership =>
|
||||||
|
CaptureOwnership();
|
||||||
public bool IsWorldSimulationAvailable =>
|
public bool IsWorldSimulationAvailable =>
|
||||||
_snapshot.WorldSimulationAvailable;
|
_snapshot.WorldSimulationAvailable;
|
||||||
public bool IsTeleportActive => _teleportActive;
|
public bool IsTeleportActive => _teleportActive;
|
||||||
|
|
@ -57,6 +102,30 @@ public sealed class RuntimeWorldTransitState
|
||||||
public int DiagnosticFailureCount { get; private set; }
|
public int DiagnosticFailureCount { get; private set; }
|
||||||
public Exception? LastDiagnosticFailure { get; private set; }
|
public Exception? LastDiagnosticFailure { get; private set; }
|
||||||
|
|
||||||
|
public RuntimeWorldTransitOwnershipSnapshot CaptureOwnership()
|
||||||
|
{
|
||||||
|
int pendingHostAcknowledgements = 0;
|
||||||
|
foreach (HostProjectionRecord host in _hostProjections.Values)
|
||||||
|
{
|
||||||
|
pendingHostAcknowledgements = checked(
|
||||||
|
pendingHostAcknowledgements
|
||||||
|
+ host.Snapshot.PendingAcknowledgementCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool revealActive = _snapshot.Generation != 0
|
||||||
|
&& !_snapshot.Completed
|
||||||
|
&& !_snapshot.Cancelled;
|
||||||
|
return new RuntimeWorldTransitOwnershipSnapshot(
|
||||||
|
_bufferedDestinations.Count,
|
||||||
|
_hasPendingTeleportStart ? 1 : 0,
|
||||||
|
_teleportActive ? 1 : 0,
|
||||||
|
_hasAcceptedDestination ? 1 : 0,
|
||||||
|
revealActive ? 1 : 0,
|
||||||
|
revealActive && !_snapshot.IsReady ? 1 : 0,
|
||||||
|
_hostProjections.Count,
|
||||||
|
pendingHostAcknowledgements);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Starts initial-login reveal. Portal reveals must instead claim the
|
/// Starts initial-login reveal. Portal reveals must instead claim the
|
||||||
/// exact F751-correlated destination through
|
/// exact F751-correlated destination through
|
||||||
|
|
@ -69,15 +138,30 @@ public sealed class RuntimeWorldTransitState
|
||||||
/// Atomically claims the one accepted Position paired with the active F751
|
/// Atomically claims the one accepted Position paired with the active F751
|
||||||
/// sequence and starts its reveal generation.
|
/// sequence and starts its reveal generation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public bool CanBeginPortalReveal(
|
||||||
|
ushort teleportSequence,
|
||||||
|
uint destinationCell)
|
||||||
|
{
|
||||||
|
if (!_teleportActive
|
||||||
|
|| teleportSequence != _activeTeleportSequence
|
||||||
|
|| !_hasAcceptedDestination)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeTeleportDestination destination = _acceptedDestination;
|
||||||
|
return destination.TeleportSequence == teleportSequence
|
||||||
|
&& destination.CellId != 0u
|
||||||
|
&& destination.CellId == destinationCell;
|
||||||
|
}
|
||||||
|
|
||||||
public bool TryBeginPortalReveal(
|
public bool TryBeginPortalReveal(
|
||||||
ushort teleportSequence,
|
ushort teleportSequence,
|
||||||
uint destinationCell,
|
uint destinationCell,
|
||||||
out long generation)
|
out long generation)
|
||||||
{
|
{
|
||||||
generation = 0;
|
generation = 0;
|
||||||
if (!_teleportActive
|
if (!CanBeginPortalReveal(teleportSequence, destinationCell))
|
||||||
|| teleportSequence != _activeTeleportSequence
|
|
||||||
|| !_hasAcceptedDestination)
|
|
||||||
{
|
{
|
||||||
LogRejected(
|
LogRejected(
|
||||||
"portal-begin-without-destination",
|
"portal-begin-without-destination",
|
||||||
|
|
@ -89,19 +173,6 @@ public sealed class RuntimeWorldTransitState
|
||||||
}
|
}
|
||||||
|
|
||||||
RuntimeTeleportDestination destination = _acceptedDestination;
|
RuntimeTeleportDestination destination = _acceptedDestination;
|
||||||
if (destination.TeleportSequence != teleportSequence
|
|
||||||
|| destination.CellId == 0u
|
|
||||||
|| destination.CellId != destinationCell)
|
|
||||||
{
|
|
||||||
FailInvariant(
|
|
||||||
"portal-begin-destination-mismatch",
|
|
||||||
$"sequence={teleportSequence} "
|
|
||||||
+ $"acceptedSequence={destination.TeleportSequence} "
|
|
||||||
+ $"expectedCell=0x{destination.CellId:X8} "
|
|
||||||
+ $"actualCell=0x{destinationCell:X8}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
generation = BeginRevealCore(
|
generation = BeginRevealCore(
|
||||||
RuntimePortalKind.Portal,
|
RuntimePortalKind.Portal,
|
||||||
destinationCell);
|
destinationCell);
|
||||||
|
|
@ -110,6 +181,159 @@ public sealed class RuntimeWorldTransitState
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers one presentation-independent host projection against the
|
||||||
|
/// exact active reveal. Graphical and direct hosts receive the same token.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryRegisterHostProjection(
|
||||||
|
long generation,
|
||||||
|
uint destinationCell,
|
||||||
|
out RuntimeWorldHostProjectionToken token)
|
||||||
|
{
|
||||||
|
token = new RuntimeWorldHostProjectionToken(
|
||||||
|
generation,
|
||||||
|
destinationCell);
|
||||||
|
if (!token.IsValid
|
||||||
|
|| generation != _snapshot.Generation
|
||||||
|
|| destinationCell != _snapshot.DestinationCell
|
||||||
|
|| _snapshot.Cancelled
|
||||||
|
|| _snapshot.Completed)
|
||||||
|
{
|
||||||
|
LogRejected(
|
||||||
|
"host-register-mismatch",
|
||||||
|
$"generation={generation} "
|
||||||
|
+ $"cell=0x{destinationCell:X8}");
|
||||||
|
token = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hostProjections.TryGetValue(
|
||||||
|
generation,
|
||||||
|
out HostProjectionRecord? existing))
|
||||||
|
{
|
||||||
|
if (existing.Token == token)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
LogRejected(
|
||||||
|
"host-register-conflict",
|
||||||
|
$"generation={generation} "
|
||||||
|
+ $"expectedCell=0x{existing.Token.DestinationCell:X8} "
|
||||||
|
+ $"actualCell=0x{destinationCell:X8}");
|
||||||
|
token = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hostProjections.Add(
|
||||||
|
generation,
|
||||||
|
new HostProjectionRecord(token));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetHostProjection(
|
||||||
|
in RuntimeWorldHostProjectionToken token,
|
||||||
|
out RuntimeWorldHostProjectionSnapshot projection)
|
||||||
|
{
|
||||||
|
if (token.IsValid
|
||||||
|
&& _hostProjections.TryGetValue(
|
||||||
|
token.Generation,
|
||||||
|
out HostProjectionRecord? host)
|
||||||
|
&& host.Token == token)
|
||||||
|
{
|
||||||
|
projection = host.Snapshot;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
projection = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks the graphical reservation-release callback as due before the
|
||||||
|
/// host executes it. A thrown callback therefore leaves an exact pending
|
||||||
|
/// acknowledgement instead of losing the unfinished suffix.
|
||||||
|
/// </summary>
|
||||||
|
public bool RequireDestinationReservationRelease(
|
||||||
|
in RuntimeWorldHostProjectionToken token)
|
||||||
|
{
|
||||||
|
if (!TryGetHostRecord(token, out HostProjectionRecord? host))
|
||||||
|
return false;
|
||||||
|
if (host.DestinationReservationReleased)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
host.Pending |= RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an older host projection into an exact withdrawal transaction
|
||||||
|
/// while a replacement reveal keeps world simulation quiesced.
|
||||||
|
/// </summary>
|
||||||
|
public bool BeginHostProjectionSupersession(
|
||||||
|
in RuntimeWorldHostProjectionToken token)
|
||||||
|
{
|
||||||
|
if (!TryGetHostRecord(token, out HostProjectionRecord? host))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
BeginHostProjectionSupersession(host);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AcknowledgeHostProjection(
|
||||||
|
in RuntimeWorldHostAcknowledgement acknowledgement)
|
||||||
|
{
|
||||||
|
RuntimeWorldHostAcknowledgementStage stage =
|
||||||
|
acknowledgement.Stage;
|
||||||
|
if (!IsSingleStage(stage)
|
||||||
|
|| !TryGetHostRecord(
|
||||||
|
acknowledgement.Projection,
|
||||||
|
out HostProjectionRecord? host)
|
||||||
|
|| (host.Pending & stage) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (stage)
|
||||||
|
{
|
||||||
|
case RuntimeWorldHostAcknowledgementStage.ProjectionRegistered:
|
||||||
|
if (host.IsSuperseding)
|
||||||
|
return false;
|
||||||
|
host.ProjectionRegistered = true;
|
||||||
|
break;
|
||||||
|
case RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected:
|
||||||
|
if (host.Token.Generation == _snapshot.Generation
|
||||||
|
&& !_snapshot.WorldSimulationAvailable)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
host.SimulationReleaseProjected = true;
|
||||||
|
break;
|
||||||
|
case RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased:
|
||||||
|
host.DestinationReservationReleased = true;
|
||||||
|
break;
|
||||||
|
case RuntimeWorldHostAcknowledgementStage.TerminalProjected:
|
||||||
|
if (!host.IsSuperseding
|
||||||
|
&& (host.Token.Generation != _snapshot.Generation
|
||||||
|
|| (!_snapshot.Completed
|
||||||
|
&& !_snapshot.Cancelled)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((host.Pending & ~stage) != 0)
|
||||||
|
return false;
|
||||||
|
host.Pending &= ~stage;
|
||||||
|
_hostProjections.Remove(host.Token.Generation);
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
host.Pending &= ~stage;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private long BeginRevealCore(
|
private long BeginRevealCore(
|
||||||
RuntimePortalKind kind,
|
RuntimePortalKind kind,
|
||||||
uint destinationCell)
|
uint destinationCell)
|
||||||
|
|
@ -125,6 +349,12 @@ public sealed class RuntimeWorldTransitState
|
||||||
{
|
{
|
||||||
Log("superseded", _snapshot);
|
Log("superseded", _snapshot);
|
||||||
}
|
}
|
||||||
|
if (_hostProjections.TryGetValue(
|
||||||
|
_snapshot.Generation,
|
||||||
|
out HostProjectionRecord? superseded))
|
||||||
|
{
|
||||||
|
BeginHostProjectionSupersession(superseded);
|
||||||
|
}
|
||||||
|
|
||||||
long generation = checked(++_nextGeneration);
|
long generation = checked(++_nextGeneration);
|
||||||
int failures = _snapshot.InvariantFailureCount;
|
int failures = _snapshot.InvariantFailureCount;
|
||||||
|
|
@ -372,6 +602,10 @@ public sealed class RuntimeWorldTransitState
|
||||||
WorldSimulationAvailable = true,
|
WorldSimulationAvailable = true,
|
||||||
PortalMaterializationCount = count,
|
PortalMaterializationCount = count,
|
||||||
};
|
};
|
||||||
|
RequireHostStage(
|
||||||
|
generation,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected);
|
||||||
Log("materialized", _snapshot);
|
Log("materialized", _snapshot);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -444,11 +678,16 @@ public sealed class RuntimeWorldTransitState
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool simulationWasAvailable =
|
||||||
|
_snapshot.WorldSimulationAvailable;
|
||||||
_snapshot = _snapshot with
|
_snapshot = _snapshot with
|
||||||
{
|
{
|
||||||
Completed = true,
|
Completed = true,
|
||||||
WorldSimulationAvailable = true,
|
WorldSimulationAvailable = true,
|
||||||
};
|
};
|
||||||
|
RequireTerminalHostProjection(
|
||||||
|
generation,
|
||||||
|
requireSimulationRelease: !simulationWasAvailable);
|
||||||
Log("complete", _snapshot);
|
Log("complete", _snapshot);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -463,11 +702,16 @@ public sealed class RuntimeWorldTransitState
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool simulationWasAvailable =
|
||||||
|
_snapshot.WorldSimulationAvailable;
|
||||||
_snapshot = _snapshot with
|
_snapshot = _snapshot with
|
||||||
{
|
{
|
||||||
Cancelled = true,
|
Cancelled = true,
|
||||||
WorldSimulationAvailable = true,
|
WorldSimulationAvailable = true,
|
||||||
};
|
};
|
||||||
|
RequireTerminalHostProjection(
|
||||||
|
generation,
|
||||||
|
requireSimulationRelease: !simulationWasAvailable);
|
||||||
Log("cancel", _snapshot);
|
Log("cancel", _snapshot);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -479,6 +723,17 @@ public sealed class RuntimeWorldTransitState
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ResetSession()
|
public void ResetSession()
|
||||||
{
|
{
|
||||||
|
if (_hostProjections.Count != 0)
|
||||||
|
{
|
||||||
|
RuntimeWorldTransitOwnershipSnapshot ownership =
|
||||||
|
CaptureOwnership();
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"World transit cannot reset while a host projection "
|
||||||
|
+ "still owns an acknowledgement suffix "
|
||||||
|
+ $"(hosts={ownership.HostProjectionCount}, "
|
||||||
|
+ $"pending={ownership.PendingHostAcknowledgementCount}).");
|
||||||
|
}
|
||||||
|
|
||||||
_snapshot = RuntimePortalSnapshot.Idle;
|
_snapshot = RuntimePortalSnapshot.Idle;
|
||||||
EndTeleport();
|
EndTeleport();
|
||||||
_hasLastTeleportStart = false;
|
_hasLastTeleportStart = false;
|
||||||
|
|
@ -486,6 +741,105 @@ public sealed class RuntimeWorldTransitState
|
||||||
_bufferedDestinations.Clear();
|
_bufferedDestinations.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryGetHostRecord(
|
||||||
|
in RuntimeWorldHostProjectionToken token,
|
||||||
|
out HostProjectionRecord host)
|
||||||
|
{
|
||||||
|
if (token.IsValid
|
||||||
|
&& _hostProjections.TryGetValue(
|
||||||
|
token.Generation,
|
||||||
|
out HostProjectionRecord? candidate)
|
||||||
|
&& candidate.Token == token)
|
||||||
|
{
|
||||||
|
host = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
host = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RequireHostStage(
|
||||||
|
long generation,
|
||||||
|
RuntimeWorldHostAcknowledgementStage stage)
|
||||||
|
{
|
||||||
|
if (!_hostProjections.TryGetValue(
|
||||||
|
generation,
|
||||||
|
out HostProjectionRecord? host))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool alreadyAcknowledged = stage switch
|
||||||
|
{
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered =>
|
||||||
|
host.ProjectionRegistered,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected =>
|
||||||
|
host.SimulationReleaseProjected,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased =>
|
||||||
|
host.DestinationReservationReleased,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if (!alreadyAcknowledged)
|
||||||
|
host.Pending |= stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RequireTerminalHostProjection(
|
||||||
|
long generation,
|
||||||
|
bool requireSimulationRelease)
|
||||||
|
{
|
||||||
|
if (!_hostProjections.TryGetValue(
|
||||||
|
generation,
|
||||||
|
out HostProjectionRecord? host))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A host that is terminating no longer has to finish acquiring a
|
||||||
|
// projection. It must instead release every resource it did acquire.
|
||||||
|
host.Pending &=
|
||||||
|
~RuntimeWorldHostAcknowledgementStage.ProjectionRegistered;
|
||||||
|
if (requireSimulationRelease
|
||||||
|
&& !host.SimulationReleaseProjected)
|
||||||
|
{
|
||||||
|
host.Pending |= RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected;
|
||||||
|
}
|
||||||
|
if (!host.DestinationReservationReleased)
|
||||||
|
{
|
||||||
|
host.Pending |= RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased;
|
||||||
|
}
|
||||||
|
host.Pending |=
|
||||||
|
RuntimeWorldHostAcknowledgementStage.TerminalProjected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BeginHostProjectionSupersession(
|
||||||
|
HostProjectionRecord host)
|
||||||
|
{
|
||||||
|
host.IsSuperseding = true;
|
||||||
|
host.Pending &=
|
||||||
|
~(RuntimeWorldHostAcknowledgementStage.ProjectionRegistered
|
||||||
|
| RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected);
|
||||||
|
if (!host.DestinationReservationReleased)
|
||||||
|
{
|
||||||
|
host.Pending |= RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased;
|
||||||
|
}
|
||||||
|
host.Pending |=
|
||||||
|
RuntimeWorldHostAcknowledgementStage.TerminalProjected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSingleStage(
|
||||||
|
RuntimeWorldHostAcknowledgementStage stage)
|
||||||
|
{
|
||||||
|
int value = (int)stage;
|
||||||
|
return value != 0 && (value & (value - 1)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
private bool IsCurrentPortalDestination(
|
private bool IsCurrentPortalDestination(
|
||||||
long generation,
|
long generation,
|
||||||
ushort teleportSequence,
|
ushort teleportSequence,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.UI.Testing;
|
using AcDream.App.UI.Testing;
|
||||||
using AcDream.Runtime;
|
using AcDream.Runtime;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Diagnostics;
|
namespace AcDream.App.Tests.Diagnostics;
|
||||||
|
|
@ -179,6 +180,19 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
Path.Combine(directory, "screenshots"));
|
Path.Combine(directory, "screenshots"));
|
||||||
var controller = new WorldLifecycleAutomationController(
|
var controller = new WorldLifecycleAutomationController(
|
||||||
() => reveal,
|
() => reveal,
|
||||||
|
() => new RuntimeWorldEnvironmentOwnershipSnapshot(
|
||||||
|
IsInitialized: true,
|
||||||
|
DayGroupDefinitionCount: 4,
|
||||||
|
ActiveDayGroupCount: 1),
|
||||||
|
() => new RuntimeWorldTransitOwnershipSnapshot(
|
||||||
|
BufferedTeleportDestinationCount: 0,
|
||||||
|
PendingTeleportStartCount: 0,
|
||||||
|
ActiveTeleportCount: 0,
|
||||||
|
AcceptedTeleportDestinationCount: 0,
|
||||||
|
ActiveRevealCount: 0,
|
||||||
|
PendingDestinationReadinessCount: 0,
|
||||||
|
HostProjectionCount: 0,
|
||||||
|
PendingHostAcknowledgementCount: 0),
|
||||||
() => 3,
|
() => 3,
|
||||||
_ => resources,
|
_ => resources,
|
||||||
screenshots,
|
screenshots,
|
||||||
|
|
@ -205,6 +219,15 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
using JsonDocument json = JsonDocument.Parse(line);
|
using JsonDocument json = JsonDocument.Parse(line);
|
||||||
Assert.Equal("dungeon", json.RootElement.GetProperty("name").GetString());
|
Assert.Equal("dungeon", json.RootElement.GetProperty("name").GetString());
|
||||||
Assert.Equal(7, json.RootElement.GetProperty("reveal").GetProperty("generation").GetInt64());
|
Assert.Equal(7, json.RootElement.GetProperty("reveal").GetProperty("generation").GetInt64());
|
||||||
|
Assert.Equal(
|
||||||
|
4,
|
||||||
|
json.RootElement.GetProperty("environmentOwnership")
|
||||||
|
.GetProperty("dayGroupDefinitionCount").GetInt32());
|
||||||
|
Assert.Equal(
|
||||||
|
0,
|
||||||
|
json.RootElement.GetProperty("transitOwnership")
|
||||||
|
.GetProperty("pendingHostAcknowledgementCount")
|
||||||
|
.GetInt32());
|
||||||
Assert.Equal(4, json.RootElement.GetProperty("render").GetProperty("world")
|
Assert.Equal(4, json.RootElement.GetProperty("render").GetProperty("world")
|
||||||
.GetProperty("visibleLandblocks").GetInt32());
|
.GetProperty("visibleLandblocks").GetInt32());
|
||||||
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
|
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
|
||||||
|
|
@ -373,6 +396,8 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
string directory,
|
string directory,
|
||||||
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> capture) =>
|
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> capture) =>
|
||||||
new(
|
new(
|
||||||
|
() => default,
|
||||||
|
() => default,
|
||||||
() => default,
|
() => default,
|
||||||
() => 0,
|
() => 0,
|
||||||
capture,
|
capture,
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,34 @@ public sealed class RuntimeWorldTransitOwnershipTests
|
||||||
coordinatorFields,
|
coordinatorFields,
|
||||||
field => field.Name is "_activeGeneration"
|
field => field.Name is "_activeGeneration"
|
||||||
or "_worldSimulationReleased"
|
or "_worldSimulationReleased"
|
||||||
or "_lifecycle");
|
or "_lifecycle"
|
||||||
|
or "_reservationGeneration"
|
||||||
|
or "_worldViewportReleased"
|
||||||
|
or "_hostProjection");
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
coordinatorFields,
|
||||||
|
field => field.FieldType == typeof(RuntimePortalSnapshot)
|
||||||
|
|| field.FieldType == typeof(RuntimeDestinationReadiness)
|
||||||
|
|| field.FieldType == typeof(long)
|
||||||
|
|| field.FieldType == typeof(uint));
|
||||||
|
|
||||||
|
Type hostProjection = Assert.Single(
|
||||||
|
typeof(WorldRevealCoordinator).GetNestedTypes(
|
||||||
|
System.Reflection.BindingFlags.NonPublic),
|
||||||
|
type => type.Name == "HostProjection");
|
||||||
|
var hostFields = hostProjection.GetFields(
|
||||||
|
System.Reflection.BindingFlags.Instance
|
||||||
|
| System.Reflection.BindingFlags.NonPublic);
|
||||||
|
Assert.Single(
|
||||||
|
hostFields,
|
||||||
|
field => field.FieldType
|
||||||
|
== typeof(RuntimeWorldHostProjectionToken));
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
hostFields,
|
||||||
|
field => field.FieldType == typeof(RuntimePortalSnapshot)
|
||||||
|
|| field.FieldType == typeof(RuntimeDestinationReadiness)
|
||||||
|
|| field.FieldType == typeof(long)
|
||||||
|
|| field.FieldType == typeof(uint));
|
||||||
|
|
||||||
var teleportFields = typeof(LocalPlayerTeleportController)
|
var teleportFields = typeof(LocalPlayerTeleportController)
|
||||||
.GetFields(
|
.GetFields(
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,39 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
Assert.Equal(inventoryGuid, selection.SelectedObjectId);
|
Assert.Equal(inventoryGuid, selection.SelectedObjectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReentrantBeginDuringAudioResume_ReconcilesToNewestEdge()
|
||||||
|
{
|
||||||
|
var transit = new RuntimeWorldTransitState();
|
||||||
|
var world = new GpuWorldState(
|
||||||
|
availability: new WorldGenerationAvailabilityState(transit));
|
||||||
|
var audio = new ReentrantAudioQuiescence();
|
||||||
|
var quiescence = new WorldGenerationQuiescence(
|
||||||
|
new SelectionState(),
|
||||||
|
world,
|
||||||
|
_ => null,
|
||||||
|
audio);
|
||||||
|
var edge = new WorldGenerationQuiescenceEdge(
|
||||||
|
ShouldApply: true,
|
||||||
|
ClearWorldSelection: false);
|
||||||
|
quiescence.CommitBegin(edge);
|
||||||
|
audio.OnResume = () =>
|
||||||
|
{
|
||||||
|
audio.OnResume = null;
|
||||||
|
quiescence.CommitBegin(edge);
|
||||||
|
};
|
||||||
|
|
||||||
|
quiescence.ObserveReleased();
|
||||||
|
|
||||||
|
Assert.Equal(2, audio.SuspendCalls);
|
||||||
|
Assert.Equal(1, audio.ResumeCalls);
|
||||||
|
|
||||||
|
quiescence.ObserveReleased();
|
||||||
|
|
||||||
|
Assert.Equal(2, audio.SuspendCalls);
|
||||||
|
Assert.Equal(2, audio.ResumeCalls);
|
||||||
|
}
|
||||||
|
|
||||||
private static RuntimeDestinationReadiness Ready(
|
private static RuntimeDestinationReadiness Ready(
|
||||||
long generation,
|
long generation,
|
||||||
uint destinationCell) =>
|
uint destinationCell) =>
|
||||||
|
|
@ -178,4 +211,19 @@ public sealed class WorldGenerationQuiescenceTests
|
||||||
public void SuspendWorldAudio() => SuspendCalls++;
|
public void SuspendWorldAudio() => SuspendCalls++;
|
||||||
public void ResumeWorldAudio() => ResumeCalls++;
|
public void ResumeWorldAudio() => ResumeCalls++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class ReentrantAudioQuiescence : IWorldAudioQuiescence
|
||||||
|
{
|
||||||
|
public int SuspendCalls { get; private set; }
|
||||||
|
public int ResumeCalls { get; private set; }
|
||||||
|
public Action? OnResume { get; set; }
|
||||||
|
|
||||||
|
public void SuspendWorldAudio() => SuspendCalls++;
|
||||||
|
|
||||||
|
public void ResumeWorldAudio()
|
||||||
|
{
|
||||||
|
ResumeCalls++;
|
||||||
|
OnResume?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -409,8 +409,9 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
(second, 0x20210123u, 0),
|
(second, 0x20210123u, 0),
|
||||||
],
|
],
|
||||||
scheduler.Begins);
|
scheduler.Begins);
|
||||||
coordinator.Complete();
|
Assert.Equal([first], scheduler.Ends);
|
||||||
Assert.Equal([second], scheduler.Ends);
|
coordinator.Cancel();
|
||||||
|
Assert.Equal([first, second], scheduler.Ends);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -434,10 +435,200 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
sequence: 2);
|
sequence: 2);
|
||||||
|
|
||||||
Assert.Equal([first, second], resources.Begins);
|
Assert.Equal([first, second], resources.Begins);
|
||||||
|
Assert.Equal([first], resources.Ends);
|
||||||
coordinator.RevealWorldViewport();
|
coordinator.RevealWorldViewport();
|
||||||
coordinator.Complete();
|
coordinator.Complete();
|
||||||
|
|
||||||
Assert.Equal([second], resources.Ends);
|
Assert.Equal([first, second], resources.Ends);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegistrationCallback_ReentrantReplacementRetiresExactOldSuffix()
|
||||||
|
{
|
||||||
|
var state = new State();
|
||||||
|
var scheduler = new RecordingDestinationScheduler();
|
||||||
|
WorldRevealCoordinator coordinator = null!;
|
||||||
|
long replacement = 0;
|
||||||
|
scheduler.OnBegin = () =>
|
||||||
|
{
|
||||||
|
scheduler.OnBegin = null;
|
||||||
|
replacement = coordinator.BeginLogin(0x20210123u);
|
||||||
|
};
|
||||||
|
coordinator = state.Build(streaming: scheduler);
|
||||||
|
|
||||||
|
long first = coordinator.BeginLogin(0x11340021u);
|
||||||
|
|
||||||
|
Assert.NotEqual(first, replacement);
|
||||||
|
Assert.Equal(replacement, coordinator.Snapshot.Generation);
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
(first, 0x11340021u, 1),
|
||||||
|
(replacement, 0x20210123u, 0),
|
||||||
|
],
|
||||||
|
scheduler.Begins);
|
||||||
|
Assert.Equal([first], scheduler.Ends);
|
||||||
|
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
|
||||||
|
Assert.Equal(
|
||||||
|
0,
|
||||||
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
|
||||||
|
coordinator.Cancel();
|
||||||
|
|
||||||
|
Assert.Equal([first, replacement], scheduler.Ends);
|
||||||
|
Assert.Equal(0, coordinator.Ownership.HostProjectionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RegistrationFailure_RetriesExactSuffixWithoutReplayingBegin()
|
||||||
|
{
|
||||||
|
var state = new State();
|
||||||
|
var scheduler = new RecordingDestinationScheduler();
|
||||||
|
var resources = new RecordingRenderResourceScheduler
|
||||||
|
{
|
||||||
|
ThrowBeginCount = 1,
|
||||||
|
};
|
||||||
|
WorldRevealCoordinator coordinator = state.Build(
|
||||||
|
streaming: scheduler,
|
||||||
|
renderResources: resources);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
() => coordinator.BeginLogin(0x11340021u));
|
||||||
|
Assert.Single(scheduler.Begins);
|
||||||
|
Assert.Equal(1, resources.BeginAttempts);
|
||||||
|
Assert.Empty(resources.Begins);
|
||||||
|
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
|
||||||
|
Assert.Equal(
|
||||||
|
1,
|
||||||
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
|
||||||
|
coordinator.RetryPendingHostWork();
|
||||||
|
|
||||||
|
Assert.Single(scheduler.Begins);
|
||||||
|
Assert.Equal(2, resources.BeginAttempts);
|
||||||
|
Assert.Single(resources.Begins);
|
||||||
|
Assert.Equal(
|
||||||
|
0,
|
||||||
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
|
||||||
|
coordinator.Cancel();
|
||||||
|
Assert.Equal(0, coordinator.Ownership.HostProjectionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReservationReleaseFailure_RetriesOnlyUnfinishedCallback()
|
||||||
|
{
|
||||||
|
var state = new State();
|
||||||
|
var scheduler = new RecordingDestinationScheduler();
|
||||||
|
var resources = new RecordingRenderResourceScheduler
|
||||||
|
{
|
||||||
|
ThrowEndCount = 1,
|
||||||
|
};
|
||||||
|
WorldRevealCoordinator coordinator = state.Build(
|
||||||
|
streaming: scheduler,
|
||||||
|
renderResources: resources);
|
||||||
|
long generation = coordinator.BeginLogin(0x11340021u);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
coordinator.RevealWorldViewport);
|
||||||
|
Assert.Equal([generation], scheduler.Ends);
|
||||||
|
Assert.Equal(1, resources.EndAttempts);
|
||||||
|
Assert.Empty(resources.Ends);
|
||||||
|
Assert.Equal(
|
||||||
|
1,
|
||||||
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
|
||||||
|
coordinator.RetryPendingHostWork();
|
||||||
|
|
||||||
|
Assert.Equal([generation], scheduler.Ends);
|
||||||
|
Assert.Equal(2, resources.EndAttempts);
|
||||||
|
Assert.Equal([generation], resources.Ends);
|
||||||
|
Assert.Equal(
|
||||||
|
0,
|
||||||
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
coordinator.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResetFailure_RetainsWithdrawalAndConvergesOnRetry()
|
||||||
|
{
|
||||||
|
var state = new State();
|
||||||
|
var scheduler = new RecordingDestinationScheduler
|
||||||
|
{
|
||||||
|
ThrowEndCount = 1,
|
||||||
|
};
|
||||||
|
WorldRevealCoordinator coordinator =
|
||||||
|
state.Build(streaming: scheduler);
|
||||||
|
coordinator.BeginLogin(0x11340021u);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(coordinator.ResetSession);
|
||||||
|
Assert.True(coordinator.Snapshot.Cancelled);
|
||||||
|
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
|
||||||
|
Assert.True(
|
||||||
|
coordinator.Ownership.PendingHostAcknowledgementCount > 0);
|
||||||
|
|
||||||
|
coordinator.ResetSession();
|
||||||
|
|
||||||
|
Assert.Equal(RuntimePortalSnapshot.Idle, coordinator.Snapshot);
|
||||||
|
Assert.True(coordinator.Ownership.IsSessionIdle);
|
||||||
|
Assert.Equal(2, scheduler.EndAttempts);
|
||||||
|
Assert.Single(scheduler.Ends);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SimulationReleaseFailure_RetriesAudioWithoutReplayingMaterialization()
|
||||||
|
{
|
||||||
|
var transit = new RuntimeWorldTransitState();
|
||||||
|
var availability = new WorldGenerationAvailabilityState(transit);
|
||||||
|
var world = new GpuWorldState(availability: availability);
|
||||||
|
var audio = new RecordingAudioQuiescence
|
||||||
|
{
|
||||||
|
ThrowResumeCount = 1,
|
||||||
|
};
|
||||||
|
var quiescence = new WorldGenerationQuiescence(
|
||||||
|
new SelectionState(),
|
||||||
|
world,
|
||||||
|
_ => null,
|
||||||
|
audio);
|
||||||
|
var coordinator = new WorldRevealCoordinator(
|
||||||
|
transit,
|
||||||
|
isRenderNeighborhoodReady: (_, _) => true,
|
||||||
|
isSpawnCellReady: _ => true,
|
||||||
|
isTerrainNeighborhoodReady: (_, _) => true,
|
||||||
|
areCompositeTexturesReady: () => true,
|
||||||
|
prepareCompositeTextures: (_, _) => { },
|
||||||
|
invalidateCompositeTextures: () => { },
|
||||||
|
isSpawnClaimUnhydratable: _ => false,
|
||||||
|
quiescence: quiescence);
|
||||||
|
const uint cell = 0x3032001Cu;
|
||||||
|
long generation = BeginPortal(
|
||||||
|
coordinator,
|
||||||
|
transit,
|
||||||
|
cell,
|
||||||
|
sequence: 1);
|
||||||
|
Assert.True(coordinator.Evaluate(cell).IsReady);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
coordinator.ObserveMaterialized(
|
||||||
|
generation,
|
||||||
|
1,
|
||||||
|
cell));
|
||||||
|
Assert.True(coordinator.Snapshot.Materialized);
|
||||||
|
Assert.Equal(1, coordinator.PortalMaterializationCount);
|
||||||
|
Assert.Equal(1, audio.ResumeAttempts);
|
||||||
|
Assert.Equal(0, audio.ResumeCalls);
|
||||||
|
Assert.Equal(
|
||||||
|
1,
|
||||||
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
|
||||||
|
coordinator.RetryPendingHostWork();
|
||||||
|
|
||||||
|
Assert.Equal(2, audio.ResumeAttempts);
|
||||||
|
Assert.Equal(1, audio.ResumeCalls);
|
||||||
|
Assert.Equal(1, coordinator.PortalMaterializationCount);
|
||||||
|
Assert.Equal(
|
||||||
|
0,
|
||||||
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
coordinator.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static long BeginPortal(
|
private static long BeginPortal(
|
||||||
|
|
@ -471,9 +662,32 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
{
|
{
|
||||||
public int SuspendCalls { get; private set; }
|
public int SuspendCalls { get; private set; }
|
||||||
public int ResumeCalls { get; private set; }
|
public int ResumeCalls { get; private set; }
|
||||||
|
public int SuspendAttempts { get; private set; }
|
||||||
|
public int ResumeAttempts { get; private set; }
|
||||||
|
public int ThrowSuspendCount { get; set; }
|
||||||
|
public int ThrowResumeCount { get; set; }
|
||||||
|
|
||||||
public void SuspendWorldAudio() => SuspendCalls++;
|
public void SuspendWorldAudio()
|
||||||
public void ResumeWorldAudio() => ResumeCalls++;
|
{
|
||||||
|
SuspendAttempts++;
|
||||||
|
if (ThrowSuspendCount > 0)
|
||||||
|
{
|
||||||
|
ThrowSuspendCount--;
|
||||||
|
throw new InvalidOperationException("suspend failed");
|
||||||
|
}
|
||||||
|
SuspendCalls++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResumeWorldAudio()
|
||||||
|
{
|
||||||
|
ResumeAttempts++;
|
||||||
|
if (ThrowResumeCount > 0)
|
||||||
|
{
|
||||||
|
ThrowResumeCount--;
|
||||||
|
throw new InvalidOperationException("resume failed");
|
||||||
|
}
|
||||||
|
ResumeCalls++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RecordingDestinationScheduler
|
private sealed class RecordingDestinationScheduler
|
||||||
|
|
@ -481,16 +695,40 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
{
|
{
|
||||||
public List<(long Generation, uint Cell, int Radius)> Begins { get; } = [];
|
public List<(long Generation, uint Cell, int Radius)> Begins { get; } = [];
|
||||||
public List<long> Ends { get; } = [];
|
public List<long> Ends { get; } = [];
|
||||||
|
public int BeginAttempts { get; private set; }
|
||||||
|
public int EndAttempts { get; private set; }
|
||||||
|
public int ThrowBeginCount { get; set; }
|
||||||
|
public int ThrowEndCount { get; set; }
|
||||||
|
public Action? OnBegin { get; set; }
|
||||||
|
public Action? OnEnd { get; set; }
|
||||||
|
|
||||||
public void BeginDestinationReservation(
|
public void BeginDestinationReservation(
|
||||||
long revealGeneration,
|
long revealGeneration,
|
||||||
uint destinationCell,
|
uint destinationCell,
|
||||||
int requiredRenderRadius) =>
|
int requiredRenderRadius)
|
||||||
|
{
|
||||||
|
BeginAttempts++;
|
||||||
|
if (ThrowBeginCount > 0)
|
||||||
|
{
|
||||||
|
ThrowBeginCount--;
|
||||||
|
throw new InvalidOperationException("stream begin failed");
|
||||||
|
}
|
||||||
Begins.Add(
|
Begins.Add(
|
||||||
(revealGeneration, destinationCell, requiredRenderRadius));
|
(revealGeneration, destinationCell, requiredRenderRadius));
|
||||||
|
OnBegin?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
public void EndDestinationReservation(long revealGeneration) =>
|
public void EndDestinationReservation(long revealGeneration)
|
||||||
|
{
|
||||||
|
EndAttempts++;
|
||||||
|
if (ThrowEndCount > 0)
|
||||||
|
{
|
||||||
|
ThrowEndCount--;
|
||||||
|
throw new InvalidOperationException("stream end failed");
|
||||||
|
}
|
||||||
Ends.Add(revealGeneration);
|
Ends.Add(revealGeneration);
|
||||||
|
OnEnd?.Invoke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class RecordingRenderResourceScheduler
|
private sealed class RecordingRenderResourceScheduler
|
||||||
|
|
@ -498,11 +736,31 @@ public sealed class WorldRevealCoordinatorTests
|
||||||
{
|
{
|
||||||
public List<long> Begins { get; } = [];
|
public List<long> Begins { get; } = [];
|
||||||
public List<long> Ends { get; } = [];
|
public List<long> Ends { get; } = [];
|
||||||
|
public int BeginAttempts { get; private set; }
|
||||||
|
public int EndAttempts { get; private set; }
|
||||||
|
public int ThrowBeginCount { get; set; }
|
||||||
|
public int ThrowEndCount { get; set; }
|
||||||
|
|
||||||
public void BeginDestinationReveal(long revealGeneration) =>
|
public void BeginDestinationReveal(long revealGeneration)
|
||||||
|
{
|
||||||
|
BeginAttempts++;
|
||||||
|
if (ThrowBeginCount > 0)
|
||||||
|
{
|
||||||
|
ThrowBeginCount--;
|
||||||
|
throw new InvalidOperationException("render begin failed");
|
||||||
|
}
|
||||||
Begins.Add(revealGeneration);
|
Begins.Add(revealGeneration);
|
||||||
|
}
|
||||||
|
|
||||||
public void EndDestinationReveal(long revealGeneration) =>
|
public void EndDestinationReveal(long revealGeneration)
|
||||||
|
{
|
||||||
|
EndAttempts++;
|
||||||
|
if (ThrowEndCount > 0)
|
||||||
|
{
|
||||||
|
ThrowEndCount--;
|
||||||
|
throw new InvalidOperationException("render end failed");
|
||||||
|
}
|
||||||
Ends.Add(revealGeneration);
|
Ends.Add(revealGeneration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using AcDream.Runtime.Gameplay;
|
using AcDream.Runtime.Gameplay;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
|
|
||||||
namespace AcDream.Runtime.Tests;
|
namespace AcDream.Runtime.Tests;
|
||||||
|
|
||||||
|
|
@ -183,7 +184,20 @@ public sealed class GameRuntimeContractTests
|
||||||
InteractionTransactions: default),
|
InteractionTransactions: default),
|
||||||
default,
|
default,
|
||||||
default,
|
default,
|
||||||
default);
|
new RuntimeWorldEnvironmentOwnershipSnapshot(
|
||||||
|
IsInitialized: true,
|
||||||
|
DayGroupDefinitionCount: 4,
|
||||||
|
ActiveDayGroupCount: 1),
|
||||||
|
default,
|
||||||
|
new RuntimeWorldTransitOwnershipSnapshot(
|
||||||
|
BufferedTeleportDestinationCount: 0,
|
||||||
|
PendingTeleportStartCount: 0,
|
||||||
|
ActiveTeleportCount: 1,
|
||||||
|
AcceptedTeleportDestinationCount: 0,
|
||||||
|
ActiveRevealCount: 1,
|
||||||
|
PendingDestinationReadinessCount: 1,
|
||||||
|
HostProjectionCount: 1,
|
||||||
|
PendingHostAcknowledgementCount: 2));
|
||||||
|
|
||||||
recorder.AddCheckpoint(stamp, checkpoint);
|
recorder.AddCheckpoint(stamp, checkpoint);
|
||||||
|
|
||||||
|
|
@ -191,6 +205,8 @@ public sealed class GameRuntimeContractTests
|
||||||
Assert.Contains("inventory-state=2:4:1:3", entry.Text);
|
Assert.Contains("inventory-state=2:4:1:3", entry.Text);
|
||||||
Assert.Contains("character=5:6:7:8:0:0", entry.Text);
|
Assert.Contains("character=5:6:7:8:0:0", entry.Text);
|
||||||
Assert.Contains("social=9:10:11", entry.Text);
|
Assert.Contains("social=9:10:11", entry.Text);
|
||||||
|
Assert.Contains("environment-owner=True:4:1", entry.Text);
|
||||||
|
Assert.Contains("transit-owner=0:0:1:0:1:1:1:2", entry.Text);
|
||||||
Assert.Contains(
|
Assert.Contains(
|
||||||
"actions=14:50000001:15:4:1:16:1:00000000:0:00000000:" +
|
"actions=14:50000001:15:4:1:16:1:00000000:0:00000000:" +
|
||||||
"00000000:00000000:00000000:0:0:0:0:00000000:00000000:" +
|
"00000000:00000000:00000000:0:0:0:0:00000000:00000000:" +
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.Physics;
|
||||||
|
using AcDream.Runtime.World;
|
||||||
|
|
||||||
|
namespace AcDream.Runtime.Tests.World;
|
||||||
|
|
||||||
|
public sealed class RuntimeWorldTransitDirectHostTests
|
||||||
|
{
|
||||||
|
private static readonly string[] ForbiddenHostAssemblyPrefixes =
|
||||||
|
[
|
||||||
|
"AcDream.App",
|
||||||
|
"AcDream.UI.",
|
||||||
|
"Silk.NET",
|
||||||
|
"OpenAL",
|
||||||
|
"Arch",
|
||||||
|
"ImGui",
|
||||||
|
];
|
||||||
|
|
||||||
|
private const uint OutdoorCell = 0x11340021u;
|
||||||
|
private const uint IndoorCell = 0x8A020164u;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DirectHost_CompletesLoginPortalCancellationAndReset()
|
||||||
|
{
|
||||||
|
string[] loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.Select(static assembly =>
|
||||||
|
assembly.GetName().Name ?? string.Empty)
|
||||||
|
.ToArray();
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
loadedAssemblies,
|
||||||
|
name => ForbiddenHostAssemblyPrefixes.Any(
|
||||||
|
prefix => name.StartsWith(
|
||||||
|
prefix,
|
||||||
|
StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
var state = new RuntimeWorldTransitState();
|
||||||
|
var host = new DirectHost(state);
|
||||||
|
|
||||||
|
host.CompleteLogin(OutdoorCell);
|
||||||
|
Assert.True(state.Snapshot.Completed);
|
||||||
|
Assert.True(state.Snapshot.WorldViewportObserved);
|
||||||
|
Assert.Equal(0, state.Ownership.HostProjectionCount);
|
||||||
|
|
||||||
|
state.ResetSession();
|
||||||
|
host.CompletePortal(IndoorCell, sequence: 1);
|
||||||
|
Assert.True(state.Snapshot.Completed);
|
||||||
|
Assert.True(state.Snapshot.Materialized);
|
||||||
|
Assert.Equal(1, state.Snapshot.PortalMaterializationCount);
|
||||||
|
Assert.Equal(0, state.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
|
||||||
|
state.EndTeleport();
|
||||||
|
state.ResetSession();
|
||||||
|
host.CancelLogin(OutdoorCell);
|
||||||
|
Assert.True(state.Snapshot.Cancelled);
|
||||||
|
Assert.Equal(0, state.Ownership.HostProjectionCount);
|
||||||
|
|
||||||
|
state.ResetSession();
|
||||||
|
Assert.Equal(RuntimePortalSnapshot.Idle, state.Snapshot);
|
||||||
|
Assert.True(state.Ownership.IsSessionIdle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DirectHost(RuntimeWorldTransitState state)
|
||||||
|
{
|
||||||
|
public void CompleteLogin(uint cell)
|
||||||
|
{
|
||||||
|
long generation = state.BeginLoginReveal(cell);
|
||||||
|
RuntimeWorldHostProjectionToken host =
|
||||||
|
Register(generation, cell);
|
||||||
|
Acknowledge(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
|
||||||
|
Assert.True(state.AcknowledgeDestinationReadiness(
|
||||||
|
Ready(generation, cell)));
|
||||||
|
Assert.True(state.Complete(generation));
|
||||||
|
DrainPending(host);
|
||||||
|
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompletePortal(uint cell, ushort sequence)
|
||||||
|
{
|
||||||
|
Assert.True(state.TryQueueTeleportStart(sequence));
|
||||||
|
Assert.True(state.ActivateQueuedTeleport());
|
||||||
|
Assert.True(state.OfferTeleportDestination(
|
||||||
|
Destination(cell, sequence),
|
||||||
|
teleportTimestampAdvanced: true));
|
||||||
|
Assert.True(state.TryBeginPortalReveal(
|
||||||
|
sequence,
|
||||||
|
cell,
|
||||||
|
out long generation));
|
||||||
|
RuntimeWorldHostProjectionToken host =
|
||||||
|
Register(generation, cell);
|
||||||
|
Acknowledge(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
|
||||||
|
Assert.True(state.AcknowledgeDestinationReadiness(
|
||||||
|
Ready(generation, cell)));
|
||||||
|
Assert.True(state.AcknowledgePortalMaterialized(
|
||||||
|
generation,
|
||||||
|
sequence,
|
||||||
|
cell));
|
||||||
|
DrainPending(host);
|
||||||
|
Assert.True(state.RequireDestinationReservationRelease(host));
|
||||||
|
DrainPending(host);
|
||||||
|
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
|
||||||
|
Assert.True(state.Complete(generation));
|
||||||
|
DrainPending(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CancelLogin(uint cell)
|
||||||
|
{
|
||||||
|
long generation = state.BeginLoginReveal(cell);
|
||||||
|
RuntimeWorldHostProjectionToken host =
|
||||||
|
Register(generation, cell);
|
||||||
|
Acknowledge(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered);
|
||||||
|
Assert.True(state.Cancel(generation));
|
||||||
|
DrainPending(host);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeWorldHostProjectionToken Register(
|
||||||
|
long generation,
|
||||||
|
uint cell)
|
||||||
|
{
|
||||||
|
Assert.True(state.TryRegisterHostProjection(
|
||||||
|
generation,
|
||||||
|
cell,
|
||||||
|
out RuntimeWorldHostProjectionToken host));
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrainPending(RuntimeWorldHostProjectionToken host)
|
||||||
|
{
|
||||||
|
while (state.TryGetHostProjection(
|
||||||
|
host,
|
||||||
|
out RuntimeWorldHostProjectionSnapshot projection))
|
||||||
|
{
|
||||||
|
RuntimeWorldHostAcknowledgementStage pending =
|
||||||
|
projection.PendingAcknowledgements;
|
||||||
|
if ((pending & RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected) != 0)
|
||||||
|
{
|
||||||
|
Acknowledge(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((pending & RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased) != 0)
|
||||||
|
{
|
||||||
|
Acknowledge(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((pending & RuntimeWorldHostAcknowledgementStage
|
||||||
|
.TerminalProjected) != 0)
|
||||||
|
{
|
||||||
|
Acknowledge(
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.TerminalProjected);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Acknowledge(
|
||||||
|
RuntimeWorldHostProjectionToken host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage stage) =>
|
||||||
|
Assert.True(state.AcknowledgeHostProjection(
|
||||||
|
new RuntimeWorldHostAcknowledgement(host, stage)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RuntimeDestinationReadiness Ready(
|
||||||
|
long generation,
|
||||||
|
uint cell) =>
|
||||||
|
new(
|
||||||
|
generation,
|
||||||
|
cell,
|
||||||
|
IsIndoor: (cell & 0xFFFFu) >= 0x0100u,
|
||||||
|
IsUnhydratable: false,
|
||||||
|
RequiredRenderRadius: (cell & 0xFFFFu) >= 0x0100u ? 0 : 1,
|
||||||
|
IsRenderNeighborhoodReady: true,
|
||||||
|
AreCompositeTexturesReady: true,
|
||||||
|
IsCollisionReady: true);
|
||||||
|
|
||||||
|
private static RuntimeTeleportDestination Destination(
|
||||||
|
uint cell,
|
||||||
|
ushort sequence) =>
|
||||||
|
new(
|
||||||
|
EntityGuid: 0x50000001u,
|
||||||
|
InstanceSequence: 1,
|
||||||
|
PositionSequence: 1,
|
||||||
|
TeleportSequence: sequence,
|
||||||
|
ForcePositionSequence: 1,
|
||||||
|
Position: new Position(
|
||||||
|
cell,
|
||||||
|
Vector3.Zero,
|
||||||
|
Quaternion.Identity));
|
||||||
|
}
|
||||||
|
|
@ -338,6 +338,18 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
BeginPortal(first, OutdoorCell);
|
BeginPortal(first, OutdoorCell);
|
||||||
long secondGeneration =
|
long secondGeneration =
|
||||||
second.BeginLoginReveal(OtherCell);
|
second.BeginLoginReveal(OtherCell);
|
||||||
|
RuntimeWorldHostProjectionToken firstHost =
|
||||||
|
RegisterHost(first, firstGeneration, OutdoorCell);
|
||||||
|
RuntimeWorldHostProjectionToken secondHost =
|
||||||
|
RegisterHost(second, secondGeneration, OtherCell);
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
first,
|
||||||
|
firstHost,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
second,
|
||||||
|
secondHost,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
|
||||||
first.AcknowledgeDestinationReadiness(
|
first.AcknowledgeDestinationReadiness(
|
||||||
Ready(firstGeneration, OutdoorCell));
|
Ready(firstGeneration, OutdoorCell));
|
||||||
first.AcknowledgePortalMaterialized(
|
first.AcknowledgePortalMaterialized(
|
||||||
|
|
@ -351,6 +363,13 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
Assert.False(second.IsWorldSimulationAvailable);
|
Assert.False(second.IsWorldSimulationAvailable);
|
||||||
Assert.Equal(OtherCell, second.Snapshot.DestinationCell);
|
Assert.Equal(OtherCell, second.Snapshot.DestinationCell);
|
||||||
Assert.Equal(1, secondGeneration);
|
Assert.Equal(1, secondGeneration);
|
||||||
|
Assert.False(Acknowledge(
|
||||||
|
second,
|
||||||
|
firstHost,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected));
|
||||||
|
Assert.Equal(1, first.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
Assert.Equal(0, second.Ownership.PendingHostAcknowledgementCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -616,6 +635,195 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
Assert.True(state.TryQueueTeleportStart(8));
|
Assert.True(state.TryQueueTeleportStart(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HostProjection_TracksExactGenerationScopedAcknowledgementSuffix()
|
||||||
|
{
|
||||||
|
var state = new RuntimeWorldTransitState();
|
||||||
|
long generation = BeginPortal(state, OutdoorCell);
|
||||||
|
RuntimeWorldHostProjectionToken host =
|
||||||
|
RegisterHost(state, generation, OutdoorCell);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new RuntimeWorldTransitOwnershipSnapshot(
|
||||||
|
BufferedTeleportDestinationCount: 0,
|
||||||
|
PendingTeleportStartCount: 0,
|
||||||
|
ActiveTeleportCount: 1,
|
||||||
|
AcceptedTeleportDestinationCount: 0,
|
||||||
|
ActiveRevealCount: 1,
|
||||||
|
PendingDestinationReadinessCount: 1,
|
||||||
|
HostProjectionCount: 1,
|
||||||
|
PendingHostAcknowledgementCount: 1),
|
||||||
|
state.Ownership);
|
||||||
|
Assert.False(Acknowledge(
|
||||||
|
state,
|
||||||
|
host with { Generation = generation + 1 },
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
|
||||||
|
Assert.False(Acknowledge(
|
||||||
|
state,
|
||||||
|
host with { DestinationCell = OtherCell },
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
|
||||||
|
Assert.False(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.None));
|
||||||
|
Assert.False(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered
|
||||||
|
| RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
|
||||||
|
|
||||||
|
Assert.True(state.AcknowledgeDestinationReadiness(
|
||||||
|
Ready(generation, OutdoorCell)));
|
||||||
|
Assert.True(state.AcknowledgePortalMaterialized(
|
||||||
|
generation,
|
||||||
|
1,
|
||||||
|
OutdoorCell));
|
||||||
|
Assert.Equal(1, state.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected));
|
||||||
|
|
||||||
|
Assert.True(state.RequireDestinationReservationRelease(host));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased));
|
||||||
|
Assert.True(state.AcknowledgeWorldViewportVisible(generation));
|
||||||
|
Assert.True(state.Complete(generation));
|
||||||
|
Assert.Equal(1, state.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
|
||||||
|
Assert.Equal(0, state.Ownership.HostProjectionCount);
|
||||||
|
Assert.Equal(0, state.Ownership.PendingHostAcknowledgementCount);
|
||||||
|
|
||||||
|
state.EndTeleport();
|
||||||
|
state.ResetSession();
|
||||||
|
Assert.True(state.Ownership.IsSessionIdle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Cancel_ReplacesIncompleteRegistrationWithRetryableWithdrawal()
|
||||||
|
{
|
||||||
|
var state = new RuntimeWorldTransitState();
|
||||||
|
long generation = state.BeginLoginReveal(OutdoorCell);
|
||||||
|
RuntimeWorldHostProjectionToken host =
|
||||||
|
RegisterHost(state, generation, OutdoorCell);
|
||||||
|
|
||||||
|
Assert.True(state.Cancel(generation));
|
||||||
|
Assert.True(state.TryGetHostProjection(
|
||||||
|
host,
|
||||||
|
out RuntimeWorldHostProjectionSnapshot projection));
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected
|
||||||
|
| RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased
|
||||||
|
| RuntimeWorldHostAcknowledgementStage.TerminalProjected,
|
||||||
|
projection.PendingAcknowledgements);
|
||||||
|
Assert.False(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
|
||||||
|
|
||||||
|
state.ResetSession();
|
||||||
|
Assert.True(state.Ownership.IsSessionIdle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Supersession_PreservesOldHostWithdrawalByExactGeneration()
|
||||||
|
{
|
||||||
|
var state = new RuntimeWorldTransitState();
|
||||||
|
long first = state.BeginLoginReveal(OutdoorCell);
|
||||||
|
RuntimeWorldHostProjectionToken firstHost =
|
||||||
|
RegisterHost(state, first, OutdoorCell);
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
firstHost,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.ProjectionRegistered));
|
||||||
|
|
||||||
|
long second = BeginPortal(state, OtherCell, sequence: 2);
|
||||||
|
|
||||||
|
Assert.True(state.TryGetHostProjection(
|
||||||
|
firstHost,
|
||||||
|
out RuntimeWorldHostProjectionSnapshot superseded));
|
||||||
|
Assert.True(superseded.IsSuperseding);
|
||||||
|
Assert.Equal(
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased
|
||||||
|
| RuntimeWorldHostAcknowledgementStage.TerminalProjected,
|
||||||
|
superseded.PendingAcknowledgements);
|
||||||
|
Assert.False(Acknowledge(
|
||||||
|
state,
|
||||||
|
firstHost,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
firstHost,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
firstHost,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
|
||||||
|
Assert.Equal(second, state.Snapshot.Generation);
|
||||||
|
Assert.Equal(0, state.Ownership.HostProjectionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResetSession_RejectsUnacknowledgedHostProjection()
|
||||||
|
{
|
||||||
|
var state = new RuntimeWorldTransitState();
|
||||||
|
long generation = state.BeginLoginReveal(OutdoorCell);
|
||||||
|
RuntimeWorldHostProjectionToken host =
|
||||||
|
RegisterHost(state, generation, OutdoorCell);
|
||||||
|
|
||||||
|
InvalidOperationException error =
|
||||||
|
Assert.Throws<InvalidOperationException>(state.ResetSession);
|
||||||
|
Assert.Contains("hosts=1", error.Message);
|
||||||
|
Assert.Equal(generation, state.Snapshot.Generation);
|
||||||
|
|
||||||
|
Assert.True(state.Cancel(generation));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.SimulationReleaseProjected));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage
|
||||||
|
.DestinationReservationReleased));
|
||||||
|
Assert.True(Acknowledge(
|
||||||
|
state,
|
||||||
|
host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage.TerminalProjected));
|
||||||
|
state.ResetSession();
|
||||||
|
}
|
||||||
|
|
||||||
private static long BeginPortal(
|
private static long BeginPortal(
|
||||||
RuntimeWorldTransitState state,
|
RuntimeWorldTransitState state,
|
||||||
uint cell,
|
uint cell,
|
||||||
|
|
@ -633,6 +841,25 @@ public sealed class RuntimeWorldTransitStateTests
|
||||||
return generation;
|
return generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RuntimeWorldHostProjectionToken RegisterHost(
|
||||||
|
RuntimeWorldTransitState state,
|
||||||
|
long generation,
|
||||||
|
uint cell)
|
||||||
|
{
|
||||||
|
Assert.True(state.TryRegisterHostProjection(
|
||||||
|
generation,
|
||||||
|
cell,
|
||||||
|
out RuntimeWorldHostProjectionToken host));
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Acknowledge(
|
||||||
|
RuntimeWorldTransitState state,
|
||||||
|
RuntimeWorldHostProjectionToken host,
|
||||||
|
RuntimeWorldHostAcknowledgementStage stage) =>
|
||||||
|
state.AcknowledgeHostProjection(
|
||||||
|
new RuntimeWorldHostAcknowledgement(host, stage));
|
||||||
|
|
||||||
private static RuntimeTeleportDestination Destination(
|
private static RuntimeTeleportDestination Destination(
|
||||||
uint cell,
|
uint cell,
|
||||||
ushort sequence,
|
ushort sequence,
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,8 @@ function Read-Checkpoints([string]$Path) {
|
||||||
function Validate-Checkpoint([string]$SessionLabel, [object]$Checkpoint) {
|
function Validate-Checkpoint([string]$SessionLabel, [object]$Checkpoint) {
|
||||||
$name = $Checkpoint.name
|
$name = $Checkpoint.name
|
||||||
$reveal = $Checkpoint.reveal
|
$reveal = $Checkpoint.reveal
|
||||||
|
$environmentOwnership = $Checkpoint.environmentOwnership
|
||||||
|
$transitOwnership = $Checkpoint.transitOwnership
|
||||||
$resources = $Checkpoint.resources
|
$resources = $Checkpoint.resources
|
||||||
if (-not $reveal.readiness.isReady) {
|
if (-not $reveal.readiness.isReady) {
|
||||||
$failures.Add("${SessionLabel}/${name}: reveal was not ready")
|
$failures.Add("${SessionLabel}/${name}: reveal was not ready")
|
||||||
|
|
@ -161,6 +163,31 @@ function Validate-Checkpoint([string]$SessionLabel, [object]$Checkpoint) {
|
||||||
$failures.Add("${SessionLabel}/${name}: collision was not ready")
|
$failures.Add("${SessionLabel}/${name}: collision was not ready")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (-not $environmentOwnership.isInitialized) {
|
||||||
|
$failures.Add("${SessionLabel}/${name}: Runtime world environment is not initialized")
|
||||||
|
}
|
||||||
|
if (($environmentOwnership.dayGroupDefinitionCount -le 0) -or
|
||||||
|
($environmentOwnership.activeDayGroupCount -ne 1)) {
|
||||||
|
$failures.Add((
|
||||||
|
"${SessionLabel}/${name}: Runtime environment ownership is {0}/{1}, expected definitions with one active group" -f
|
||||||
|
$environmentOwnership.dayGroupDefinitionCount,
|
||||||
|
$environmentOwnership.activeDayGroupCount))
|
||||||
|
}
|
||||||
|
foreach ($field in @(
|
||||||
|
'bufferedTeleportDestinationCount',
|
||||||
|
'pendingTeleportStartCount',
|
||||||
|
'activeTeleportCount',
|
||||||
|
'acceptedTeleportDestinationCount',
|
||||||
|
'activeRevealCount',
|
||||||
|
'pendingDestinationReadinessCount',
|
||||||
|
'hostProjectionCount',
|
||||||
|
'pendingHostAcknowledgementCount')) {
|
||||||
|
if ([int]$transitOwnership.$field -ne 0) {
|
||||||
|
$failures.Add((
|
||||||
|
"${SessionLabel}/${name}: transitOwnership.$field={0}, expected zero at a stable checkpoint" -f
|
||||||
|
$transitOwnership.$field))
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($resources.pendingLiveTeardowns -ne 0) {
|
if ($resources.pendingLiveTeardowns -ne 0) {
|
||||||
$failures.Add("${SessionLabel}/${name}: $($resources.pendingLiveTeardowns) live teardown(s) pending")
|
$failures.Add("${SessionLabel}/${name}: $($resources.pendingLiveTeardowns) live teardown(s) pending")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue