using System.IO.Pipes;
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Rendering;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class NativeGalaxyDustProfileTests
{
private static JsonObject Profile()=>JsonNode.Parse("""
{"origin":[1,2,3],"queryCentre":[4,5,6],"surfaceDensity":0.4,"qualityStarCount":0,"runtimeRadiusMultiplier":1,
"sparseExpansion":4,"smallMode":false,"originalViewMode":0,"debugEnabled":false,"referenceKind":"camera-eye"}
""")!.AsObject();
private static JsonObject Dust()=>JsonNode.Parse("""
{"scope":"observed-game-dust-producer-inputs","step":1066,"height":1280,"layerAlphaSize":[1,2,3,4,5,6],
"alphaMultiplier":0.5,"sizeMultiplier":32,"transitionPerStep":100,"minimumDistance":200,"runtimeTint":null,
"observedGamePublications":[{"ordinal":0,"publishedRevision":10,"requestedRevision":11,"radius":10000,"records":7},
{"ordinal":1,"publishedRevision":0,"requestedRevision":22,"radius":null,"records":0},
{"ordinal":2,"publishedRevision":30,"requestedRevision":30,"radius":1000,"records":9}]}
""")!.AsObject();
private static GalaxyCatalogueProfile Read(JsonObject value)=>NativeGalaxyCatalog.ReadCatalogueProfile(JsonSerializer.SerializeToElement(value));
private static GalaxyCatalogueProfile WithDust(JsonNode? dust)
{var value=Profile();value["dustInputs"]=dust;return Read(value);}
[Test] public void OldOrUnavailableDustKeepsItsOwnUnknownStateWithoutBlockingLod()
{
var old=Read(Profile());var unknown=WithDust(null);
Check.That(old.HasLodInputs&&unknown.HasLodInputs&&old.DustInputs is null&&unknown.DustInputs is null);
var catalogue=new NativeGalaxyCatalog(123);
catalogue.ApplyProtocolDescription(JsonSerializer.SerializeToElement(new{processId=123,galaxyCatalogueProfile=true}));
Check.That(catalogue.SupportsCatalogueProfile&&!catalogue.SupportsDustInputs);
catalogue.ApplyProtocolDescription(JsonSerializer.SerializeToElement(new{processId=123,galaxyCatalogueProfile=true,dustInputs=true}));
Check.That(catalogue.SupportsDustInputs);
}
[Test] public void ActualParametersPreserveInterleavedLayerFactorsAndNeverBecomeToolCacheReady()
{
var profile=WithDust(Dust());var dust=profile.DustInputs!;
Check.That(profile.HasLodInputs&&!dust.ToolCacheReady&&dust.RuntimeTint is null);
Check.Equal(1066u,dust.Settings.Step);Check.Equal(1280u,dust.Settings.Height);
Check.That(dust.Settings.Layers.SequenceEqual(new[]{new GalaxyOriginalDustLayerFactors(1,2),new(3,4),new(5,6)}));
Check.Equal(.5f,dust.Settings.AlphaMultiplier);Check.Equal(32f,dust.Settings.SizeMultiplier);
Check.Equal(100f,dust.Settings.TransitionPerStep);Check.Equal(200f,dust.Settings.MinimumDistance);
Check.That(dust.ObservedGamePublications[1] is{PublishedRevision:0,RequestedRevision:22,Radius:null,RecordCount:0});
Check.That(dust.ObservedGamePublications[2] is{PublishedRevision:30,RequestedRevision:30,Radius:1000,RecordCount:9});
Check.That(!GalaxyOriginalDustFrame.TryCreate(dust.Settings,null,0,out _));
var zero=Dust();zero["alphaMultiplier"]=0;zero["sizeMultiplier"]=0;zero["step"]=uint.MaxValue;
var explicitZero=WithDust(zero).DustInputs!;
Check.Equal(0f,explicitZero.Settings.AlphaMultiplier);Check.Equal(0f,explicitZero.Settings.SizeMultiplier);Check.Equal(uint.MaxValue,explicitZero.Settings.Step);
}
[Test] public void MalformedOrOverclaimedDustInputsAreRejectedRatherThanDefaulted()
{
Action<JsonObject>[] invalid=[
d=>d["scope"]="tool-cache-ready",d=>d["step"]=0,d=>d["height"]=-1,d=>d.Remove("runtimeTint"),d=>d["runtimeTint"]=new JsonArray(0,0,0),
d=>d["layerAlphaSize"]=new JsonArray(1,2,3),d=>d["layerAlphaSize"]![1]=-1,d=>d["layerAlphaSize"]![2]=JsonNode.Parse("1e39"),
d=>d["alphaMultiplier"]="0.5",d=>d["transitionPerStep"]=-1,d=>d["minimumDistance"]=null,
d=>d["observedGamePublications"]=new JsonArray(),d=>d["observedGamePublications"]![1]!["ordinal"]=0,
d=>d["observedGamePublications"]![0]!["records"]=1_048_577,d=>d["observedGamePublications"]![1]!["radius"]=0,
d=>d["observedGamePublications"]![2]!["radius"]=null,d=>d["observedGamePublications"]![0]!["requestedRevision"]=-1
];
foreach(var change in invalid){var dust=Dust();change(dust);Check.Throws<InvalidDataException>(()=>WithDust(dust));}
}
[Test] public async Task FirstLodPageCarriesOptionalDustWithoutWaitingForPointTableCompletion()
{
var process=Random.Shared.Next(190000000,195000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var profile=Profile();profile["dustInputs"]=Dust();
profile["pointOverrides"]=JsonSerializer.SerializeToNode(new{scope="published-renderer-d50+local-navroute",processId=process,sourceGeneration=9,
capturedAtTick=1UL,navRouteViewMode=0,d50Complete=true,navRouteComplete=true,overrideOffset=0,overrideTotal=2,navRouteOffset=0,navRouteTotal=0,
overrides=new[]{new{systemAddress="1",minimumExtra=1f,farFade=false,pending=false,position=new[]{1d,2d,3d},name=(string?)null,starClass="G",originalAppearance=(object?)null}},
navRouteMembers=Array.Empty<string>()});
var server=Task.Run(async()=>
{
await using var pipe=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{process}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
await pipe.WaitForConnectionAsync(stop.Token);var bytes=new byte[4096];var count=0;JsonElement? request=null;
while(request is null)
{
var read=await pipe.ReadAsync(bytes.AsMemory(count),stop.Token);if(read==0)throw new IOException("Empty request");
count+=read;request=ParseRequest(bytes,count);if(count==bytes.Length&&request is null)throw new IOException("Oversized request");
}
Check.Equal("galaxy.catalogue.profile",request.Value.GetProperty("op").GetString());
var result=JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result=new{generation=9,done=true,error="",profile}});
await pipe.WriteAsync(result,stop.Token);await pipe.WriteAsync(new byte[]{10},stop.Token);await pipe.FlushAsync(stop.Token);
if(OperatingSystem.IsWindows())pipe.WaitForPipeDrain();
});
var value=await new NativeGalaxyCatalog(process).CatalogueProfileFirstPageAsync(new(0,0,0),new(0,0,0),stop.Token);await server;
Check.That(value.HasLodInputs&&value.PointOverrides is{D50Complete:false}&&value.DustInputs is{ToolCacheReady:false,RuntimeTint:null});
Check.Equal(1280u,value.DustInputs!.Settings.Height);
}
private static JsonElement? ParseRequest(byte[] data,int count)
{
var reader=new Utf8JsonReader(data.AsSpan(0,count),false,default);
if(!JsonDocument.TryParseValue(ref reader,out var doc))return null;
using(doc)return doc.RootElement.Clone();
}
}
using System.IO.Pipes;
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Rendering;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public sealed class NativeGalaxyDustProfileTests
{
private static JsonObject Profile()=>JsonNode.Parse("""
{"origin":[1,2,3],"queryCentre":[4,5,6],"surfaceDensity":0.4,"qualityStarCount":0,"runtimeRadiusMultiplier":1,
"sparseExpansion":4,"smallMode":false,"originalViewMode":0,"debugEnabled":false,"referenceKind":"camera-eye"}
""")!.AsObject();
private static JsonObject Dust()=>JsonNode.Parse("""
{"scope":"observed-game-dust-producer-inputs","step":1066,"height":1280,"layerAlphaSize":[1,2,3,4,5,6],
"alphaMultiplier":0.5,"sizeMultiplier":32,"transitionPerStep":100,"minimumDistance":200,"runtimeTint":null,
"observedGamePublications":[{"ordinal":0,"publishedRevision":10,"requestedRevision":11,"radius":10000,"records":7},
{"ordinal":1,"publishedRevision":0,"requestedRevision":22,"radius":null,"records":0},
{"ordinal":2,"publishedRevision":30,"requestedRevision":30,"radius":1000,"records":9}]}
""")!.AsObject();
private static GalaxyCatalogueProfile Read(JsonObject value)=>NativeGalaxyCatalog.ReadCatalogueProfile(JsonSerializer.SerializeToElement(value));
private static GalaxyCatalogueProfile WithDust(JsonNode? dust)
{var value=Profile();value["dustInputs"]=dust;return Read(value);}
[Test] public void OldOrUnavailableDustKeepsItsOwnUnknownStateWithoutBlockingLod()
{
var old=Read(Profile());var unknown=WithDust(null);
Check.That(old.HasLodInputs&&unknown.HasLodInputs&&old.DustInputs is null&&unknown.DustInputs is null);
var catalogue=new NativeGalaxyCatalog(123);
catalogue.ApplyProtocolDescription(JsonSerializer.SerializeToElement(new{processId=123,galaxyCatalogueProfile=true}));
Check.That(catalogue.SupportsCatalogueProfile&&!catalogue.SupportsDustInputs);
catalogue.ApplyProtocolDescription(JsonSerializer.SerializeToElement(new{processId=123,galaxyCatalogueProfile=true,dustInputs=true}));
Check.That(catalogue.SupportsDustInputs);
}
[Test] public void ActualParametersPreserveInterleavedLayerFactorsAndNeverBecomeToolCacheReady()
{
var profile=WithDust(Dust());var dust=profile.DustInputs!;
Check.That(profile.HasLodInputs&&!dust.ToolCacheReady&&dust.RuntimeTint is null);
Check.Equal(1066u,dust.Settings.Step);Check.Equal(1280u,dust.Settings.Height);
Check.That(dust.Settings.Layers.SequenceEqual(new[]{new GalaxyOriginalDustLayerFactors(1,2),new(3,4),new(5,6)}));
Check.Equal(.5f,dust.Settings.AlphaMultiplier);Check.Equal(32f,dust.Settings.SizeMultiplier);
Check.Equal(100f,dust.Settings.TransitionPerStep);Check.Equal(200f,dust.Settings.MinimumDistance);
Check.That(dust.ObservedGamePublications[1] is{PublishedRevision:0,RequestedRevision:22,Radius:null,RecordCount:0});
Check.That(dust.ObservedGamePublications[2] is{PublishedRevision:30,RequestedRevision:30,Radius:1000,RecordCount:9});
Check.That(!GalaxyOriginalDustFrame.TryCreate(dust.Settings,null,0,out _));
var zero=Dust();zero["alphaMultiplier"]=0;zero["sizeMultiplier"]=0;zero["step"]=uint.MaxValue;
var explicitZero=WithDust(zero).DustInputs!;
Check.Equal(0f,explicitZero.Settings.AlphaMultiplier);Check.Equal(0f,explicitZero.Settings.SizeMultiplier);Check.Equal(uint.MaxValue,explicitZero.Settings.Step);
}
[Test] public void MalformedOrOverclaimedDustInputsAreRejectedRatherThanDefaulted()
{
Action<JsonObject>[] invalid=[
d=>d["scope"]="tool-cache-ready",d=>d["step"]=0,d=>d["height"]=-1,d=>d.Remove("runtimeTint"),d=>d["runtimeTint"]=new JsonArray(0,0,0),
d=>d["layerAlphaSize"]=new JsonArray(1,2,3),d=>d["layerAlphaSize"]![1]=-1,d=>d["layerAlphaSize"]![2]=JsonNode.Parse("1e39"),
d=>d["alphaMultiplier"]="0.5",d=>d["transitionPerStep"]=-1,d=>d["minimumDistance"]=null,
d=>d["observedGamePublications"]=new JsonArray(),d=>d["observedGamePublications"]![1]!["ordinal"]=0,
d=>d["observedGamePublications"]![0]!["records"]=1_048_577,d=>d["observedGamePublications"]![1]!["radius"]=0,
d=>d["observedGamePublications"]![2]!["radius"]=null,d=>d["observedGamePublications"]![0]!["requestedRevision"]=-1
];
foreach(var change in invalid){var dust=Dust();change(dust);Check.Throws<InvalidDataException>(()=>WithDust(dust));}
}
[Test] public async Task FirstLodPageCarriesOptionalDustWithoutWaitingForPointTableCompletion()
{
var process=Random.Shared.Next(190000000,195000000);using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var profile=Profile();profile["dustInputs"]=Dust();
profile["pointOverrides"]=JsonSerializer.SerializeToNode(new{scope="published-renderer-d50+local-navroute",processId=process,sourceGeneration=9,
capturedAtTick=1UL,navRouteViewMode=0,d50Complete=true,navRouteComplete=true,overrideOffset=0,overrideTotal=2,navRouteOffset=0,navRouteTotal=0,
overrides=new[]{new{systemAddress="1",minimumExtra=1f,farFade=false,pending=false,position=new[]{1d,2d,3d},name=(string?)null,starClass="G",originalAppearance=(object?)null}},
navRouteMembers=Array.Empty<string>()});
var server=Task.Run(async()=>
{
await using var pipe=new NamedPipeServerStream($"XFEEDToolBox.Navigation.{process}",PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
await pipe.WaitForConnectionAsync(stop.Token);var bytes=new byte[4096];var count=0;JsonElement? request=null;
while(request is null)
{
var read=await pipe.ReadAsync(bytes.AsMemory(count),stop.Token);if(read==0)throw new IOException("Empty request");
count+=read;request=ParseRequest(bytes,count);if(count==bytes.Length&&request is null)throw new IOException("Oversized request");
}
Check.Equal("galaxy.catalogue.profile",request.Value.GetProperty("op").GetString());
var result=JsonSerializer.SerializeToUtf8Bytes(new{ok=true,result=new{generation=9,done=true,error="",profile}});
await pipe.WriteAsync(result,stop.Token);await pipe.WriteAsync(new byte[]{10},stop.Token);await pipe.FlushAsync(stop.Token);
if(OperatingSystem.IsWindows())pipe.WaitForPipeDrain();
});
var value=await new NativeGalaxyCatalog(process).CatalogueProfileFirstPageAsync(new(0,0,0),new(0,0,0),stop.Token);await server;
Check.That(value.HasLodInputs&&value.PointOverrides is{D50Complete:false}&&value.DustInputs is{ToolCacheReady:false,RuntimeTint:null});
Check.Equal(1280u,value.DustInputs!.Settings.Height);
}
private static JsonElement? ParseRequest(byte[] data,int count)
{
var reader=new Utf8JsonReader(data.AsSpan(0,count),false,default);
if(!JsonDocument.TryParseValue(ref reader,out var doc))return null;
using(doc)return doc.RootElement.Clone();
}
}