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
|
|
@ -6,6 +6,7 @@ using AcDream.App.Rendering.Scene;
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI.Testing;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.World;
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace AcDream.App.Tests.Diagnostics;
|
||||
|
|
@ -179,6 +180,19 @@ public sealed class WorldLifecycleAutomationControllerTests
|
|||
Path.Combine(directory, "screenshots"));
|
||||
var controller = new WorldLifecycleAutomationController(
|
||||
() => 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,
|
||||
_ => resources,
|
||||
screenshots,
|
||||
|
|
@ -205,6 +219,15 @@ public sealed class WorldLifecycleAutomationControllerTests
|
|||
using JsonDocument json = JsonDocument.Parse(line);
|
||||
Assert.Equal("dungeon", json.RootElement.GetProperty("name").GetString());
|
||||
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")
|
||||
.GetProperty("visibleLandblocks").GetInt32());
|
||||
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
|
||||
|
|
@ -373,6 +396,8 @@ public sealed class WorldLifecycleAutomationControllerTests
|
|||
string directory,
|
||||
Func<RenderFrameOutcome, WorldLifecycleResourceSnapshot> capture) =>
|
||||
new(
|
||||
() => default,
|
||||
() => default,
|
||||
() => default,
|
||||
() => 0,
|
||||
capture,
|
||||
|
|
|
|||
|
|
@ -81,7 +81,34 @@ public sealed class RuntimeWorldTransitOwnershipTests
|
|||
coordinatorFields,
|
||||
field => field.Name is "_activeGeneration"
|
||||
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)
|
||||
.GetFields(
|
||||
|
|
|
|||
|
|
@ -144,6 +144,39 @@ public sealed class WorldGenerationQuiescenceTests
|
|||
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(
|
||||
long generation,
|
||||
uint destinationCell) =>
|
||||
|
|
@ -178,4 +211,19 @@ public sealed class WorldGenerationQuiescenceTests
|
|||
public void SuspendWorldAudio() => SuspendCalls++;
|
||||
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),
|
||||
],
|
||||
scheduler.Begins);
|
||||
coordinator.Complete();
|
||||
Assert.Equal([second], scheduler.Ends);
|
||||
Assert.Equal([first], scheduler.Ends);
|
||||
coordinator.Cancel();
|
||||
Assert.Equal([first, second], scheduler.Ends);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -434,10 +435,200 @@ public sealed class WorldRevealCoordinatorTests
|
|||
sequence: 2);
|
||||
|
||||
Assert.Equal([first, second], resources.Begins);
|
||||
Assert.Equal([first], resources.Ends);
|
||||
coordinator.RevealWorldViewport();
|
||||
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(
|
||||
|
|
@ -471,9 +662,32 @@ public sealed class WorldRevealCoordinatorTests
|
|||
{
|
||||
public int SuspendCalls { 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 ResumeWorldAudio() => ResumeCalls++;
|
||||
public void SuspendWorldAudio()
|
||||
{
|
||||
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
|
||||
|
|
@ -481,16 +695,40 @@ public sealed class WorldRevealCoordinatorTests
|
|||
{
|
||||
public List<(long Generation, uint Cell, int Radius)> Begins { 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(
|
||||
long revealGeneration,
|
||||
uint destinationCell,
|
||||
int requiredRenderRadius) =>
|
||||
int requiredRenderRadius)
|
||||
{
|
||||
BeginAttempts++;
|
||||
if (ThrowBeginCount > 0)
|
||||
{
|
||||
ThrowBeginCount--;
|
||||
throw new InvalidOperationException("stream begin failed");
|
||||
}
|
||||
Begins.Add(
|
||||
(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);
|
||||
OnEnd?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingRenderResourceScheduler
|
||||
|
|
@ -498,11 +736,31 @@ public sealed class WorldRevealCoordinatorTests
|
|||
{
|
||||
public List<long> Begins { 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);
|
||||
}
|
||||
|
||||
public void EndDestinationReveal(long revealGeneration) =>
|
||||
public void EndDestinationReveal(long revealGeneration)
|
||||
{
|
||||
EndAttempts++;
|
||||
if (ThrowEndCount > 0)
|
||||
{
|
||||
ThrowEndCount--;
|
||||
throw new InvalidOperationException("render end failed");
|
||||
}
|
||||
Ends.Add(revealGeneration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Reflection;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.World;
|
||||
|
||||
namespace AcDream.Runtime.Tests;
|
||||
|
||||
|
|
@ -183,7 +184,20 @@ public sealed class GameRuntimeContractTests
|
|||
InteractionTransactions: 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);
|
||||
|
||||
|
|
@ -191,6 +205,8 @@ public sealed class GameRuntimeContractTests
|
|||
Assert.Contains("inventory-state=2:4:1:3", entry.Text);
|
||||
Assert.Contains("character=5:6:7:8:0:0", 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(
|
||||
"actions=14:50000001:15:4:1:16:1:00000000:0: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);
|
||||
long secondGeneration =
|
||||
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(
|
||||
Ready(firstGeneration, OutdoorCell));
|
||||
first.AcknowledgePortalMaterialized(
|
||||
|
|
@ -351,6 +363,13 @@ public sealed class RuntimeWorldTransitStateTests
|
|||
Assert.False(second.IsWorldSimulationAvailable);
|
||||
Assert.Equal(OtherCell, second.Snapshot.DestinationCell);
|
||||
Assert.Equal(1, secondGeneration);
|
||||
Assert.False(Acknowledge(
|
||||
second,
|
||||
firstHost,
|
||||
RuntimeWorldHostAcknowledgementStage
|
||||
.SimulationReleaseProjected));
|
||||
Assert.Equal(1, first.Ownership.PendingHostAcknowledgementCount);
|
||||
Assert.Equal(0, second.Ownership.PendingHostAcknowledgementCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -616,6 +635,195 @@ public sealed class RuntimeWorldTransitStateTests
|
|||
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(
|
||||
RuntimeWorldTransitState state,
|
||||
uint cell,
|
||||
|
|
@ -633,6 +841,25 @@ public sealed class RuntimeWorldTransitStateTests
|
|||
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(
|
||||
uint cell,
|
||||
ushort sequence,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue