acdream/tests/AcDream.App.Tests/Streaming/LandblockRenderPublisherTests.cs

365 lines
13 KiB
C#

using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
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 LandblockRenderPublisherTests
{
private const uint LandblockId = 0xA9B4FFFFu;
private static readonly LandblockBuildOrigin CapturedOrigin = new(0xA8, 0xB3);
[Fact]
public void BeginPublication_PublishesTerrainVisibilityAndAabbFromCapturedOrigin()
{
var calls = new List<string>();
var visibility = new CellVisibility();
var state = new GpuWorldState();
LoadedCell cell = VisibilityCell(0xA9B40100u);
LandblockBuild build = Build(
new EnvCellLandblockBuild(
LandblockId,
[cell],
Array.Empty<EnvCellShellPlacement>()));
LandblockMeshData mesh = MeshAtHeights(7f, 19f);
var publisher = Publisher(
calls,
visibility,
state,
out _,
out _,
out _);
LandblockRenderPublication receipt = publisher.BeginPublication(build, mesh);
Assert.Equal(new Vector3(192f, 192f, 0f), receipt.Origin);
Assert.Equal(["terrain:A9B4FFFF:192:192"], calls);
Assert.True(visibility.TryGetCell(cell.CellId, out LoadedCell? committed));
Assert.Same(cell, committed);
Assert.Single(receipt.VisibilityCells);
state.AddLandblock(build.Landblock);
var bounds = Assert.Single(state.LandblockBounds);
Assert.Equal(new Vector3(192f, 192f, -3f), bounds.AabbMin);
Assert.Equal(new Vector3(384f, 384f, 69f), bounds.AabbMax);
}
[Fact]
public void BeginPublication_VisibilityOnlyCellDoesNotRequireDrawableShell()
{
var visibility = new CellVisibility();
LoadedCell cell = VisibilityCell(0xA9B40100u);
LandblockBuild build = Build(new EnvCellLandblockBuild(
LandblockId,
[cell],
Array.Empty<EnvCellShellPlacement>()));
var publisher = Publisher(
new List<string>(),
visibility,
new GpuWorldState(),
out _,
out _,
out _);
LandblockRenderPublication receipt = publisher.BeginPublication(
build,
EmptyMesh());
publisher.CompletePublication(receipt);
Assert.True(visibility.TryGetCell(cell.CellId, out _));
Assert.Single(publisher.BuildingRegistries);
Assert.Empty(Assert.Single(publisher.BuildingRegistries).All());
}
[Fact]
public void BeginPublication_MismatchedEnvCellTransactionHasNoSideEffects()
{
var calls = new List<string>();
var visibility = new CellVisibility();
var state = new GpuWorldState();
LoadedCell cell = VisibilityCell(0xAAB40100u);
LandblockBuild build = Build(new EnvCellLandblockBuild(
0xAAB4FFFFu,
[cell],
Array.Empty<EnvCellShellPlacement>()));
var publisher = Publisher(
calls,
visibility,
state,
out _,
out _,
out _);
Assert.Throws<InvalidOperationException>(() =>
publisher.BeginPublication(build, EmptyMesh()));
Assert.Empty(calls);
Assert.False(visibility.TryGetCell(cell.CellId, out _));
Assert.Null(state.DetachLandblock(LandblockId));
Assert.Equal(0, publisher.Diagnostics.BeginCount);
}
[Fact]
public void CompletePublication_CommitsBuildingThenEnvCellsAndIsIdempotent()
{
var calls = new List<string>();
LandblockBuild build = Build(EmptyEnvCells());
int envCommits = 0;
LandblockRenderPublisher? publisher = null;
publisher = new LandblockRenderPublisher(
publishTerrain: (_, _, _) => calls.Add("terrain"),
removeTerrain: _ => { },
cellVisibility: new CellVisibility(),
worldState: new GpuWorldState(),
commitEnvCells: _ =>
{
Assert.Single(publisher!.BuildingRegistries);
calls.Add("env-cells");
envCommits++;
});
LandblockRenderPublication receipt = publisher.BeginPublication(build, EmptyMesh());
publisher.CompletePublication(receipt);
publisher.CompletePublication(receipt);
Assert.Equal(["terrain", "env-cells"], calls);
Assert.Equal(1, envCommits);
Assert.Single(publisher.BuildingRegistries);
Assert.Equal(1, publisher.Diagnostics.CompleteCount);
}
[Fact]
public void CompletePublication_FailedEnvCommitRetriesWithoutDuplicatingRegistry()
{
int attempts = 0;
LandblockBuild build = Build(EmptyEnvCells());
var publisher = new LandblockRenderPublisher(
publishTerrain: (_, _, _) => { },
removeTerrain: _ => { },
cellVisibility: new CellVisibility(),
worldState: new GpuWorldState(),
commitEnvCells: _ =>
{
attempts++;
if (attempts == 1)
throw new InvalidOperationException("injected EnvCell failure");
});
LandblockRenderPublication receipt = publisher.BeginPublication(build, EmptyMesh());
Assert.Throws<InvalidOperationException>(() =>
publisher.CompletePublication(receipt));
publisher.CompletePublication(receipt);
Assert.Equal(2, attempts);
Assert.Single(publisher.BuildingRegistries);
Assert.Equal(1, publisher.Diagnostics.CompleteCount);
}
[Fact]
public void CompletePublication_ForeignReceiptIsRejectedWithoutConsumingIt()
{
var first = Publisher(
new List<string>(),
new CellVisibility(),
new GpuWorldState(),
out int[] firstCommits,
out _,
out _);
var second = Publisher(
new List<string>(),
new CellVisibility(),
new GpuWorldState(),
out int[] secondCommits,
out _,
out _);
LandblockRenderPublication receipt = first.BeginPublication(
Build(EmptyEnvCells()),
EmptyMesh());
Assert.Throws<ArgumentException>(() =>
second.CompletePublication(receipt));
Assert.Empty(second.BuildingRegistries);
Assert.Equal(0, secondCommits[0]);
Assert.Equal(0, second.Diagnostics.CompleteCount);
first.CompletePublication(receipt);
Assert.Single(first.BuildingRegistries);
Assert.Equal(1, firstCommits[0]);
Assert.Equal(1, first.Diagnostics.CompleteCount);
}
[Fact]
public void PrepareAndRetirementOperationsStayBalancedAtTheirOwnerBoundary()
{
var calls = new List<string>();
var visibility = new CellVisibility();
var publisher = Publisher(
calls,
visibility,
new GpuWorldState(),
out _,
out int[] preparations,
out int[] envRemovals);
LoadedCell cell = VisibilityCell(0xA9B40100u);
EnvCellLandblockBuild envCells = new(
LandblockId,
[cell],
Array.Empty<EnvCellShellPlacement>());
LandblockRenderPublication receipt = publisher.BeginPublication(
Build(envCells),
EmptyMesh());
publisher.CompletePublication(receipt);
publisher.PrepareAfterRenderPins(envCells);
publisher.RemoveTerrain(LandblockId);
publisher.RemoveCellVisibility(LandblockId);
publisher.RemoveBuildingRegistry(LandblockId);
publisher.RemoveEnvironmentCells(LandblockId);
Assert.Equal(1, preparations[0]);
Assert.Equal(1, envRemovals[0]);
Assert.Equal(
[
"terrain:A9B4FFFF:192:192",
"prepare",
"remove-terrain:A9B4FFFF",
"remove-env:A9B4FFFF",
],
calls);
Assert.False(visibility.TryGetCell(cell.CellId, out _));
Assert.Empty(publisher.BuildingRegistries);
LandblockRenderPublisherDiagnostics diagnostics = publisher.Diagnostics;
Assert.Equal(1, diagnostics.EnvCellPreparationCount);
Assert.Equal(1, diagnostics.TerrainRemovalCount);
Assert.Equal(1, diagnostics.CellVisibilityRemovalCount);
Assert.Equal(1, diagnostics.BuildingRegistryRemovalCount);
Assert.Equal(1, diagnostics.EnvCellRemovalCount);
}
[Fact]
public void PublisherSurface_HasNoDatResidencyPolicyOrLiveOriginDependency()
{
Type type = typeof(LandblockRenderPublisher);
Type[] dependencies = type
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
.Select(field => field.FieldType)
.Concat(type.GetConstructors().SelectMany(ctor =>
ctor.GetParameters().Select(parameter => parameter.ParameterType)))
.ToArray();
Assert.DoesNotContain(dependencies, dependency =>
dependency.FullName?.Contains("DatReader", StringComparison.Ordinal) == true);
Assert.DoesNotContain(dependencies, dependency =>
dependency.FullName?.Contains("StreamingRegion", StringComparison.Ordinal) == true);
Assert.DoesNotContain(dependencies, dependency =>
dependency.FullName?.Contains("LiveWorldOrigin", StringComparison.Ordinal) == true);
}
[Fact]
public void GameWindow_HasNoDirectRenderPublicationBodies()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
Assert.DoesNotContain("_terrain.AddLandblockWithMesh", source, StringComparison.Ordinal);
Assert.DoesNotContain("_cellVisibility.CommitLandblock", source, StringComparison.Ordinal);
Assert.DoesNotContain("_buildingRegistries", source, StringComparison.Ordinal);
Assert.DoesNotContain("_envCellRenderer?.CommitLandblock", source, StringComparison.Ordinal);
Assert.DoesNotContain("EnsureEnvCellMeshesAfterPin", source, StringComparison.Ordinal);
}
private static LandblockRenderPublisher Publisher(
List<string> calls,
CellVisibility visibility,
GpuWorldState state,
out int[] envCommits,
out int[] preparations,
out int[] envRemovals)
{
envCommits = [0];
preparations = [0];
envRemovals = [0];
int[] commitCounter = envCommits;
int[] preparationCounter = preparations;
int[] removalCounter = envRemovals;
return new LandblockRenderPublisher(
publishTerrain: (id, _, origin) => calls.Add(
$"terrain:{id:X8}:{origin.X:0}:{origin.Y:0}"),
removeTerrain: id => calls.Add($"remove-terrain:{id:X8}"),
cellVisibility: visibility,
worldState: state,
commitEnvCells: _ => commitCounter[0]++,
prepareEnvCells: _ =>
{
preparationCounter[0]++;
calls.Add("prepare");
},
removeEnvCells: id =>
{
removalCounter[0]++;
calls.Add($"remove-env:{id:X8}");
});
}
private static LandblockBuild Build(EnvCellLandblockBuild envCells)
{
var physics = new PhysicsDatBundle(
new LandBlockInfo(),
new Dictionary<uint, EnvCell>(),
new Dictionary<uint, DatReaderWriter.DBObjs.Environment>(),
new Dictionary<uint, Setup>(),
new Dictionary<uint, GfxObj>());
return new LandblockBuild(
new LoadedLandblock(
LandblockId,
new LandBlock(),
Array.Empty<WorldEntity>(),
physics),
envCells,
CapturedOrigin);
}
private static EnvCellLandblockBuild EmptyEnvCells() => new(
LandblockId,
Array.Empty<LoadedCell>(),
Array.Empty<EnvCellShellPlacement>());
private static LoadedCell VisibilityCell(uint cellId) => new()
{
CellId = cellId,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
};
private static LandblockMeshData MeshAtHeights(float first, float second) => new(
[
new TerrainVertex(new Vector3(0f, 0f, first), Vector3.UnitZ, 0, 0, 0, 0),
new TerrainVertex(new Vector3(1f, 1f, second), Vector3.UnitZ, 0, 0, 0, 0),
],
Array.Empty<uint>());
private static LandblockMeshData EmptyMesh() =>
new(Array.Empty<TerrainVertex>(), Array.Empty<uint>());
private static string FindRepoRoot()
{
string? directory = AppContext.BaseDirectory;
while (directory is not null)
{
if (File.Exists(Path.Combine(directory, "AcDream.slnx")))
return directory;
directory = Directory.GetParent(directory)?.FullName;
}
throw new DirectoryNotFoundException("Could not locate repository root.");
}
}