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<GalaxyRegionResult> 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<GalaxyRegionResult> 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<List<GalaxyStreamUpdate>> Collect(GalaxyStreamingSession session, GalaxyViewport view,
Func<Coordinates, double, CancellationToken, Task<GalaxyRegionResult>> query)
{
var updates = new List<GalaxyStreamUpdate>();
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<Coordinates>();
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<double>();
Task<GalaxyRegionResult> 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<GalaxyRegionResult> 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<GalaxyRegionResult> 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<GalaxyRegionResult> 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<GalaxyRegionResult> 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<JsonElement> 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<string>();
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<object>() }, 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<object>() }, deadline.Token, Inspect);
await Serve(id, new { generation = 71, done = true, error = "", total = 0, items = Array.Empty<object>(), 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<GalaxyNativeRegionProgress>(TaskCreationOptions.RunContinuationsAsynchronously);
var progress=new List<GalaxyNativeRegionProgress>();var offsets=new List<int>();
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<object>()},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<object>(),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<double>();
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<object>(),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<object>(),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<GalaxyNativeRegionProgress>();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<string>();
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<object>()}};
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<object>(),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);
}
}
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<GalaxyRegionResult> 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<GalaxyRegionResult> 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<List<GalaxyStreamUpdate>> Collect(GalaxyStreamingSession session, GalaxyViewport view,
Func<Coordinates, double, CancellationToken, Task<GalaxyRegionResult>> query)
{
var updates = new List<GalaxyStreamUpdate>();
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<Coordinates>();
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<double>();
Task<GalaxyRegionResult> 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<GalaxyRegionResult> 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<GalaxyRegionResult> 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<GalaxyRegionResult> 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<GalaxyRegionResult> 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<JsonElement> 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<string>();
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<object>() }, 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<object>() }, deadline.Token, Inspect);
await Serve(id, new { generation = 71, done = true, error = "", total = 0, items = Array.Empty<object>(), 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<GalaxyNativeRegionProgress>(TaskCreationOptions.RunContinuationsAsynchronously);
var progress=new List<GalaxyNativeRegionProgress>();var offsets=new List<int>();
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<object>()},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<object>(),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<double>();
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<object>(),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<object>(),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<GalaxyNativeRegionProgress>();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<string>();
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<object>()}};
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<object>(),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);
}
}