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( 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( 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(() => 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(() => streamer.EnqueueLoad(0xA9B4FFFFu)); } [Fact] public void RequestAwareLoader_RejectsDirectRequestWithUnspecifiedOrigin() { using var streamer = LandblockStreamer.CreateForRequests( loadLandblock: request => EmptyBuild(request.LandblockId, request.Origin)); Assert.Throws(() => streamer.EnqueueLoad( new LandblockBuildRequest( 0xA9B4FFFFu, LandblockStreamJobKind.LoadNear, Generation: 80, Origin: default))); } [Fact] public async Task TwoQueuedLoads_RetainTheirDistinctOriginAndGeneration() { var observed = new List(); 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 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() .Select(result => result.Build.Origin)); } [Fact] public async Task QueuedPromotion_SupersedesOldFarWithItsOwnOriginAndGeneration() { var observed = new List(); 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( 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(), new Dictionary(), new Dictionary(), new Dictionary()); var envCells = new EnvCellLandblockBuild( landblockId, Array.Empty(), Array.Empty()); using var streamer = LandblockStreamer.CreateForRequests( loadLandblock: request => new LandblockBuild( new LoadedLandblock( request.LandblockId, new LandBlock(), Array.Empty(), physics), envCells, request.Origin), buildMeshOrNull: (_, _) => EmptyMesh()); streamer.EnqueueLoad(new LandblockBuildRequest( landblockId, LandblockStreamJobKind.LoadFar, Generation: 84, origin)); streamer.Start(); var loaded = Assert.IsType( 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 GameWindowBuildAndPublication_UseCapturedOriginInsteadOfLiveCenter() { string root = FindRepoRoot(); string source = File.ReadAllText(Path.Combine( root, "src", "AcDream.App", "Rendering", "GameWindow.cs")); string buildSection = Slice( source, "private AcDream.App.Streaming.LandblockBuild? BuildLandblockForStreaming(", "private void AdvanceLandblockPresentationRetirement("); string publicationSection = Slice( source, "private void ApplyLoadedTerrainLocked(", "private void OnUpdate(double dt)"); Assert.Contains("request.Origin", buildSection, StringComparison.Ordinal); Assert.DoesNotContain("_liveCenterX", buildSection, StringComparison.Ordinal); Assert.DoesNotContain("_liveCenterY", buildSection, StringComparison.Ordinal); Assert.Contains("build.Origin.CenterX", publicationSection, StringComparison.Ordinal); Assert.Contains("build.Origin.CenterY", publicationSection, StringComparison.Ordinal); Assert.DoesNotContain("_liveCenterX", publicationSection, StringComparison.Ordinal); Assert.DoesNotContain("_liveCenterY", publicationSection, StringComparison.Ordinal); } private static LandblockBuild EmptyBuild(uint landblockId, LandblockBuildOrigin origin) => new( new LoadedLandblock( landblockId, new LandBlock(), Array.Empty()), Origin: origin); private static LandblockMeshData EmptyMesh() => new(Array.Empty(), Array.Empty()); private static async Task DrainFirstAsync( LandblockStreamer streamer) => (await DrainCountAsync(streamer, 1))[0]; private static async Task> DrainCountAsync( LandblockStreamer streamer, int count) { var results = new List(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 Slice(string source, string startMarker, string endMarker) { int start = source.IndexOf(startMarker, StringComparison.Ordinal); Assert.True(start >= 0, $"Missing source marker: {startMarker}"); int end = source.IndexOf(endMarker, start, StringComparison.Ordinal); Assert.True(end > start, $"Missing source marker: {endMarker}"); return source[start..end]; } 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."); } }