using System.IO.Pipes; using System.Text.Json; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class GameRoutePlotOptionsTests { private const long Destination=9007199254740995; private static readonly NavWaypoint Target=new(Destination,"Destination",new(30,0,0),"K"); private static readonly NavWaypoint[] Route=[new(1,"Origin",new(0,0,0),"F"),Target]; private static PlayerState Player(int capacity=256)=>new(){InGame=true,CargoCapacity=capacity,Navigation=new(){CurrentSystemAddress=1,Route=Route}}; private class LegacyDriver:IGameRoutePlanningDriver { public bool SupportsGameRoutePlanning=>true; public int LegacyRequests,Reads; public Task RequestGameRouteAsync(NavWaypoint target,CancellationToken token) {token.ThrowIfCancellationRequested();Check.Equal(Target,target);LegacyRequests++;return Task.FromResult(77L);} public Task ReadGameRouteStatusAsync(long generation,long address,CancellationToken token) { token.ThrowIfCancellationRequested();Check.Equal(77L,generation);Check.Equal(Destination,address);Reads++; return Task.FromResult(new GameRouteStatus(77,true,false,false,Destination,Destination,true,true,Route.Length,GameRouteFingerprint.Compute(Route))); } } private sealed class OptionsDriver:LegacyDriver,IGameRouteOptionsPlanningDriver { public bool SupportsGameRouteOptions {get;set;}=true; public bool SupportsRouteStarClassFilter {get;set;} public bool SupportsRouteMapFilters {get;set;} public int OptionsRequests; public GameRoutePlotOptions? Submitted; public Task RequestGameRouteAsync(NavWaypoint target,GameRoutePlotOptions options,CancellationToken token) {token.ThrowIfCancellationRequested();Check.Equal(Target,target);OptionsRequests++;Submitted=options;return Task.FromResult(77L);} } private static async Task Rejects(Func action) where T:Exception {try{await action();}catch(T){return;}throw new InvalidOperationException("Expected "+typeof(T).Name);} [Test] public async Task NullOrEmptyPreserveUsesTheLegacyPathEvenWithoutAnOptionsCapability() { foreach(var driver in new LegacyDriver[]{new(),new OptionsDriver{SupportsGameRouteOptions=false}}) { var planner=new GameRoutePlanner(()=>Player()); foreach(var options in new GameRoutePlotOptions?[]{null,new()}) { var result=await planner.PlanAsync(Target,driver,options:options); Check.That(ReferenceEquals(Route,result.Route));Check.Equal(Target,result.Next); } Check.Equal(2,driver.LegacyRequests);Check.Equal(2,driver.Reads); if(driver is OptionsDriver optional)Check.Equal(0,optional.OptionsRequests); } Check.That(!new GameRoutePlotOptions().HasOverrides); Check.That(new GameRoutePlotOptions{UseJetConeBoost=false}.HasOverrides); Check.That(new GameRoutePlotOptions{VirtualCargoMass=0}.HasOverrides); } [Test] public async Task ExplicitOptionsReachTheOptionalDriverWithoutLosingFalseZeroOrPreservedFields() { var driver=new OptionsDriver();var planner=new GameRoutePlanner(()=>Player()); foreach(var options in new[]{new GameRoutePlotOptions{Mode=GameRoutePlotMode.Economical}, new(){UseJetConeBoost=false},new(){VirtualCargoMass=0}, new(){Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=0,UseJetConeBoost=true,VirtualCargoMass=42.5}}) { var result=await planner.PlanAsync(Target,driver,options:options); Check.That(ReferenceEquals(options,driver.Submitted));Check.Equal(Destination,result.Next.SystemAddress); } Check.Equal(4,driver.OptionsRequests);Check.Equal(0,driver.LegacyRequests); } [Test] public async Task UnsupportedOrInvalidOverridesAreRejectedBeforeRequestingAnyRoute() { foreach(var driver in new LegacyDriver[]{new(),new OptionsDriver{SupportsGameRouteOptions=false}}) { await Rejects(()=>new GameRoutePlanner(()=>Player()).PlanAsync(Target,driver,options:new(){UseJetConeBoost=false})); Check.Equal(0,driver.LegacyRequests);Check.Equal(0,driver.Reads); if(driver is OptionsDriver optional)Check.Equal(0,optional.OptionsRequests); } var capable=new OptionsDriver(); await Rejects(()=>new GameRoutePlanner(()=>Player(16)).PlanAsync(Target,capable,options:new(){VirtualCargoMass=16.01})); await Rejects(()=>new GameRoutePlanner(()=>Player()).PlanAsync(Target,capable,options:new(){VirtualCargoMass=double.NaN})); Check.Equal(0,capable.LegacyRequests);Check.Equal(0,capable.OptionsRequests);Check.Equal(0,capable.Reads); } [Test] public void ValidationCoversEnumsBoostModeFiniteCargoAndConfirmedCapacityBoundaries() { foreach(var mode in Enum.GetValues())new GameRoutePlotOptions{Mode=mode}.Validate(); foreach(var level in new[]{0,1,2,3})new GameRoutePlotOptions{Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=level}.Validate(); Check.Throws(()=>new GameRoutePlotOptions{Mode=(GameRoutePlotMode)(-1)}.Validate()); Check.Throws(()=>new GameRoutePlotOptions{Mode=(GameRoutePlotMode)3}.Validate()); foreach(var level in new[]{-1,4})Check.Throws(()=>new GameRoutePlotOptions{Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=level}.Validate()); foreach(var mode in new GameRoutePlotMode?[]{null,GameRoutePlotMode.Economical,GameRoutePlotMode.Fastest}) Check.Throws(()=>new GameRoutePlotOptions{Mode=mode,FsdBoostLevel=1}.Validate()); foreach(var mass in new[]{double.NaN,double.PositiveInfinity,double.NegativeInfinity,-.01,2048.01}) Check.Throws(()=>new GameRoutePlotOptions{VirtualCargoMass=mass}.Validate()); new GameRoutePlotOptions{VirtualCargoMass=2048}.Validate(); new GameRoutePlotOptions{VirtualCargoMass=256}.Validate(256); new GameRoutePlotOptions{VirtualCargoMass=0}.Validate(0); new GameRoutePlotOptions{VirtualCargoMass=0}.Validate(-1); Check.Throws(()=>new GameRoutePlotOptions{VirtualCargoMass=.01}.Validate(0)); Check.Throws(()=>new GameRoutePlotOptions{VirtualCargoMass=256.01}.Validate(256)); } private static object Hello(int id,string hash,bool? options,bool? starFilter=null,bool? mapFilters=null) { var result=new Dictionary { ["schema"]=1,["name"]="XFEEDToolBox.Navigation",["processId"]=id,["gameSha256"]=hash, ["verified"]=true,["continuousPose"]=true,["continuousHeat"]=true,["starEnvelopes"]=true, ["nativeSteering"]=true,["chargeAndCommitInterlock"]=true,["gameRoutePlanning"]=true, ["evidence"]="Isolated owned named-pipe route-options fixture" }; if(options is {} supported)result["gameRouteOptions"]=supported; if(starFilter is{} starSupported)result["routeStarClassFilter"]=starSupported; if(mapFilters is{} mapSupported)result["routeMapFilters"]=mapSupported; return new{ok=true,result}; } private static async Task Serve(string name,object response,List requests,CancellationToken token) { await using var pipe=new NamedPipeServerStream(name,PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous); await pipe.WaitForConnectionAsync(token); var bytes=new byte[4096];var count=0; while(true) { var read=await pipe.ReadAsync(bytes.AsMemory(count),token);if(read==0)throw new EndOfStreamException();count+=read; try{using var document=JsonDocument.Parse(bytes.AsMemory(0,count));requests.Add(document.RootElement.Clone());break;} catch(JsonException)when(count(); var options=new[]{new GameRoutePlotOptions{Mode=GameRoutePlotMode.Economical}, new(){Mode=GameRoutePlotMode.Fastest,UseJetConeBoost=false,VirtualCargoMass=42.5}, new(){Mode=GameRoutePlotMode.FsdBoost,UseJetConeBoost=true,FsdBoostLevel=3,VirtualCargoMass=0}, new(){UseJetConeBoost=false}}; var server=Task.Run(async()=> { await Serve(name,Hello(id,hash,true),requests,stop.Token); for(var i=0;iPlayer()); Check.That((await driver.PreflightAsync(stop.Token)).Ready&&driver.SupportsGameRouteOptions); await Rejects(()=>driver.RequestGameRouteAsync(Target,new(){VirtualCargoMass=257},stop.Token)); await Rejects(()=>driver.RequestGameRouteAsync(Target,new(){Mode=(GameRoutePlotMode)3},stop.Token)); await Rejects(()=>driver.RequestGameRouteAsync(Target,new(){VirtualCargoMass=double.NaN},stop.Token)); await Rejects(()=>driver.RequestGameRouteAsync(Target,new(){Mode=GameRoutePlotMode.Fastest,FsdBoostLevel=1},stop.Token)); for(var i=0;i(); var server=Task.Run(async()=> { // The old describe response entirely omits this capability. await Serve(name,Hello(id,hash,null),requests,stop.Token); await Serve(name,new{ok=true,result=new{generation=91L}},requests,stop.Token); }); var driver=new NativeFlightControlDriver(id,hash,()=>Player(8));Check.That((await driver.PreflightAsync(stop.Token)).Ready); Check.That(!driver.SupportsGameRouteOptions); await Rejects(()=>driver.RequestGameRouteAsync(Target,new(){UseJetConeBoost=false},stop.Token)); Check.Equal(91L,await driver.RequestGameRouteAsync(Target,stop.Token));await server; Check.Equal(2,requests.Count);Check.That(!requests[1].TryGetProperty("routeOptions",out _)); } [Test] public async Task StarClassRouteFilterRequiresItsOwnCapabilityBeforeAnyPlannerRequest() { var driver=new OptionsDriver();var options=new GameRoutePlotOptions{StarClassFilter=GameRouteStarClassFilter.Scoopable}; Check.That(options.HasOverrides); await Rejects(()=>new GameRoutePlanner(()=>Player()).PlanAsync(Target,driver,options:options)); Check.Equal(0,driver.OptionsRequests);Check.Equal(0,driver.Reads); driver.SupportsRouteStarClassFilter=true; await new GameRoutePlanner(()=>Player()).PlanAsync(Target,driver,options:options); Check.Equal(0x807Fu,driver.Submitted!.StarClassFilter!.OriginalMask);Check.Equal(1,driver.OptionsRequests); } [Test] public async Task NativeStarClassMaskPreservesExplicitEmptyAndDisallowsOldOptionsOnlyPlugins() { foreach(var supported in new[]{false,true}) { var id=Random.Shared.Next(93000000,93999999);var hash=new string('c',64);var name=$"XFEEDToolBox.Navigation.{id}"; using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List(); var server=Task.Run(async()=> { await Serve(name,Hello(id,hash,true,supported?true:null),requests,stop.Token); if(supported)for(var i=0;i<2;i++)await Serve(name,new{ok=true,result=new{generation=101L+i}},requests,stop.Token); }); var driver=new NativeFlightControlDriver(id,hash,()=>Player());Check.That((await driver.PreflightAsync(stop.Token)).Ready); Check.Equal(supported,driver.SupportsRouteStarClassFilter); var empty=new GameRoutePlotOptions{StarClassFilter=GameRouteStarClassFilter.FromOriginalMask(0)}; if(!supported)await Rejects(()=>driver.RequestGameRouteAsync(Target,empty,stop.Token)); else { await driver.RequestGameRouteAsync(Target,empty,stop.Token); await driver.RequestGameRouteAsync(Target,new(){StarClassFilter=GameRouteStarClassFilter.Scoopable},stop.Token); } await server;Check.Equal(supported?3:1,requests.Count); if(supported) { Check.Equal(0u,requests[1].GetProperty("routeOptions").GetProperty("starClassMask").GetUInt32()); Check.Equal(0x807Fu,requests[2].GetProperty("routeOptions").GetProperty("starClassMask").GetUInt32()); Check.Equal(Destination,requests[2].GetProperty("destinationSystemAddress").GetInt64()); } } } [Test] public async Task GenericRouteMapFilterRequiresAnIndependentPlannerCapability() { var driver=new OptionsDriver{SupportsRouteStarClassFilter=true}; var options=new GameRoutePlotOptions{UseJetConeBoost=false,VirtualCargoMass=0, MapFilter=new(){CategoryId=7,ExpectedKeys=["anarchy","lawless","lowsecurity"],SelectedKeys=[],AffectsRoutePlotting=false}}; await Rejects(()=>new GameRoutePlanner(()=>Player()).PlanAsync(Target,driver,options:options)); Check.Equal(0,driver.OptionsRequests);Check.Equal(0,driver.LegacyRequests);Check.Equal(0,driver.Reads); driver.SupportsRouteMapFilters=true; await new GameRoutePlanner(()=>Player()).PlanAsync(Target,driver,options:options); Check.That(ReferenceEquals(options,driver.Submitted)&&ReferenceEquals(options.MapFilter,driver.Submitted!.MapFilter)); Check.That(driver.Submitted!.MapFilter is{SelectedKeys.Count:0,AffectsRoutePlotting:false});Check.Equal(1,driver.OptionsRequests); } [Test] public async Task MissingOrFalseMapCapabilityRejectsLocallyWithoutDisablingStarOrLegacyRoutes() { foreach(var capability in new bool?[]{null,false}) { var id=Random.Shared.Next(94000000,94999999);var hash=new string('d',64);var name=$"XFEEDToolBox.Navigation.{id}"; using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List(); var server=Task.Run(async()=> { await Serve(name,Hello(id,hash,true,true,capability),requests,stop.Token); await Serve(name,new{ok=true,result=new{generation=111L}},requests,stop.Token); await Serve(name,new{ok=true,result=new{generation=112L}},requests,stop.Token); }); var driver=new NativeFlightControlDriver(id,hash,()=>Player());Check.That((await driver.PreflightAsync(stop.Token)).Ready); Check.That(driver.SupportsGameRouteOptions&&driver.SupportsRouteStarClassFilter&&!driver.SupportsRouteMapFilters); await Rejects(()=>driver.RequestGameRouteAsync(Target,new(){UseJetConeBoost=false,VirtualCargoMass=0, MapFilter=new(){CategoryId=8,ExpectedKeys=["notvisited","visited"],SelectedKeys=[],AffectsRoutePlotting=false}},stop.Token)); Check.Equal(111L,await driver.RequestGameRouteAsync(Target,new(){StarClassFilter=GameRouteStarClassFilter.Scoopable},stop.Token)); Check.Equal(112L,await driver.RequestGameRouteAsync(Target,stop.Token));await server; Check.Equal(3,requests.Count);Check.Equal(0x807Fu,requests[1].GetProperty("routeOptions").GetProperty("starClassMask").GetUInt32()); Check.Equal(JsonValueKind.Null,requests[1].GetProperty("routeOptions").GetProperty("mapFilter").ValueKind); Check.That(!requests[2].TryGetProperty("routeOptions",out _)); } } [Test] public async Task NativeMapWirePreservesExactNamesExplicitEmptyFalseAndEveryOtherOverride() { var id=Random.Shared.Next(95000000,95999999);var hash=new string('e',64);var name=$"XFEEDToolBox.Navigation.{id}"; using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List(); var options=new[]{new GameRoutePlotOptions{Mode=GameRoutePlotMode.Fastest,UseJetConeBoost=false,VirtualCargoMass=12.5, MapFilter=new(){CategoryId=7,ExpectedKeys=["lowsecurity","lawless","anarchy"],SelectedKeys=["lawless"],AffectsRoutePlotting=false}}, new(){Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=0,UseJetConeBoost=false,VirtualCargoMass=0,StarClassFilter=GameRouteStarClassFilter.Scoopable, MapFilter=new(){CategoryId=3,ExpectedKeys=["democracy","anarchy","corporate"],SelectedKeys=[],AffectsRoutePlotting=false}}}; var server=Task.Run(async()=> { await Serve(name,Hello(id,hash,true,true,true),requests,stop.Token); for(var i=0;iPlayer());Check.That((await driver.PreflightAsync(stop.Token)).Ready&&driver.SupportsRouteMapFilters); for(var i=0;ix.GetString()).SequenceEqual(expected.MapFilter.ExpectedKeys)); Check.That(map.GetProperty("selectedKeys").EnumerateArray().Select(x=>x.GetString()).SequenceEqual(expected.MapFilter.SelectedKeys)); Check.That(!map.GetProperty("affectsRoutePlotting").GetBoolean()); Check.Equal((int)expected.Mode!.Value,actual.GetProperty("mode").GetInt32());Check.That(!actual.GetProperty("useJetConeBoost").GetBoolean()); Check.Equal(expected.VirtualCargoMass!.Value,actual.GetProperty("virtualCargoMass").GetDouble()); if(expected.FsdBoostLevel is{} level)Check.Equal(level,actual.GetProperty("fsdBoostLevel").GetInt32()); else Check.Equal(JsonValueKind.Null,actual.GetProperty("fsdBoostLevel").ValueKind); if(expected.StarClassFilter is{} stars)Check.Equal(stars.OriginalMask,actual.GetProperty("starClassMask").GetUInt32()); else Check.Equal(JsonValueKind.Null,actual.GetProperty("starClassMask").ValueKind); } Check.Equal("lawless",requests[1].GetProperty("routeOptions").GetProperty("mapFilter").GetProperty("selectedKeys")[0].GetString()); Check.Equal(0,requests[2].GetProperty("routeOptions").GetProperty("mapFilter").GetProperty("selectedKeys").GetArrayLength()); } [Test] public async Task GenericMapCapabilityDoesNotRequireOrImplyTheStarClassCapability() { var id=Random.Shared.Next(96000000,96999999);var hash=new string('f',64);var name=$"XFEEDToolBox.Navigation.{id}"; using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List(); var server=Task.Run(async()=> { await Serve(name,Hello(id,hash,true,null,true),requests,stop.Token); await Serve(name,new{ok=true,result=new{generation=131L}},requests,stop.Token); }); var driver=new NativeFlightControlDriver(id,hash,()=>Player());Check.That((await driver.PreflightAsync(stop.Token)).Ready); Check.That(driver.SupportsRouteMapFilters&&!driver.SupportsRouteStarClassFilter); await Rejects(()=>driver.RequestGameRouteAsync(Target,new(){StarClassFilter=GameRouteStarClassFilter.Scoopable},stop.Token)); Check.Equal(131L,await driver.RequestGameRouteAsync(Target,new(){MapFilter=new(){CategoryId=8,ExpectedKeys=["visited","notvisited"],SelectedKeys=[],AffectsRoutePlotting=false}},stop.Token)); await server;Check.Equal(2,requests.Count);Check.Equal(JsonValueKind.Null,requests[1].GetProperty("routeOptions").GetProperty("starClassMask").ValueKind); Check.Equal(0,requests[1].GetProperty("routeOptions").GetProperty("mapFilter").GetProperty("selectedKeys").GetArrayLength()); } }