Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
282 lines
10 KiB
C#
282 lines
10 KiB
C#
using AcDream.App.Streaming;
|
|
using AcDream.Core.Selection;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class WorldRevealCoordinatorTests
|
|
{
|
|
private sealed class State
|
|
{
|
|
public bool RenderReady { get; set; }
|
|
public bool CollisionReady { get; set; }
|
|
public bool CompositesReady { get; set; }
|
|
public int InvalidateCount { get; private set; }
|
|
public int PrepareCount { get; private set; }
|
|
|
|
public WorldRevealCoordinator Build(
|
|
List<string>? logs = null,
|
|
IWorldRevealStreamingScheduler? streaming = null,
|
|
IWorldRevealRenderResourceScheduler? renderResources = null) => new(
|
|
isRenderNeighborhoodReady: (_, _) => RenderReady,
|
|
isSpawnCellReady: _ => CollisionReady,
|
|
isTerrainNeighborhoodReady: (_, _) => CollisionReady,
|
|
areCompositeTexturesReady: () => CompositesReady,
|
|
prepareCompositeTextures: (_, _) => PrepareCount++,
|
|
invalidateCompositeTextures: () =>
|
|
{
|
|
InvalidateCount++;
|
|
CompositesReady = false;
|
|
},
|
|
isSpawnClaimUnhydratable: _ => false,
|
|
log: logs is null ? null : new Action<string>(logs.Add),
|
|
streaming: streaming,
|
|
renderResources: renderResources);
|
|
}
|
|
|
|
[Fact]
|
|
public void Begin_InvalidatesPriorDestinationAndStartsOneGeneration()
|
|
{
|
|
var state = new State { CompositesReady = true };
|
|
WorldRevealCoordinator coordinator = state.Build();
|
|
|
|
long generation = coordinator.Begin(WorldRevealKind.Login, 0x11340021u);
|
|
|
|
Assert.Equal(1, generation);
|
|
Assert.Equal(1, state.InvalidateCount);
|
|
Assert.False(state.CompositesReady);
|
|
Assert.Equal(WorldRevealKind.Login, coordinator.Snapshot.Kind);
|
|
}
|
|
|
|
[Fact]
|
|
public void PrepareAndEvaluate_UsesOneCanonicalSnapshotForLifecycle()
|
|
{
|
|
const uint outdoorCell = 0x11340021u;
|
|
var state = new State
|
|
{
|
|
RenderReady = true,
|
|
CollisionReady = true,
|
|
};
|
|
WorldRevealCoordinator coordinator = state.Build();
|
|
coordinator.Begin(WorldRevealKind.Login, outdoorCell);
|
|
|
|
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
|
|
state.CompositesReady = true;
|
|
WorldRevealReadinessSnapshot ready = coordinator.PrepareAndEvaluate(outdoorCell);
|
|
|
|
Assert.False(first.IsReady);
|
|
Assert.True(ready.IsReady);
|
|
Assert.Equal(2, state.PrepareCount);
|
|
Assert.Equal(ready, coordinator.Snapshot.Readiness);
|
|
}
|
|
|
|
[Fact]
|
|
public void PortalLifecycle_CountsMaterializationAndCompletesVisibleGeneration()
|
|
{
|
|
const uint outdoorCell = 0x3032001Cu;
|
|
var logs = new List<string>();
|
|
var state = new State
|
|
{
|
|
RenderReady = true,
|
|
CollisionReady = true,
|
|
CompositesReady = true,
|
|
};
|
|
WorldRevealCoordinator coordinator = state.Build(logs);
|
|
coordinator.Begin(WorldRevealKind.Portal, outdoorCell);
|
|
state.CompositesReady = true;
|
|
|
|
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
|
|
coordinator.ObserveMaterialized();
|
|
coordinator.ObserveMaterialized();
|
|
coordinator.ObserveWorldViewportVisible();
|
|
coordinator.Complete();
|
|
|
|
Assert.Equal(1, coordinator.PortalMaterializationCount);
|
|
Assert.True(coordinator.Snapshot.Materialized);
|
|
Assert.True(coordinator.Snapshot.WorldViewportObserved);
|
|
Assert.True(coordinator.Snapshot.Completed);
|
|
Assert.Equal(0, coordinator.Snapshot.InvariantFailureCount);
|
|
Assert.Single(logs, line => line.Contains("event=materialized", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void Cancel_PreventsLateDestinationWorkFromMutatingLifecycle()
|
|
{
|
|
var state = new State
|
|
{
|
|
RenderReady = true,
|
|
CollisionReady = true,
|
|
CompositesReady = true,
|
|
};
|
|
WorldRevealCoordinator coordinator = state.Build();
|
|
coordinator.Begin(WorldRevealKind.Portal, 0x11340021u);
|
|
coordinator.Cancel();
|
|
|
|
coordinator.PrepareAndEvaluate(0x11340021u);
|
|
coordinator.ObserveMaterialized();
|
|
coordinator.ObserveWorldViewportVisible();
|
|
coordinator.Complete();
|
|
|
|
Assert.True(coordinator.Snapshot.Cancelled);
|
|
Assert.Equal(default, coordinator.Snapshot.Readiness);
|
|
Assert.Equal(0, coordinator.PortalMaterializationCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RevealLifetime_QuiescesContinuouslyAndOnlyActiveGenerationReopens()
|
|
{
|
|
var availability = new WorldGenerationAvailabilityState();
|
|
var world = new GpuWorldState(availability: availability);
|
|
var audio = new RecordingAudioQuiescence();
|
|
var quiescence = new WorldGenerationQuiescence(
|
|
availability,
|
|
new SelectionState(),
|
|
world,
|
|
audio);
|
|
var coordinator = new WorldRevealCoordinator(
|
|
isRenderNeighborhoodReady: (_, _) => true,
|
|
isSpawnCellReady: _ => true,
|
|
isTerrainNeighborhoodReady: (_, _) => true,
|
|
areCompositeTexturesReady: () => true,
|
|
prepareCompositeTextures: (_, _) => { },
|
|
invalidateCompositeTextures: () => { },
|
|
isSpawnClaimUnhydratable: _ => false,
|
|
quiescence: quiescence);
|
|
|
|
Assert.Equal(1, coordinator.Begin(WorldRevealKind.Login, 0x11340021u));
|
|
Assert.Equal(2, coordinator.Begin(WorldRevealKind.Portal, 0x11340021u));
|
|
Assert.False(availability.IsWorldAvailable);
|
|
Assert.Equal(2, availability.QuiescedGeneration);
|
|
Assert.Equal(1, audio.SuspendCalls);
|
|
|
|
coordinator.Complete();
|
|
|
|
Assert.True(availability.IsWorldAvailable);
|
|
Assert.Equal(1, audio.ResumeCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public void PortalViewportReveal_ReopensWorldWithoutCompletingProtocolTail()
|
|
{
|
|
var availability = new WorldGenerationAvailabilityState();
|
|
var world = new GpuWorldState(availability: availability);
|
|
var audio = new RecordingAudioQuiescence();
|
|
var quiescence = new WorldGenerationQuiescence(
|
|
availability,
|
|
new SelectionState(),
|
|
world,
|
|
audio);
|
|
var scheduler = new RecordingDestinationScheduler();
|
|
var coordinator = new WorldRevealCoordinator(
|
|
isRenderNeighborhoodReady: (_, _) => true,
|
|
isSpawnCellReady: _ => true,
|
|
isTerrainNeighborhoodReady: (_, _) => true,
|
|
areCompositeTexturesReady: () => true,
|
|
prepareCompositeTextures: (_, _) => { },
|
|
invalidateCompositeTextures: () => { },
|
|
isSpawnClaimUnhydratable: _ => false,
|
|
quiescence: quiescence,
|
|
streaming: scheduler);
|
|
long generation = coordinator.Begin(
|
|
WorldRevealKind.Portal,
|
|
0x3032001Cu);
|
|
|
|
coordinator.RevealWorldViewport();
|
|
coordinator.RevealWorldViewport();
|
|
|
|
Assert.True(availability.IsWorldAvailable);
|
|
Assert.False(coordinator.Snapshot.Completed);
|
|
Assert.Equal(1, audio.ResumeCalls);
|
|
Assert.Equal([generation], scheduler.Ends);
|
|
|
|
coordinator.Complete();
|
|
|
|
Assert.True(coordinator.Snapshot.Completed);
|
|
Assert.Equal(1, audio.ResumeCalls);
|
|
Assert.Equal([generation], scheduler.Ends);
|
|
}
|
|
|
|
[Fact]
|
|
public void RevealGeneration_OwnsExactDestinationReservationUntilCompletion()
|
|
{
|
|
var state = new State();
|
|
var scheduler = new RecordingDestinationScheduler();
|
|
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
|
|
|
|
long first = coordinator.Begin(
|
|
WorldRevealKind.Portal,
|
|
0x11340021u);
|
|
long second = coordinator.Begin(
|
|
WorldRevealKind.Portal,
|
|
0x20210123u);
|
|
|
|
Assert.Equal(
|
|
[
|
|
(first, 0x11340021u, 1),
|
|
(second, 0x20210123u, 0),
|
|
],
|
|
scheduler.Begins);
|
|
coordinator.Complete();
|
|
Assert.Equal([second], scheduler.Ends);
|
|
}
|
|
|
|
[Fact]
|
|
public void RevealGeneration_OwnsRenderUploadProfileUntilViewportRelease()
|
|
{
|
|
var state = new State();
|
|
var resources = new RecordingRenderResourceScheduler();
|
|
WorldRevealCoordinator coordinator =
|
|
state.Build(renderResources: resources);
|
|
|
|
long first = coordinator.Begin(
|
|
WorldRevealKind.Portal,
|
|
0x11340021u);
|
|
long second = coordinator.Begin(
|
|
WorldRevealKind.Portal,
|
|
0x20210123u);
|
|
|
|
Assert.Equal([first, second], resources.Begins);
|
|
coordinator.RevealWorldViewport();
|
|
coordinator.Complete();
|
|
|
|
Assert.Equal([second], resources.Ends);
|
|
}
|
|
|
|
private sealed class RecordingAudioQuiescence : AcDream.App.Audio.IWorldAudioQuiescence
|
|
{
|
|
public int SuspendCalls { get; private set; }
|
|
public int ResumeCalls { get; private set; }
|
|
|
|
public void SuspendWorldAudio() => SuspendCalls++;
|
|
public void ResumeWorldAudio() => ResumeCalls++;
|
|
}
|
|
|
|
private sealed class RecordingDestinationScheduler
|
|
: IWorldRevealStreamingScheduler
|
|
{
|
|
public List<(long Generation, uint Cell, int Radius)> Begins { get; } = [];
|
|
public List<long> Ends { get; } = [];
|
|
|
|
public void BeginDestinationReservation(
|
|
long revealGeneration,
|
|
uint destinationCell,
|
|
int requiredRenderRadius) =>
|
|
Begins.Add(
|
|
(revealGeneration, destinationCell, requiredRenderRadius));
|
|
|
|
public void EndDestinationReservation(long revealGeneration) =>
|
|
Ends.Add(revealGeneration);
|
|
}
|
|
|
|
private sealed class RecordingRenderResourceScheduler
|
|
: IWorldRevealRenderResourceScheduler
|
|
{
|
|
public List<long> Begins { get; } = [];
|
|
public List<long> Ends { get; } = [];
|
|
|
|
public void BeginDestinationReveal(long revealGeneration) =>
|
|
Begins.Add(revealGeneration);
|
|
|
|
public void EndDestinationReveal(long revealGeneration) =>
|
|
Ends.Add(revealGeneration);
|
|
}
|
|
}
|