Join destination scheduling to the canonical reveal generation, protect its share across every typed frame-budget dimension, and prevent stale work from clearing a replacement reservation. Remove forced incomplete materialization and project retail's centered portal wait cue while the authored tunnel remains active. Tests: Release build clean; 91 focused reservation/reveal tests; full solution 8,158 passed, 5 skipped. Co-authored-by: Codex <noreply@openai.com>
204 lines
7.3 KiB
C#
204 lines
7.3 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) => 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);
|
|
}
|
|
|
|
[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 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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|