using System.IO.Pipes; using System.Text.Json; using XFEEDToolBox.Core.Galaxy; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class GalaxyStreamingTests { [Test] public async Task UnverifiedEmptyLegacyTilesCanReloadAfterTheGameFinishesLoading() { using var session=new GalaxyStreamingSession(new(){UnverifiedCacheMilliseconds=0}); var view=SmallView();var queries=0; Task Empty(Coordinates c,double radius,CancellationToken token) {queries++;return Task.FromResult(new GalaxyRegionResult([],0,1,1));} var first=await Collect(session,view,Empty);var firstCount=queries; Task Loaded(Coordinates c,double radius,CancellationToken token) {queries++;return Task.FromResult(new GalaxyRegionResult([new("Loaded fixture",new(100,100,100))],0,1,1));} var second=await Collect(session,view,Loaded); Check.That(first[^1].UnverifiedTiles>0&&!first[^1].CoverageComplete); Check.That(queries>firstCount&&second.SelectMany(u=>u.Systems).Any(s=>s.Name=="Loaded fixture")); Check.That(!second[^1].CoverageComplete,"A refreshed legacy tile still cannot certify complete coverage."); } private static GalaxyViewport SmallView(double x = 100) => new(new(x, 100, 100), new(1, 0, 0), new(0, 1, 0), new(0, 0, 1), 7, 7, 7); private static async Task> Collect(GalaxyStreamingSession session, GalaxyViewport view, Func> query) { var updates = new List(); await foreach (var update in session.StreamAsync(view, query)) updates.Add(update); return updates; } [Test] public void FrustumPlanCoversNearAndFarOffAxisStarsBeyondTheOldPivotSphere() { var camera = GalaxyCamera.Create(new(0, 0, 26000), 1600, 900, .6, -.2, 1); var samples = new List(); foreach (var depth in new[] { camera.Near * 3, camera.Distance, camera.Far * .98 }) foreach (var side in new[] { -.9, .9 }) { var offset = camera.Forward * depth + camera.Right * (side * camera.Width / (2 * camera.FocalLength) * depth) + camera.Up * (side * camera.Height / (2 * camera.FocalLength) * depth); samples.Add(new(camera.Eye.X + offset.X, camera.Eye.Y + offset.Y, camera.Eye.Z + offset.Z)); } Check.That(samples.All(p => camera.Project(p).IsVisible)); Check.That(samples.Any(p => p.DistanceTo(camera.Focus) > 700)); var plan = new GalaxyQueryPlan(camera.ToViewport()); var remaining = samples.ToList(); var count = 0; while (plan.TryTake(out var tile)) { Check.That(tile.Radius <= 700 && GalaxyQueryPlan.Intersects(camera.ToViewport(), tile, 1.12)); remaining.RemoveAll(tile.Contains); count++; Check.That(count < 20000, "Frustum construction must be lazy and bounded."); } Check.Equal(0, remaining.Count); Check.Equal(0, plan.OmittedTiles); } [Test] public async Task TruncatedAndCoarseRegionsRefineInsteadOfBeingCachedAsCovered() { using var session = new GalaxyStreamingSession(); var queries = new List(); Task Query(Coordinates c, double radius, CancellationToken token) { queries.Add(radius); return Task.FromResult(radius > 10 ? new GalaxyRegionResult([], 2, 8, 17, true, false) : GalaxyRegionResult.Covered([])); } var updates = await Collect(session, SmallView(), Query); Check.That(queries.Any(r => r > 10) && queries.Any(r => r <= 10)); Check.That(updates.Any(u => u.RefinedTiles > 0 && !u.CoverageComplete)); Check.That(updates[^1].Finished && updates[^1].CoverageComplete && updates[^1].PendingTiles == 0); var firstQueries = queries.Count; var cached = await Collect(session, SmallView(100.25), Query); Check.Equal(firstQueries, queries.Count); Check.That(cached.Any(u => u.FromCache) && cached[^1].CoverageComplete); } [Test] public async Task SmallLegacyRepliesRemainUnverifiedAndCacheIsBounded() { using var session = new GalaxyStreamingSession(new() { MaximumCachedTiles = 3, MaximumCachedSystems = 3 }); Task Query(Coordinates c, double radius, CancellationToken token) => Task.FromResult(new GalaxyRegionResult([new("At " + c, c)], 0, 8, 8)); var updates = await Collect(session, SmallView(), Query); Check.That(updates[^1].Finished && !updates[^1].CoverageComplete && updates[^1].UnverifiedTiles > 0); Check.That(session.CachedTiles <= 3); } [Test] public async Task FailedTerminalTileCanBeRetriedByTheNextView() { using var session=new GalaxyStreamingSession(new(){MinimumTileLevel=4});var queried=0; Task Query(Coordinates c,double radius,CancellationToken token) {queried++;return Task.FromResult(new GalaxyRegionResult([],0,1,2,false,false,1){Error="failed cell"});} var first=await Collect(session,SmallView(),Query);var before=queried; var second=await Collect(session,SmallView(),Query); Check.That(before>0&&queried>before&&!first[^1].CoverageComplete&&!second[^1].CoverageComplete); } [Test] public async Task ANewViewCancelsStaleWorkBeforeItCanPublishOrCoverTiles() { using var session = new GalaxyStreamingSession(); var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); async Task OldQuery(Coordinates c, double radius, CancellationToken token) { started.TrySetResult(); await Task.Delay(Timeout.Infinite, token); return GalaxyRegionResult.Covered([new("Stale", c)]); } await using var old = session.StreamAsync(SmallView(), OldQuery).GetAsyncEnumerator(); var oldMove = old.MoveNextAsync().AsTask(); await started.Task.WaitAsync(TimeSpan.FromSeconds(3)); var latest = await Collect(session, SmallView(10000), (c, r, t) => Task.FromResult(GalaxyRegionResult.Covered([]))); var cancelled = false; try { await oldMove; } catch (OperationCanceledException) { cancelled = true; } Check.That(cancelled && latest[^1].CoverageComplete); Check.That(latest.SelectMany(u => u.Systems).All(s => s.Name != "Stale")); } [Test] public async Task OverlappingCameraUpdatesFinishAndReuseAUsefulInflightTile() { using var session = new GalaxyStreamingSession(); var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); CancellationToken acceptedToken = default; async Task Accepted(Coordinates c, double radius, CancellationToken token) { acceptedToken = token; started.TrySetResult(); await release.Task.WaitAsync(token); return GalaxyRegionResult.Covered([new("Useful native tile", c, 123)]); } await using var old = session.StreamAsync(SmallView(), Accepted).GetAsyncEnumerator(); var oldMove = old.MoveNextAsync().AsTask(); await started.Task.WaitAsync(TimeSpan.FromSeconds(3)); var latestTask = Collect(session, SmallView(100.25), (c, r, t) => Task.FromResult(GalaxyRegionResult.Covered([]))); Check.That(!acceptedToken.IsCancellationRequested, "A useful tile must survive continuous overlapping pan updates."); release.TrySetResult(); var latest = await latestTask; var cancelled = false; try { await oldMove; } catch (OperationCanceledException) { cancelled = true; } Check.That(cancelled && latest[^1].CoverageComplete); Check.That(latest.SelectMany(u => u.Systems).Any(s => s.Name == "Useful native tile")); } [Test] public void CoverageReceiptsDistinguishCapsMassLodFailuresAndLegacyUnknown() { var complete = NativeGalaxyCatalog.ReadCoverage(Check.Json("""{"minimumMassCode":0,"processedCells":17,"totalCells":17,"failedCells":0,"complete":true,"truncated":false}"""), []); Check.That(complete.IsFullyCovered); var capped = NativeGalaxyCatalog.ReadCoverage(Check.Json("""{"minimumMassCode":0,"processedCells":17,"totalCells":17,"complete":true}"""), [], 24000); Check.That(capped.Truncated && !capped.IsFullyCovered && capped.NeedsSubdivision); var coarse = NativeGalaxyCatalog.ReadCoverage(Check.Json("""{"minimumMassCode":4,"processedCells":17,"totalCells":17,"complete":true}"""), []); Check.That(!coarse.IsFullyCovered && coarse.NeedsSubdivision); var failed = NativeGalaxyCatalog.ReadCoverage(Check.Json("""{"minimumMassCode":0,"processedCells":17,"totalCells":17,"failedCells":1,"complete":true}"""), []); Check.That(!failed.IsFullyCovered); var legacy = NativeGalaxyCatalog.ReadCoverage(Check.Json("{}"), []); Check.That(legacy.MinimumMassCode == -1 && !legacy.IsFullyCovered && !legacy.HasCoverageEvidence); } [Test] public void RecordedTwentyFourThousandStarGalacticCentreResponseDoesNotProveCoverage() { using var fixture = JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "Galaxy", "native-core-700-sample.json"))); var data = fixture.RootElement; var rows = data.GetProperty("sample").EnumerateArray().Select(e => { var p = e.GetProperty("position"); return new GalacticSystem(e.GetProperty("name").GetString()!, new(p[0].GetDouble(), p[1].GetDouble(), p[2].GetDouble()), long.Parse(e.GetProperty("systemAddress").GetString()!), e.GetProperty("starClass").GetString()!, "Recorded native Stellar Forge"); }).ToArray(); Check.Equal(24000, data.GetProperty("returnedCount").GetInt32()); Check.Equal(32, rows.Length); Check.That(rows.All(s => s.Position.IsValid && s.SystemAddress is > 0)); var receipt = NativeGalaxyCatalog.ReadCoverage(Check.Json("{}"), rows, data.GetProperty("returnedCount").GetInt32()); Check.That(receipt.Truncated && receipt.NeedsSubdivision && !receipt.IsFullyCovered); Check.That(rows.All(s => s.Position.DistanceTo(new(25.21875, -20.90625, 25899.96875)) <= 700.00001)); } private static async Task Serve(int id, object response, CancellationToken token, Action inspect) { await using var server = new NamedPipeServerStream($"XFEEDToolBox.Navigation.{id}", PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); await server.WaitForConnectionAsync(token); var buffer = new byte[65536]; var count = await server.ReadAsync(buffer, token); using (var request = JsonDocument.Parse(buffer.AsMemory(0, count))) inspect(request.RootElement); await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(new { ok = true, result = response }), token); await server.WriteAsync(new byte[] { 10 }, token); } [Test] public async Task CancelledNativeRegionRetiresItsGenerationBeforeTheNextQuery() { var id = Random.Shared.Next(145000000, 150000000); using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5)); using var cancel = new CancellationTokenSource(); var operations = new List(); void Inspect(JsonElement e) => operations.Add(e.GetProperty("op").GetString()!); var server = Task.Run(async () => { await Serve(id, new { generation = 70, done = false, error = "", total = 0, items = Array.Empty() }, deadline.Token, e => { Inspect(e); cancel.Cancel(); }); await Serve(id, new { generation = 70, cancelled = true }, deadline.Token, Inspect); await Serve(id, new { generation = 70, done = true, error = "", total = 0, items = Array.Empty() }, deadline.Token, Inspect); await Serve(id, new { generation = 71, done = true, error = "", total = 0, items = Array.Empty(), minimumMassCode = 0, processedCells = 9, totalCells = 9, complete = true }, deadline.Token, Inspect); }); var catalog = new NativeGalaxyCatalog(id); var cancelled = false; var cancelledProgress = 0; catalog.RegionProgress += _ => { if(cancel.IsCancellationRequested)cancelledProgress++; }; try { await catalog.RegionPageAsync(new(0, 0, 0), 10, cancel.Token); } catch (OperationCanceledException) { cancelled = true; } Check.Equal(0,cancelledProgress); var next = await catalog.RegionPageAsync(new(0, 0, 26000), 10, deadline.Token); await server; Check.That(cancelled && next.IsFullyCovered); Check.That(operations.SequenceEqual(new[] { "galaxy.region", "galaxy.cancel", "galaxy.result", "galaxy.region" })); } [Test] public async Task BusyNativeRegionPublishesActualRowsBeforeDoneWithoutDownloadingThemTwice() { var id=Random.Shared.Next(151000000,155000000); using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var firstProgress=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var progress=new List();var offsets=new List(); object[] Rows(int offset,int count)=>Enumerable.Range(offset,count).Select(i=>(object)new {name="Native progress "+i,systemAddress=(9007199254740993L+i).ToString(),position=new[]{(double)i,0d,26000d},starClass="M"}).ToArray(); void Inspect(JsonElement e)=>offsets.Add(e.GetProperty("offset").GetInt32()); var server=Task.Run(async()=> { await Serve(id,new{generation=90,done=false,error="",total=0,items=Array.Empty()},deadline.Token,e=>Check.Equal("galaxy.region",e.GetProperty("op").GetString())); await Serve(id,new{generation=90,done=false,error="",total=96,items=Rows(0,96),minimumMassCode=0,processedCells=1,totalCells=9,complete=false},deadline.Token,Inspect); // Native work cannot finish in this test until real partial rows // have reached the caller; waiting for done would deadlock it. var early=await firstProgress.Task.WaitAsync(deadline.Token); Check.That(!early.Done&&!early.Coverage.Complete&&early.Systems.Count==96&&early.DownloadedCount==96); await Serve(id,new{generation=90,done=false,error="",total=192,items=Rows(96,96),minimumMassCode=0,processedCells=2,totalCells=9,complete=false},deadline.Token,Inspect); await Serve(id,new{generation=90,done=true,error="",total=192,items=Array.Empty(),minimumMassCode=0,processedCells=9,totalCells=9,complete=true},deadline.Token,Inspect); }); var catalog=new NativeGalaxyCatalog(id); catalog.RegionProgress+=update=>{progress.Add(update);if(update.Systems.Count>0)firstProgress.TrySetResult(update);}; var result=await catalog.RegionPageAsync(new(0,0,26000),100,deadline.Token); await server; Check.Equal(192,result.Systems.Count);Check.That(result.IsFullyCovered); Check.That(offsets.SequenceEqual(new[]{0,96,192})); var published=progress.SelectMany(p=>p.Systems).ToArray(); Check.Equal(192,published.Length);Check.Equal(192,published.Select(s=>s.SystemAddress).Distinct().Count()); Check.That(progress.All(p=>p.Generation==90&&p.Systems.Count<=512)&&progress[^1].Done&&progress[^1].Coverage.IsFullyCovered); } [Test] public async Task LegacyProtocolClampsSmallTilesToFiveLightYearsAndKeepsCoverageUnverified() { var id=Random.Shared.Next(156000000,160000000); using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var radii=new List(); var server=Task.Run(async()=> { await Serve(id,new{processId=id,schema=1,name="XFEEDToolBox.Navigation",galaxyRegion=true},deadline.Token,e=>Check.Equal("describe",e.GetProperty("op").GetString())); await Serve(id,new{generation=101,done=true,error="",total=0,items=Array.Empty(),minimumMassCode=0,processedCells=8,totalCells=8},deadline.Token,e=>radii.Add(e.GetProperty("radius").GetDouble())); await Serve(id,new{generation=102,done=true,error="",total=0,items=Array.Empty(),minimumMassCode=0,processedCells=8,totalCells=8,complete=true},deadline.Token,e=>radii.Add(e.GetProperty("radius").GetDouble())); }); var catalog=new NativeGalaxyCatalog(id); await catalog.InspectProtocolAsync(deadline.Token); var legacy=await catalog.RegionPageAsync(new(0,0,0),.2,deadline.Token); Check.That(!legacy.IsFullyCovered&&!catalog.SupportsRegionCoverage&&catalog.MinimumRegionRadius==5); catalog.ApplyProtocolDescription(JsonSerializer.SerializeToElement(new{processId=id,galaxyRegionCoverage=true,galaxyCancellation=true,galaxyMinimumRadius=.05})); var modern=await catalog.RegionPageAsync(new(0,0,0),.2,deadline.Token); await server; Check.That(modern.IsFullyCovered&&catalog.SupportsCancellation&&radii.SequenceEqual(new[]{5d,.2})); } [Test] public async Task CompletedNativeFailureRetainsFinalRowsAndRequestsAdaptiveRefinement() { var id=Random.Shared.Next(161000000,165000000); using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var rows=new[]{new{name="Actual partial star",systemAddress="12345",position=new[]{1d,2d,3d},starClass="K"}}; var server=Task.Run(()=>Serve(id,new{generation=111,done=true,error="游戏星区数据在读取时变化",total=1,items=rows,minimumMassCode=0,processedCells=1,totalCells=8,failedCells=1,complete=false},deadline.Token,e=>Check.Equal("galaxy.region",e.GetProperty("op").GetString()))); var catalog=new NativeGalaxyCatalog(id);var progress=new List();catalog.RegionProgress+=progress.Add; var result=await catalog.RegionPageAsync(new(0,0,0),10,deadline.Token); await server; Check.Equal(1,result.Systems.Count); Check.That(result.Error.Length>0&&result.FailedCells>=1&&result.NeedsSubdivision&&!result.IsFullyCovered); Check.That(progress[^1].Done&&progress[^1].Systems.Single().Name=="Actual partial star"&&progress[^1].Coverage.Error.Length>0); } [Test] public async Task NativeDeadlineRetiresAcceptedGenerationUsingAFreshCleanupDeadline() { var id=Random.Shared.Next(166000000,170000000); using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(5)); var retired=false;var retirementRead=false;var operations=new List(); var server=Task.Run(async()=> { while(!retirementRead) { await using var pipe=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{id}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous); await pipe.WaitForConnectionAsync(deadline.Token);var buffer=new byte[65536];var count=await pipe.ReadAsync(buffer,deadline.Token); using var request=JsonDocument.Parse(buffer.AsMemory(0,count));var op=request.RootElement.GetProperty("op").GetString()!;operations.Add(op); if(op=="galaxy.cancel")retired=true; if(op=="galaxy.result"&&retired)retirementRead=true; var response=new{ok=true,result=new{generation=121,done=retired,error="",total=0,items=Array.Empty()}}; try{await pipe.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(response),deadline.Token);await pipe.WriteAsync(new byte[]{10},deadline.Token);}catch(IOException){} } await Serve(id,new{generation=122,done=true,error="",total=0,items=Array.Empty(),minimumMassCode=0,processedCells=8,totalCells=8,complete=true},deadline.Token,e=>Check.Equal("galaxy.region",e.GetProperty("op").GetString())); }); var catalog=new NativeGalaxyCatalog(id,TimeSpan.FromMilliseconds(120));var timedOut=false; try{await catalog.RegionPageAsync(new(0,0,0),10,deadline.Token);}catch(TimeoutException){timedOut=true;} var next=await catalog.RegionPageAsync(new(0,0,0),10,deadline.Token); await server; Check.That(timedOut&&retired&&retirementRead&&next.IsFullyCovered); Check.That(operations.Contains("galaxy.cancel")&&operations[^1]=="galaxy.result"); } [Test] public async Task NativeBookmarkAddressResolutionPreservesLargeAddressAndRejectsSubstitution() { var id=Random.Shared.Next(171000000,175000000); using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(5)); const long address=9007199254740993; var server=Task.Run(async()=> { await Serve(id,new{generation=131,done=true,error="",items=new[]{new{name="Original bookmark system",systemAddress=address.ToString(),position=new[]{25.21875,-20.90625,25899.96875},starClass="H"}}},deadline.Token,e=> {Check.Equal("galaxy.resolve",e.GetProperty("op").GetString());Check.Equal(address.ToString(),e.GetProperty("systemAddress").GetString());Check.That(!e.TryGetProperty("query",out _));}); await Serve(id,new{generation=132,done=true,error="",items=new[]{new{name="Unrelated system",systemAddress=(address+1).ToString(),position=new[]{0d,0d,0d},starClass="G"}}},deadline.Token,e=>Check.Equal(address.ToString(),e.GetProperty("systemAddress").GetString())); }); var catalog=new NativeGalaxyCatalog(id); var resolved=await catalog.ResolveAddressAsync(address,deadline.Token); Check.Equal(address,resolved.SystemAddress);Check.Equal(25899.96875,resolved.Position.Z); var rejected=false;try{await catalog.ResolveAddressAsync(address,deadline.Token);}catch(InvalidDataException){rejected=true;} await server;Check.That(rejected); } }