440 lines
16 KiB
C#
440 lines
16 KiB
C#
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.Core.Terrain;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class LandblockBuildOriginTests
|
|
{
|
|
private const int SpinTimeoutMs = 2000;
|
|
private const int SpinStepMs = 10;
|
|
|
|
[Fact]
|
|
public void ExplicitMapZeroOrigin_IsDistinctFromUnspecifiedCompatibilityOrigin()
|
|
{
|
|
var mapZero = new LandblockBuildOrigin(0, 0);
|
|
|
|
Assert.True(mapZero.IsSpecified);
|
|
Assert.False(default(LandblockBuildOrigin).IsSpecified);
|
|
Assert.NotEqual(default, mapZero);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RequestAwareLoad_CarriesExplicitMapZeroOriginThroughFactoryAndCompletion()
|
|
{
|
|
const uint landblockId = 0x0000FFFFu;
|
|
var capturedOrigin = new LandblockBuildOrigin(0, 0);
|
|
LandblockBuildRequest? observedRequest = null;
|
|
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request =>
|
|
{
|
|
observedRequest = request;
|
|
return EmptyBuild(request.LandblockId, request.Origin);
|
|
},
|
|
buildMeshOrNull: (_, _) => EmptyMesh());
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 77,
|
|
capturedOrigin));
|
|
streamer.Start();
|
|
|
|
var loaded = Assert.IsType<LandblockStreamResult.Loaded>(
|
|
await DrainFirstAsync(streamer));
|
|
|
|
Assert.Equal(
|
|
new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.LoadNear,
|
|
77,
|
|
capturedOrigin),
|
|
observedRequest);
|
|
Assert.Equal(capturedOrigin, loaded.Build.Origin);
|
|
Assert.Equal(77ul, loaded.Generation);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RequestAwareLoad_WhenFactoryReturnsUnspecifiedOrigin_FailsAtMapZero()
|
|
{
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request => EmptyBuild(request.LandblockId, default));
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
0x0000FFFFu,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 78,
|
|
new LandblockBuildOrigin(0, 0)));
|
|
streamer.Start();
|
|
|
|
var failed = Assert.IsType<LandblockStreamResult.Failed>(
|
|
await DrainFirstAsync(streamer));
|
|
|
|
Assert.Contains("origin", failed.Error, StringComparison.OrdinalIgnoreCase);
|
|
Assert.Equal(78ul, failed.Generation);
|
|
}
|
|
|
|
[Fact]
|
|
public void CompatibilityLoader_RejectsEvenExplicitMapZeroOriginBeforeQueueing()
|
|
{
|
|
using var streamer = new LandblockStreamer(loadLandblock: _ => null);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => streamer.EnqueueLoad(
|
|
new LandblockBuildRequest(
|
|
0x0000FFFFu,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 79,
|
|
new LandblockBuildOrigin(0, 0))));
|
|
}
|
|
|
|
[Fact]
|
|
public void RequestAwareLoader_RejectsOriginlessCompatibilityEnqueue()
|
|
{
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request => EmptyBuild(request.LandblockId, request.Origin));
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
streamer.EnqueueLoad(0xA9B4FFFFu));
|
|
}
|
|
|
|
[Fact]
|
|
public void RequestAwareLoader_RejectsDirectRequestWithUnspecifiedOrigin()
|
|
{
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request => EmptyBuild(request.LandblockId, request.Origin));
|
|
|
|
Assert.Throws<InvalidOperationException>(() => streamer.EnqueueLoad(
|
|
new LandblockBuildRequest(
|
|
0xA9B4FFFFu,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 80,
|
|
Origin: default)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TwoQueuedLoads_RetainTheirDistinctOriginAndGeneration()
|
|
{
|
|
var observed = new List<LandblockBuildRequest>();
|
|
var firstOrigin = new LandblockBuildOrigin(0xA9, 0xB4);
|
|
var secondOrigin = new LandblockBuildOrigin(0x71, 0xEC);
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request =>
|
|
{
|
|
observed.Add(request);
|
|
return EmptyBuild(request.LandblockId, request.Origin);
|
|
},
|
|
buildMeshOrNull: (_, _) => EmptyMesh());
|
|
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
0xA9B4FFFFu,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 80,
|
|
firstOrigin));
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
0x71ECFFFFu,
|
|
LandblockStreamJobKind.LoadNear,
|
|
Generation: 81,
|
|
secondOrigin));
|
|
streamer.Start();
|
|
|
|
IReadOnlyList<LandblockStreamResult> completions =
|
|
await DrainCountAsync(streamer, 2);
|
|
|
|
Assert.Equal([firstOrigin, secondOrigin], observed.Select(request => request.Origin));
|
|
Assert.Equal([80ul, 81ul], completions.Select(result => result.Generation));
|
|
Assert.Equal(
|
|
[firstOrigin, secondOrigin],
|
|
completions
|
|
.Cast<LandblockStreamResult.Loaded>()
|
|
.Select(result => result.Build.Origin));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task QueuedPromotion_SupersedesOldFarWithItsOwnOriginAndGeneration()
|
|
{
|
|
var observed = new List<LandblockBuildRequest>();
|
|
var oldOrigin = new LandblockBuildOrigin(0xA9, 0xB4);
|
|
var newOrigin = new LandblockBuildOrigin(0x71, 0xEC);
|
|
const uint landblockId = 0x71ECFFFFu;
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request =>
|
|
{
|
|
observed.Add(request);
|
|
return EmptyBuild(request.LandblockId, request.Origin);
|
|
},
|
|
buildMeshOrNull: (_, _) => EmptyMesh());
|
|
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.LoadFar,
|
|
Generation: 82,
|
|
oldOrigin));
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.PromoteToNear,
|
|
Generation: 83,
|
|
newOrigin));
|
|
streamer.Start();
|
|
|
|
var promoted = Assert.IsType<LandblockStreamResult.Promoted>(
|
|
await DrainFirstAsync(streamer));
|
|
|
|
Assert.Equal([new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.PromoteToNear,
|
|
83,
|
|
newOrigin)], observed);
|
|
Assert.Equal(newOrigin, promoted.Build.Origin);
|
|
Assert.Equal(83ul, promoted.Generation);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FarLoad_StripsEnvCellsAndPhysicsEvenWhenEntityListIsAlreadyEmpty()
|
|
{
|
|
const uint landblockId = 0xA9B4FFFFu;
|
|
var origin = new LandblockBuildOrigin(0xA9, 0xB4);
|
|
var physics = new PhysicsDatBundle(
|
|
new LandBlockInfo(),
|
|
new Dictionary<uint, EnvCell>(),
|
|
new Dictionary<uint, DatReaderWriter.DBObjs.Environment>(),
|
|
new Dictionary<uint, Setup>(),
|
|
new Dictionary<uint, GfxObj>());
|
|
var envCells = new EnvCellLandblockBuild(
|
|
landblockId,
|
|
Array.Empty<AcDream.App.Rendering.LoadedCell>(),
|
|
Array.Empty<EnvCellShellPlacement>());
|
|
using var streamer = LandblockStreamer.CreateForRequests(
|
|
loadLandblock: request => new LandblockBuild(
|
|
new LoadedLandblock(
|
|
request.LandblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>(),
|
|
physics),
|
|
envCells,
|
|
request.Origin),
|
|
buildMeshOrNull: (_, _) => EmptyMesh());
|
|
streamer.EnqueueLoad(new LandblockBuildRequest(
|
|
landblockId,
|
|
LandblockStreamJobKind.LoadFar,
|
|
Generation: 84,
|
|
origin));
|
|
streamer.Start();
|
|
|
|
var loaded = Assert.IsType<LandblockStreamResult.Loaded>(
|
|
await DrainFirstAsync(streamer));
|
|
|
|
Assert.Equal(LandblockStreamTier.Far, loaded.Tier);
|
|
Assert.Empty(loaded.Landblock.Entities);
|
|
Assert.Same(PhysicsDatBundle.Empty, loaded.Landblock.PhysicsDats);
|
|
Assert.Null(loaded.Build.EnvCells);
|
|
Assert.Equal(origin, loaded.Build.Origin);
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildFactoryAndRenderPublisher_UseCapturedOriginWithoutGameWindowFacade()
|
|
{
|
|
string root = FindRepoRoot();
|
|
string gameWindowSource = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindow.cs"));
|
|
string buildSource = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Streaming",
|
|
"LandblockBuildFactory.cs"));
|
|
string renderPublisherSource = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Streaming",
|
|
"LandblockRenderPublisher.cs"));
|
|
string recenterSource = File.ReadAllText(Path.Combine(
|
|
root,
|
|
"src",
|
|
"AcDream.App",
|
|
"Streaming",
|
|
"StreamingOriginRecenterCoordinator.cs"));
|
|
Assert.Contains("request.Origin", buildSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_liveCenterX", buildSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_liveCenterY", buildSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"BuildLandblockForStreaming",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"BuildSceneryEntitiesForStreaming",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"BuildInteriorEntitiesForStreaming",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"BuildPhysicsDatBundle",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"ApplyLoadedTerrain",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"PublishLandblockStaticLightingBeforeCollision",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_landblockPhysicsPublisher!.RemoveLandblock",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_landblockRenderPublisher",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_landblockPhysicsPublisher",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_landblockStaticPresentationPublisher",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"_landblockPresentationPipeline",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"new AcDream.App.Streaming.LandblockRenderPublisher(",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"new AcDream.App.Streaming.LandblockPhysicsPublisher(",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.Contains(
|
|
"new AcDream.App.Streaming.LandblockStaticPresentationPublisher(",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain("applyTerrain:", gameWindowSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("demoteNearLayer:", gameWindowSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("retirementCoordinator:", gameWindowSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"_liveWorldOrigin.Recenter(lbX, lbY)",
|
|
gameWindowSource,
|
|
StringComparison.Ordinal);
|
|
int retirementBarrier = recenterSource.IndexOf(
|
|
"IsOriginRecenterRetirementComplete()",
|
|
StringComparison.Ordinal);
|
|
int originCommit = recenterSource.IndexOf(
|
|
"_origin.Recenter(",
|
|
StringComparison.Ordinal);
|
|
int destinationCommit = recenterSource.IndexOf(
|
|
"_streaming.TryCommitOriginRecenter(",
|
|
StringComparison.Ordinal);
|
|
Assert.True(retirementBarrier >= 0);
|
|
Assert.True(originCommit > retirementBarrier);
|
|
Assert.True(destinationCommit > originCommit);
|
|
Assert.Contains("ComputeOrigin(landblockId, build.Origin)", renderPublisherSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_liveCenterX", renderPublisherSource, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("_liveCenterY", renderPublisherSource, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void GameWindowShutdownKeepsStreamerAliveUntilSessionResetConverges()
|
|
{
|
|
string source = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindow.cs"));
|
|
int sessionStage = source.IndexOf(
|
|
"new ResourceShutdownStage(\"session lifetime\"",
|
|
StringComparison.Ordinal);
|
|
Assert.True(sessionStage >= 0);
|
|
|
|
int sessionOperation = source.IndexOf(
|
|
"new(\"live session\", () =>",
|
|
sessionStage,
|
|
StringComparison.Ordinal);
|
|
Assert.True(sessionOperation > sessionStage);
|
|
|
|
int sessionDispose = source.IndexOf(
|
|
"controller.Dispose()",
|
|
sessionOperation,
|
|
StringComparison.Ordinal);
|
|
Assert.True(sessionDispose > sessionOperation);
|
|
|
|
int disposalCompletionBarrier = source.IndexOf(
|
|
"if (!controller.IsDisposalComplete)",
|
|
sessionDispose,
|
|
StringComparison.Ordinal);
|
|
Assert.True(disposalCompletionBarrier > sessionDispose);
|
|
|
|
int sessionReferenceRelease = source.IndexOf(
|
|
"_liveSessionController = null;",
|
|
disposalCompletionBarrier,
|
|
StringComparison.Ordinal);
|
|
Assert.True(sessionReferenceRelease > disposalCompletionBarrier);
|
|
|
|
int dependentStage = source.IndexOf(
|
|
"new ResourceShutdownStage(\"session dependents\"",
|
|
sessionReferenceRelease,
|
|
StringComparison.Ordinal);
|
|
Assert.True(dependentStage > sessionReferenceRelease);
|
|
|
|
int streamerDispose = source.IndexOf(
|
|
"new(\"streamer\", () => _streamer?.Dispose())",
|
|
dependentStage,
|
|
StringComparison.Ordinal);
|
|
|
|
Assert.True(streamerDispose > dependentStage);
|
|
}
|
|
|
|
private static LandblockBuild EmptyBuild(uint landblockId, LandblockBuildOrigin origin) =>
|
|
new(
|
|
new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()),
|
|
Origin: origin);
|
|
|
|
private static LandblockMeshData EmptyMesh() =>
|
|
new(Array.Empty<TerrainVertex>(), Array.Empty<uint>());
|
|
|
|
private static async Task<LandblockStreamResult> DrainFirstAsync(
|
|
LandblockStreamer streamer) =>
|
|
(await DrainCountAsync(streamer, 1))[0];
|
|
|
|
private static async Task<IReadOnlyList<LandblockStreamResult>> DrainCountAsync(
|
|
LandblockStreamer streamer,
|
|
int count)
|
|
{
|
|
var results = new List<LandblockStreamResult>(count);
|
|
for (int i = 0; i < SpinTimeoutMs / SpinStepMs && results.Count < count; i++)
|
|
{
|
|
results.AddRange(streamer.DrainCompletions(count - results.Count));
|
|
if (results.Count < count)
|
|
await Task.Delay(SpinStepMs);
|
|
}
|
|
|
|
Assert.Equal(count, results.Count);
|
|
return results;
|
|
}
|
|
|
|
private static string FindRepoRoot()
|
|
{
|
|
string? dir = AppContext.BaseDirectory;
|
|
while (dir is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(dir, "AcDream.slnx")))
|
|
return dir;
|
|
dir = Directory.GetParent(dir)?.FullName;
|
|
}
|
|
|
|
throw new DirectoryNotFoundException("Could not locate AcDream.slnx.");
|
|
}
|
|
}
|