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<long> RequestGameRouteAsync(NavWaypoint target,CancellationToken token)
{token.ThrowIfCancellationRequested();Check.Equal(Target,target);LegacyRequests++;return Task.FromResult(77L);}
public Task<GameRouteStatus> 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<long> 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<T>(Func<Task> 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<InvalidOperationException>(()=>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<ArgumentOutOfRangeException>(()=>new GameRoutePlanner(()=>Player(16)).PlanAsync(Target,capable,options:new(){VirtualCargoMass=16.01}));
await Rejects<ArgumentOutOfRangeException>(()=>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<GameRoutePlotMode>())new GameRoutePlotOptions{Mode=mode}.Validate();
foreach(var level in new[]{0,1,2,3})new GameRoutePlotOptions{Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=level}.Validate();
Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{Mode=(GameRoutePlotMode)(-1)}.Validate());
Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{Mode=(GameRoutePlotMode)3}.Validate());
foreach(var level in new[]{-1,4})Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=level}.Validate());
foreach(var mode in new GameRoutePlotMode?[]{null,GameRoutePlotMode.Economical,GameRoutePlotMode.Fastest})
Check.Throws<ArgumentException>(()=>new GameRoutePlotOptions{Mode=mode,FsdBoostLevel=1}.Validate());
foreach(var mass in new[]{double.NaN,double.PositiveInfinity,double.NegativeInfinity,-.01,2048.01})
Check.Throws<ArgumentOutOfRangeException>(()=>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<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{VirtualCargoMass=.01}.Validate(0));
Check.Throws<ArgumentOutOfRangeException>(()=>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<string,object?>
{
["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<JsonElement> 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<bytes.Length){ }
}
await pipe.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(response,ToolboxSettings.JsonOptions),token);
await pipe.WriteAsync(new byte[]{(byte)'\n'},token);await pipe.FlushAsync(token);
}
[Test] public async Task NativePipeUsesNumericModesNullableFieldsAndTheSameExactDestinationAndOwner()
{
var id=Random.Shared.Next(91000000,91999999);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));var requests=new List<JsonElement>();
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;i<options.Length+1;i++)await Serve(name,new{ok=true,result=new{generation=81L+i}},requests,stop.Token);
});
var driver=new NativeFlightControlDriver(id,hash,()=>Player());
Check.That((await driver.PreflightAsync(stop.Token)).Ready&&driver.SupportsGameRouteOptions);
await Rejects<ArgumentOutOfRangeException>(()=>driver.RequestGameRouteAsync(Target,new(){VirtualCargoMass=257},stop.Token));
await Rejects<ArgumentOutOfRangeException>(()=>driver.RequestGameRouteAsync(Target,new(){Mode=(GameRoutePlotMode)3},stop.Token));
await Rejects<ArgumentOutOfRangeException>(()=>driver.RequestGameRouteAsync(Target,new(){VirtualCargoMass=double.NaN},stop.Token));
await Rejects<ArgumentException>(()=>driver.RequestGameRouteAsync(Target,new(){Mode=GameRoutePlotMode.Fastest,FsdBoostLevel=1},stop.Token));
for(var i=0;i<options.Length;i++)Check.Equal(81L+i,await driver.RequestGameRouteAsync(Target,options[i],stop.Token));
Check.Equal(85L,await driver.RequestGameRouteAsync(Target,stop.Token));await server;
Check.Equal("describe",requests[0].GetProperty("op").GetString());
for(var i=0;i<options.Length;i++)
{
var request=requests[i+1];Check.Equal("route.plan",request.GetProperty("op").GetString());
Check.Equal(Destination,request.GetProperty("destinationSystemAddress").GetInt64());
Check.Equal(requests[1].GetProperty("owner").GetString(),request.GetProperty("owner").GetString());
var actual=request.GetProperty("routeOptions");var expected=options[i];
Check.That(actual.GetProperty("mode").ValueKind==(expected.Mode is null?JsonValueKind.Null:JsonValueKind.Number));
if(expected.Mode is {} mode)Check.Equal((int)mode,actual.GetProperty("mode").GetInt32());
if(expected.UseJetConeBoost is {} boost)Check.Equal(boost,actual.GetProperty("useJetConeBoost").GetBoolean());
else Check.Equal(JsonValueKind.Null,actual.GetProperty("useJetConeBoost").ValueKind);
if(expected.FsdBoostLevel is {} level)Check.Equal(level,actual.GetProperty("fsdBoostLevel").GetInt32());
else Check.Equal(JsonValueKind.Null,actual.GetProperty("fsdBoostLevel").ValueKind);
if(expected.VirtualCargoMass is {} mass)Check.Equal(mass,actual.GetProperty("virtualCargoMass").GetDouble());
else Check.Equal(JsonValueKind.Null,actual.GetProperty("virtualCargoMass").ValueKind);
}
Check.That(!requests[^1].TryGetProperty("routeOptions",out _));
Check.Equal(requests[1].GetProperty("owner").GetString(),requests[^1].GetProperty("owner").GetString());
}
[Test] public async Task MissingNativeCapabilityRejectsOverridesLocallyAndStillAllowsTheLegacyRequest()
{
var id=Random.Shared.Next(92000000,92999999);var hash=new string('b',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<JsonElement>();
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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<JsonElement>();
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<InvalidOperationException>(()=>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<JsonElement>();
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;i<options.Length;i++)await Serve(name,new{ok=true,result=new{generation=121L+i}},requests,stop.Token);
});
var driver=new NativeFlightControlDriver(id,hash,()=>Player());Check.That((await driver.PreflightAsync(stop.Token)).Ready&&driver.SupportsRouteMapFilters);
for(var i=0;i<options.Length;i++)Check.Equal(121L+i,await driver.RequestGameRouteAsync(Target,options[i],stop.Token));await server;
Check.Equal(3,requests.Count);
for(var i=0;i<options.Length;i++)
{
var request=requests[i+1];var actual=request.GetProperty("routeOptions");var map=actual.GetProperty("mapFilter");var expected=options[i];
Check.Equal("route.plan",request.GetProperty("op").GetString());Check.Equal(Destination,request.GetProperty("destinationSystemAddress").GetInt64());
Check.Equal(requests[1].GetProperty("owner").GetString(),request.GetProperty("owner").GetString());
Check.Equal(expected.MapFilter!.CategoryId,map.GetProperty("categoryId").GetInt32());
Check.That(map.GetProperty("expectedKeys").EnumerateArray().Select(x=>x.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<JsonElement>();
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<InvalidOperationException>(()=>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());
}
}
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<long> RequestGameRouteAsync(NavWaypoint target,CancellationToken token)
{token.ThrowIfCancellationRequested();Check.Equal(Target,target);LegacyRequests++;return Task.FromResult(77L);}
public Task<GameRouteStatus> 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<long> 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<T>(Func<Task> 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<InvalidOperationException>(()=>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<ArgumentOutOfRangeException>(()=>new GameRoutePlanner(()=>Player(16)).PlanAsync(Target,capable,options:new(){VirtualCargoMass=16.01}));
await Rejects<ArgumentOutOfRangeException>(()=>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<GameRoutePlotMode>())new GameRoutePlotOptions{Mode=mode}.Validate();
foreach(var level in new[]{0,1,2,3})new GameRoutePlotOptions{Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=level}.Validate();
Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{Mode=(GameRoutePlotMode)(-1)}.Validate());
Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{Mode=(GameRoutePlotMode)3}.Validate());
foreach(var level in new[]{-1,4})Check.Throws<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{Mode=GameRoutePlotMode.FsdBoost,FsdBoostLevel=level}.Validate());
foreach(var mode in new GameRoutePlotMode?[]{null,GameRoutePlotMode.Economical,GameRoutePlotMode.Fastest})
Check.Throws<ArgumentException>(()=>new GameRoutePlotOptions{Mode=mode,FsdBoostLevel=1}.Validate());
foreach(var mass in new[]{double.NaN,double.PositiveInfinity,double.NegativeInfinity,-.01,2048.01})
Check.Throws<ArgumentOutOfRangeException>(()=>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<ArgumentOutOfRangeException>(()=>new GameRoutePlotOptions{VirtualCargoMass=.01}.Validate(0));
Check.Throws<ArgumentOutOfRangeException>(()=>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<string,object?>
{
["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<JsonElement> 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<bytes.Length){ }
}
await pipe.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(response,ToolboxSettings.JsonOptions),token);
await pipe.WriteAsync(new byte[]{(byte)'\n'},token);await pipe.FlushAsync(token);
}
[Test] public async Task NativePipeUsesNumericModesNullableFieldsAndTheSameExactDestinationAndOwner()
{
var id=Random.Shared.Next(91000000,91999999);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(8));var requests=new List<JsonElement>();
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;i<options.Length+1;i++)await Serve(name,new{ok=true,result=new{generation=81L+i}},requests,stop.Token);
});
var driver=new NativeFlightControlDriver(id,hash,()=>Player());
Check.That((await driver.PreflightAsync(stop.Token)).Ready&&driver.SupportsGameRouteOptions);
await Rejects<ArgumentOutOfRangeException>(()=>driver.RequestGameRouteAsync(Target,new(){VirtualCargoMass=257},stop.Token));
await Rejects<ArgumentOutOfRangeException>(()=>driver.RequestGameRouteAsync(Target,new(){Mode=(GameRoutePlotMode)3},stop.Token));
await Rejects<ArgumentOutOfRangeException>(()=>driver.RequestGameRouteAsync(Target,new(){VirtualCargoMass=double.NaN},stop.Token));
await Rejects<ArgumentException>(()=>driver.RequestGameRouteAsync(Target,new(){Mode=GameRoutePlotMode.Fastest,FsdBoostLevel=1},stop.Token));
for(var i=0;i<options.Length;i++)Check.Equal(81L+i,await driver.RequestGameRouteAsync(Target,options[i],stop.Token));
Check.Equal(85L,await driver.RequestGameRouteAsync(Target,stop.Token));await server;
Check.Equal("describe",requests[0].GetProperty("op").GetString());
for(var i=0;i<options.Length;i++)
{
var request=requests[i+1];Check.Equal("route.plan",request.GetProperty("op").GetString());
Check.Equal(Destination,request.GetProperty("destinationSystemAddress").GetInt64());
Check.Equal(requests[1].GetProperty("owner").GetString(),request.GetProperty("owner").GetString());
var actual=request.GetProperty("routeOptions");var expected=options[i];
Check.That(actual.GetProperty("mode").ValueKind==(expected.Mode is null?JsonValueKind.Null:JsonValueKind.Number));
if(expected.Mode is {} mode)Check.Equal((int)mode,actual.GetProperty("mode").GetInt32());
if(expected.UseJetConeBoost is {} boost)Check.Equal(boost,actual.GetProperty("useJetConeBoost").GetBoolean());
else Check.Equal(JsonValueKind.Null,actual.GetProperty("useJetConeBoost").ValueKind);
if(expected.FsdBoostLevel is {} level)Check.Equal(level,actual.GetProperty("fsdBoostLevel").GetInt32());
else Check.Equal(JsonValueKind.Null,actual.GetProperty("fsdBoostLevel").ValueKind);
if(expected.VirtualCargoMass is {} mass)Check.Equal(mass,actual.GetProperty("virtualCargoMass").GetDouble());
else Check.Equal(JsonValueKind.Null,actual.GetProperty("virtualCargoMass").ValueKind);
}
Check.That(!requests[^1].TryGetProperty("routeOptions",out _));
Check.Equal(requests[1].GetProperty("owner").GetString(),requests[^1].GetProperty("owner").GetString());
}
[Test] public async Task MissingNativeCapabilityRejectsOverridesLocallyAndStillAllowsTheLegacyRequest()
{
var id=Random.Shared.Next(92000000,92999999);var hash=new string('b',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var stop=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<JsonElement>();
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<InvalidOperationException>(()=>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<InvalidOperationException>(()=>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<JsonElement>();
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<InvalidOperationException>(()=>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<JsonElement>();
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;i<options.Length;i++)await Serve(name,new{ok=true,result=new{generation=121L+i}},requests,stop.Token);
});
var driver=new NativeFlightControlDriver(id,hash,()=>Player());Check.That((await driver.PreflightAsync(stop.Token)).Ready&&driver.SupportsRouteMapFilters);
for(var i=0;i<options.Length;i++)Check.Equal(121L+i,await driver.RequestGameRouteAsync(Target,options[i],stop.Token));await server;
Check.Equal(3,requests.Count);
for(var i=0;i<options.Length;i++)
{
var request=requests[i+1];var actual=request.GetProperty("routeOptions");var map=actual.GetProperty("mapFilter");var expected=options[i];
Check.Equal("route.plan",request.GetProperty("op").GetString());Check.Equal(Destination,request.GetProperty("destinationSystemAddress").GetInt64());
Check.Equal(requests[1].GetProperty("owner").GetString(),request.GetProperty("owner").GetString());
Check.Equal(expected.MapFilter!.CategoryId,map.GetProperty("categoryId").GetInt32());
Check.That(map.GetProperty("expectedKeys").EnumerateArray().Select(x=>x.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<JsonElement>();
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<InvalidOperationException>(()=>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());
}
}