using AcDream.App.Streaming; using AcDream.App.World; using AcDream.Core.Selection; using AcDream.Runtime; using AcDream.Runtime.World; 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? logs = null, IWorldRevealStreamingScheduler? streaming = null, IWorldRevealRenderResourceScheduler? renderResources = null, RuntimeWorldTransitState? transit = null) { transit ??= new RuntimeWorldTransitState( logs is null ? null : new Action(logs.Add)); return new WorldRevealCoordinator( transit, isRenderNeighborhoodReady: (_, _) => RenderReady, isSpawnCellReady: _ => CollisionReady, isTerrainNeighborhoodReady: (_, _) => CollisionReady, areCompositeTexturesReady: () => CompositesReady, prepareCompositeTextures: (_, _) => PrepareCount++, invalidateCompositeTextures: () => { InvalidateCount++; CompositesReady = false; }, isSpawnClaimUnhydratable: _ => false, streaming: streaming, renderResources: renderResources); } } [Fact] public void Begin_InvalidatesPriorDestinationAndStartsOneGeneration() { var state = new State { CompositesReady = true }; WorldRevealCoordinator coordinator = state.Build(); long generation = coordinator.Begin(RuntimePortalKind.Login, 0x11340021u); Assert.Equal(1, generation); Assert.Equal(1, state.InvalidateCount); Assert.False(state.CompositesReady); Assert.Equal(RuntimePortalKind.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(RuntimePortalKind.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.DestinationCell, coordinator.Snapshot.DestinationCell); Assert.Equal(ready.IsReady, coordinator.Snapshot.IsReady); } [Fact] public void PortalLifecycle_CountsMaterializationAndCompletesVisibleGeneration() { const uint outdoorCell = 0x3032001Cu; var logs = new List(); var state = new State { RenderReady = true, CollisionReady = true, CompositesReady = true, }; WorldRevealCoordinator coordinator = state.Build(logs); coordinator.Begin(RuntimePortalKind.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(RuntimePortalKind.Portal, 0x11340021u); coordinator.Cancel(); coordinator.PrepareAndEvaluate(0x11340021u); coordinator.ObserveMaterialized(); coordinator.ObserveWorldViewportVisible(); coordinator.Complete(); Assert.True(coordinator.Snapshot.Cancelled); Assert.Equal(0x11340021u, coordinator.Snapshot.DestinationCell); Assert.False(coordinator.Snapshot.IsReady); Assert.Equal(0, coordinator.PortalMaterializationCount); } [Fact] public void RevealLifetime_QuiescesContinuouslyAndOnlyActiveGenerationReopens() { var transit = new RuntimeWorldTransitState(); var availability = new WorldGenerationAvailabilityState(transit); var world = new GpuWorldState(availability: availability); var audio = new RecordingAudioQuiescence(); 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); Assert.Equal(1, coordinator.Begin(RuntimePortalKind.Login, 0x11340021u)); Assert.Equal(2, coordinator.Begin(RuntimePortalKind.Portal, 0x11340021u)); Assert.False(availability.IsWorldAvailable); Assert.Equal(2, availability.QuiescedGeneration); Assert.Equal(1, audio.SuspendCalls); Assert.True(coordinator.Evaluate(0x11340021u).IsReady); coordinator.ObserveMaterialized(); coordinator.Complete(); Assert.True(availability.IsWorldAvailable); Assert.Equal(1, audio.ResumeCalls); } [Fact] public void Materialization_ReopensWorldBeforePortalViewportRelease() { var transit = new RuntimeWorldTransitState(); var availability = new WorldGenerationAvailabilityState(transit); var world = new GpuWorldState(availability: availability); var audio = new RecordingAudioQuiescence(); var quiescence = new WorldGenerationQuiescence( new SelectionState(), world, _ => null, audio); var scheduler = new RecordingDestinationScheduler(); var coordinator = new WorldRevealCoordinator( transit, isRenderNeighborhoodReady: (_, _) => true, isSpawnCellReady: _ => true, isTerrainNeighborhoodReady: (_, _) => true, areCompositeTexturesReady: () => true, prepareCompositeTextures: (_, _) => { }, invalidateCompositeTextures: () => { }, isSpawnClaimUnhydratable: _ => false, quiescence: quiescence, streaming: scheduler); long generation = coordinator.Begin( RuntimePortalKind.Portal, 0x3032001Cu); Assert.False(availability.IsWorldAvailable); Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady); coordinator.ObserveMaterialized(); Assert.True(availability.IsWorldAvailable); Assert.False(coordinator.Snapshot.WorldViewportObserved); Assert.False(coordinator.Snapshot.Completed); Assert.Equal(1, audio.ResumeCalls); Assert.Empty(scheduler.Ends); 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 TunnelContinue_TicksDestinationObjectsBehindPortalViewport() { var transit = new RuntimeWorldTransitState(); var availability = new WorldGenerationAvailabilityState(transit); var world = new GpuWorldState(availability: availability); var quiescence = new WorldGenerationQuiescence( new SelectionState(), world, _ => null, audio: null); var scheduler = new RecordingDestinationScheduler(); var coordinator = new WorldRevealCoordinator( transit, isRenderNeighborhoodReady: (_, _) => true, isSpawnCellReady: _ => true, isTerrainNeighborhoodReady: (_, _) => true, areCompositeTexturesReady: () => true, prepareCompositeTextures: (_, _) => { }, invalidateCompositeTextures: () => { }, isSpawnClaimUnhydratable: _ => false, quiescence: quiescence, streaming: scheduler); var calls = new List(); var frame = new RetailLiveFrameCoordinator( new global::AcDream.App.Tests.TestLiveObjectFramePhase( _ => calls.Add("objects")), world, new global::AcDream.App.Tests.TestLiveSessionFramePhase( () => calls.Add("network")), new global::AcDream.App.Tests.TestPostNetworkCommandFramePhase( () => calls.Add("commands")), new global::AcDream.App.Tests.TestLiveSpatialReconcilePhase( () => calls.Add("spatial")), availability); coordinator.Begin(RuntimePortalKind.Portal, 0x3032001Cu); frame.Tick(1f / 60f); Assert.Equal(["network", "commands"], calls); calls.Clear(); Assert.True(coordinator.Evaluate(0x3032001Cu).IsReady); coordinator.ObserveMaterialized(); frame.Tick(1f / 60f); Assert.Equal(["objects", "network", "commands", "spatial"], calls); Assert.Empty(scheduler.Ends); Assert.False(coordinator.Snapshot.WorldViewportObserved); } [Fact] public void SessionReset_ReleasesHostEdgesAndClearsRuntimeSnapshot() { var transit = new RuntimeWorldTransitState(); var availability = new WorldGenerationAvailabilityState(transit); var world = new GpuWorldState(availability: availability); var audio = new RecordingAudioQuiescence(); var quiescence = new WorldGenerationQuiescence( new SelectionState(), world, _ => null, audio); var scheduler = new RecordingDestinationScheduler(); var coordinator = new WorldRevealCoordinator( transit, isRenderNeighborhoodReady: (_, _) => false, isSpawnCellReady: _ => false, isTerrainNeighborhoodReady: (_, _) => false, areCompositeTexturesReady: () => false, prepareCompositeTextures: (_, _) => { }, invalidateCompositeTextures: () => { }, isSpawnClaimUnhydratable: _ => false, quiescence: quiescence, streaming: scheduler); long generation = coordinator.Begin( RuntimePortalKind.Portal, 0x3032001Cu); coordinator.ResetSession(); Assert.Equal(RuntimePortalSnapshot.Idle, coordinator.Snapshot); Assert.True(availability.IsWorldAvailable); Assert.Equal(1, audio.SuspendCalls); 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( RuntimePortalKind.Portal, 0x11340021u); long second = coordinator.Begin( RuntimePortalKind.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( RuntimePortalKind.Portal, 0x11340021u); long second = coordinator.Begin( RuntimePortalKind.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 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 Begins { get; } = []; public List Ends { get; } = []; public void BeginDestinationReveal(long revealGeneration) => Begins.Add(revealGeneration); public void EndDestinationReveal(long revealGeneration) => Ends.Add(revealGeneration); } }