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[] 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(()=>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()}); 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(); } }