using System.Diagnostics;
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Plugins;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NativeNavigationTests
{
private static async Task Serve(string name,object response,CancellationToken token,Action<string>? request=null)
{
await using var server=new NamedPipeServerStream(name,PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
await server.WaitForConnectionAsync(token);
var buffer=new byte[4096];var count=await server.ReadAsync(buffer,token);
request?.Invoke(Encoding.UTF8.GetString(buffer,0,count));
await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(response,ToolboxSettings.JsonOptions),token);
await server.WriteAsync(new byte[]{(byte)'\n'},token);
server.WaitForPipeDrain();
}
private static async Task Rejects<T>(Func<Task> operation) where T:Exception
{try{await operation();}catch(T){return;}throw new InvalidOperationException("Expected "+typeof(T).Name);}
[Test] public async Task StatusCannotActivateNativeHooksAndObservationRequiresSeparateCommand()
{
var id=Random.Shared.Next(20000000,29000000);using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var commands=new List<string>();
var server=Task.Run(async()=>
{
await Serve($"XFEEDToolBox.Plugin.{id}",new{schema=1,name=GamePluginManager.PluginId,processId=id,loaded=true,active=true,observationStarted=false},timeout.Token,commands.Add);
await Serve($"XFEEDToolBox.Plugin.{id}",new{schema=1,name=GamePluginManager.PluginId,processId=id,loaded=true,active=true,observationStarted=true},timeout.Token,commands.Add);
});
Check.That(!(await GamePluginManager.ProbeAsync(id,token:timeout.Token)).ObservationStarted);
Check.That((await GamePluginManager.StartNavigationObservationAsync(id,timeout.Token)).ObservationStarted);
await server;Check.Equal("status\n",commands[0]);Check.Equal("navigation.observe.start\n",commands[1]);
}
[Test] public async Task IncompleteNativeCapabilitiesCannotReadOrWriteFlightControls()
{
var id=Random.Shared.Next(30000000,39000000);var hash=new string('a',64);
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var server=Serve($"XFEEDToolBox.Navigation.{id}",new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=false,continuousPose=true,continuousHeat=true,evidence="Observation only"}},timeout.Token);
var driver=new NativeFlightControlDriver(id,hash);
Check.That(!(await driver.PreflightAsync(timeout.Token)).Ready);await server;
await Rejects<InvalidOperationException>(async()=>{await driver.ReadAsync(timeout.Token);});
await driver.ReleaseAsync();
}
[Test] public async Task BuildMismatchIsRejectedBeforeAnyControlOperation()
{
var id=Random.Shared.Next(40000000,49000000);
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var server=Serve($"XFEEDToolBox.Navigation.{id}",new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=new string('b',64)}},timeout.Token);
var driver=new NativeFlightControlDriver(id,new string('a',64));
await Rejects<InvalidDataException>(async()=>{await driver.PreflightAsync(timeout.Token);});await server;
await Rejects<InvalidDataException>(()=>NavigationRecovery.PreflightWithRecoveryAsync(driver,null,timeout.Token));
await Rejects<InvalidOperationException>(async()=>{await driver.ReadAsync(timeout.Token);});
Check.That(!NavigationRecovery.CanRetry(new InvalidOperationException("原生导航进程、构建或协议不匹配。"),default));
}
[Test] public async Task HungNativeReplyTimesOutAndReleasesClientForNextConnection()
{
var id=Random.Shared.Next(50000000,59000000);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var server=Task.Run(async()=>
{
await using(var blocked=new NamedPipeServerStream(name,PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous))
{await blocked.WaitForConnectionAsync(timeout.Token);await blocked.ReadAsync(new byte[256],timeout.Token);await Task.Delay(750,timeout.Token);}
await Serve(name,new{ok=true,result=new{recovered=true}},timeout.Token);
});
var client=new NavigationPipeClient(id);var clock=Stopwatch.StartNew();
await Rejects<OperationCanceledException>(async()=>{await client.CallAsync(new{op="describe"},timeout.Token);});
Check.That(clock.Elapsed<TimeSpan.FromSeconds(2));
await Task.Delay(300,timeout.Token);
Check.That((await client.CallAsync(new{op="describe"},timeout.Token)).GetProperty("recovered").GetBoolean());
await server;
}
[Test] public async Task ReleaseWaitsForGameThreadCancellationAndKeepsTheSameOwner()
{
var id=Random.Shared.Next(60000000,69000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));
var requests=new List<JsonElement>();
void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated IPC test"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{accepted=true}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=false,cancellationPending=true}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true,cancellationPending=false}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash);
Check.That((await driver.PreflightAsync(timeout.Token)).Ready);
await driver.ApplyAsync(new("Test",null,0,false,true,false,null,"Cancellation"),1,
new(123,"Test",new(0,0,0),"G"),DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token);
await driver.ReleaseAsync();await server;
await driver.ReleaseAsync();
Check.Equal(4,requests.Count);
Check.Equal("release",requests[2].GetProperty("op").GetString()!);
Check.Equal("release",requests[3].GetProperty("op").GetString()!);
Check.Equal(requests[1].GetProperty("owner").GetString()!,requests[2].GetProperty("owner").GetString()!);
Check.Equal(requests[2].GetProperty("owner").GetString()!,requests[3].GetProperty("owner").GetString()!);
}
[Test] public async Task RoutePlanningUsesOriginalGameRequestAndPreserves64BitAddressesWithoutFlightInput()
{
var id=Random.Shared.Next(80000000,89000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
const long target=9007199254740995;using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var requests=new List<JsonElement>();void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{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 route IPC fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=71,applied=false,pending=true}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=71,applied=true,pending=false,failed=false,destinationAddress="9007199254740995",targetAddress="123",targetFresh=true,topologyFresh=true,waypointCount=2,routeFingerprint="18446744073709551615"}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);
var generation=await driver.RequestGameRouteAsync(new(target,"Destination",new(1,0,0),"G"),timeout.Token);
var status=await driver.ReadGameRouteStatusAsync(generation,target,timeout.Token);
await driver.ReleaseAsync();await server;
Check.Equal(3,requests.Count);Check.Equal("route.plan",requests[1].GetProperty("op").GetString()!);
Check.Equal(target,requests[1].GetProperty("destinationSystemAddress").GetInt64());Check.Equal(71L,generation);
Check.Equal("route.status",requests[2].GetProperty("op").GetString()!);Check.Equal(target,status.DestinationAddress);
Check.Equal(ulong.MaxValue,status.RouteFingerprint);
Check.Equal(requests[1].GetProperty("owner").GetString()!,requests[2].GetProperty("owner").GetString()!);
}
[Test] public async Task BankedAxesUseTheExistingGuardedFlightLeaseAndRequireAnExplicitCapability()
{
foreach(var supported in new[]{true,false})
{
var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
void Remember(string text){using var doc=JsonDocument.Parse(text);requests.Add(doc.RootElement.Clone());}
var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,GeometryVerified=true,AdaptiveFlightControl=true,
SystemAddress=1,TargetSystemAddress=2,Forward=new(0,0,1),Up=new(0,1,0)};
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=supported,evidence="Isolated three-axis IPC fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{frame}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=1,selected=true,targetAddress="2"}},timeout.Token,Remember);
if(supported)await Serve(name,new{ok=true,result=new{accepted=true,steering=new[]{0d,0,-.125},throttleCommand=0}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);await driver.ReadAsync(timeout.Token);
var next=new NavWaypoint(2,"Next",new(1,0,0),"K");await driver.SelectTargetAsync(next,timeout.Token);
var orbit=new FlightDecision("Orbit",frame.Forward,0,false,false,false,null,"Banked orbit"){UpDirection=new(1,0,0)};
if(supported)
{
await driver.ApplyAsync(orbit,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token);
Check.Equal(-.125,driver.LastAppliedControl!.Roll!.Value);
var input=requests.Single(r=>r.GetProperty("op").GetString()=="apply");var axes=input.GetProperty("steeringAxes");
Check.Equal(3,axes.GetArrayLength());Check.That(axes[2].GetDouble()<0&&Math.Abs(axes[2].GetDouble())<=.1,"An unmeasured ship starts with conservative roll authority.");
}
else await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(orbit,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
await driver.ReleaseAsync();await server;
Check.That(requests.All(r=>!r.GetProperty("op").GetString()!.StartsWith("calibrate.")));
}
}
[Test] public async Task RoutePlanningWaitsThroughArrivalCooldownWithoutFlightInputsButRejectsActiveCharging()
{
foreach(var (state,owned,fresh,retry) in new[]{(1,false,true,true),(4,false,true,true),(3,false,true,false),(1,true,true,false),(1,false,false,false)})
{
var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
void Remember(string text){using var doc=JsonDocument.Parse(text);requests.Add(doc.RootElement.Clone());}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{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 cooldown route fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=false,error="Game route planning requires a fresh idle FSD"},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{fresh,ownedCharging=owned,state}},timeout.Token,Remember);
if(retry)await Serve(name,new{ok=true,result=new{generation=72,applied=false,pending=true}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true,Navigation=new(){LastJumpAt=DateTimeOffset.UtcNow.AddSeconds(2)}});
await driver.PreflightAsync(timeout.Token);var destination=new NavWaypoint(2,"Destination",new(1,0,0),"G");
if(retry)Check.Equal(72L,await driver.RequestGameRouteAsync(destination,timeout.Token));
else await Rejects<InvalidOperationException>(()=>driver.RequestGameRouteAsync(destination,timeout.Token));
await driver.ReleaseAsync();await server;
Check.Equal(retry?4:3,requests.Count);
Check.That(requests.All(r=>r.GetProperty("op").GetString() is "describe" or "route.plan" or "fsd.describe"));
if(retry)Check.Equal(requests[1].GetProperty("owner").GetString()!,requests[3].GetProperty("owner").GetString()!);
}
}
[Test] public async Task DestinationIsAcknowledgedBeforeJumpAndFullThrottleIsRequired()
{
var id=Random.Shared.Next(70000000,79000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
const long target=9007199254740995;using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));
var requests=new List<JsonElement>();
void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated IPC fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=1,selected=false,pending=true,targetAddress="0"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=1,selected=true,pending=false,targetAddress="9007199254740995"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{frame=new FlightFrame{Sequence=1,ObservedAt=DateTimeOffset.UtcNow,GameSha256=hash,Forward=new(0,0,1),Up=new(0,1,0)}}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{accepted=true}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);
var next=new NavWaypoint(target,"Next",new(1,0,0),"G");var jump=new FlightDecision("Jump",new(1,0,0),1,false,false,true,60,"Ready");
await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(jump,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
await driver.SelectTargetAsync(next,timeout.Token);
await driver.ReadAsync(timeout.Token);
await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(jump with{Throttle=.75},1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
await driver.ApplyAsync(jump,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token);
await driver.ReleaseAsync();await server;
Check.Equal("target.select",requests[1].GetProperty("op").GetString()!);Check.Equal(target,requests[1].GetProperty("targetSystemAddress").GetInt64());
Check.Equal("target.status",requests[2].GetProperty("op").GetString()!);Check.Equal("read",requests[3].GetProperty("op").GetString()!);Check.Equal("apply",requests[4].GetProperty("op").GetString()!);
Check.Equal(1d,requests[4].GetProperty("decision").GetProperty("throttle").GetDouble());
}
private static PlayerState TargetArrivalPlayer(DateTimeOffset now)=>new(){InGame=true,CommanderId="F1",CurrentShipId=8,GameMode="Solo",LoadGameAt=now.AddHours(-1),
Navigation=new(){CurrentSystemAddress=1,TargetSystemAddress=2,CurrentSystemAt=now.AddSeconds(-1),LastJumpAt=now.AddSeconds(-1),Route=[new(1,"Here",new(0,0,0),"G"),new(2,"Next",new(1,0,0),"G")]}};
private static object TargetArrivalRaw(int state,bool manual=false,bool emergency=false)
{
var at=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
return new{sequence=7,observedAtUnixMilliseconds=at,entityId="42",referenceWorldAddress="1",localPlayerShipVerified=true,inGame=true,playerOverride=manual,rawFsdState=state,
shipHealth=new{verified=true,sequence=7,observedAtUnixMilliseconds=at,entityId="42",hullFraction=1,emergency,damageObserved=false}};
}
[Test]public async Task JournalArrivalBeforeNativeJumpTailWaitsWithoutAnyFlightInput()
{
var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List<JsonElement>();
void Remember(string text){using var doc=JsonDocument.Parse(text);requests.Add(doc.RootElement.Clone());}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated arrival-tail fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=false,error="Select the destination before starting FSD charging"},timeout.Token,Remember);
foreach(var (phase,owned) in new[]{(4,false),(3,true),(2,false)})
{
await Serve(name,new{ok=true,result=new{fresh=true,entityId="42",ownedCharging=owned,state=phase,targetValid=true,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=TargetArrivalRaw(phase)},timeout.Token,Remember);
}
await Serve(name,new{ok=true,result=new{generation=7,selected=false,pending=true,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=7,selected=true,pending=false,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var player=TargetArrivalPlayer(DateTimeOffset.UtcNow);var driver=new NativeFlightControlDriver(id,hash,()=>player);
var observations=new List<JsonElement>();driver.TargetSelectionObserved+=observations.Add;
await driver.PreflightAsync(timeout.Token);await driver.SelectTargetAsync(new(2,"Next",new(1,0,0),"G"),timeout.Token);await driver.ReleaseAsync();await server;
Check.That(requests.All(r=>r.GetProperty("op").GetString() is "describe" or "target.select" or "target.status" or "fsd.describe" or "read" or "release"));
Check.Equal(2,requests.Count(r=>r.GetProperty("op").GetString()=="target.select"));
Check.Equal("acknowledged",observations[^1].GetProperty("phase").GetString()!);
Check.That(observations.Any(o=>o.GetProperty("diagnostic").TryGetProperty("ownedCharging",out var owned)&&owned.ValueKind==JsonValueKind.True));
}
[Test]public async Task ArrivalSelectionManualOrHealthRejectionCannotIssueAnotherTargetOrFlightRequest()
{
foreach(var manual in new[]{true,false})
{
var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var operations=new List<string>();
void Remember(string text){using var doc=JsonDocument.Parse(text);operations.Add(doc.RootElement.GetProperty("op").GetString()!);}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,continuousPose=true,continuousHeat=true,
starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,evidence="Isolated arrival safety fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=false,error="Select the destination before starting FSD charging"},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{fresh=true,entityId="42",ownedCharging=false,state=2,targetValid=true,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=TargetArrivalRaw(2,manual,!manual)},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var player=TargetArrivalPlayer(DateTimeOffset.UtcNow);var driver=new NativeFlightControlDriver(id,hash,()=>player);await driver.PreflightAsync(timeout.Token);
await Rejects<InvalidOperationException>(()=>driver.SelectTargetAsync(new(2,"Next",new(1,0,0),"G"),timeout.Token));await driver.ReleaseAsync();await server;
Check.Equal(1,operations.Count(o=>o=="target.select"));Check.That(!operations.Any(o=>o is "apply" or "calibrate.fsd" or "target.status"));
}
}
[Test] public async Task ClearNormalSpaceDepartureDoesNotEnterSupercruiseOrWaitForSpeed()
{
var id=Random.Shared.Next(80000000,89000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List<string>();
var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,
Position=new(1e6,0,0),TargetPosition=new(0,1e7,0),Forward=new(0,1,0),Up=new(0,0,1),CurrentStar=new(1,"G",Vector3d.Zero,100,null,null),
InSupercruise=false,FuelMain=30,FuelNeededToNextRefuel=10,NativeAllowsCharging=true,SpeedMetresPerSecond=0,SpeedFraction=0,OutwardDepartureRequired=false};
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated reference fixture"}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{frame}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{ready=false,issue="original-flight-contexts-disabled-unconfirmed"}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{ready=true,magnitude=0}},timeout.Token,requests.Add);
});
var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true});await driver.PreflightAsync(timeout.Token);
await driver.PrepareForNavigationAsync(timeout.Token);await server;
Check.Equal(4,requests.Count);Check.That(requests.All(r=>!r.Contains("apply")&&!r.Contains("target.select")));
}
[Test] public async Task UnknownPhysicalSourceAndHeldRealInputRefuseLegacyPreparationBeforeAnyFlightCommand()
{
foreach(var missing in new[]{true,false})
{
var id=Random.Shared.Next(120000000,129000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<string>();
var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,Position=new(1e6,0,0),
TargetPosition=new(0,1e7,0),Forward=new(0,1,0),Up=new(0,0,1),CurrentStar=new(1,"G",Vector3d.Zero,100,null,null),
FuelMain=30,FuelNeededToNextRefuel=10,NativeAllowsCharging=true};
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,evidence="Isolated physical-input fixture"}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{frame}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{ready=!missing,magnitude=missing?0:1,issue=missing?"missing-or-invalid-button:YawLeftButton":null}},timeout.Token,requests.Add);
});
var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true});await driver.PreflightAsync(timeout.Token);
await Rejects<InvalidOperationException>(()=>driver.PrepareForNavigationAsync(timeout.Token));await server;
Check.Equal(3,requests.Count);Check.That(requests.All(request=>Check.Json(request).GetProperty("op").GetString() is "describe" or "read" or "input.physical.read"));
}
}
[Test] public async Task ANewPhysicalInputSceneWaitsForItsProofThenRequiresANewControlFrame()
{
var id=Random.Shared.Next(130000000,139000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<string>();
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,evidence="Isolated scene fixture"}},timeout.Token,requests.Add);
await Serve(name,new{ok=false,error="Original physical input sources unavailable: original-flight-contexts-disabled-unconfirmed"},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{ready=false,issue="original-flight-contexts-disabled-unconfirmed"}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{ready=true,magnitude=0}},timeout.Token,requests.Add);
});
var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);
await Rejects<FlightTelemetryUnavailableException>(()=>driver.ApplyAsync(new("Align",null,0,false,false,false,null,"Scene proof"),1,
new(2,"Next",new(0,0,1),"G"),DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
await server;
Check.Equal(1,requests.Count(request=>Check.Json(request).GetProperty("op").GetString()=="apply"));
Check.Equal("release",Check.Json(requests[2]).GetProperty("op").GetString()!);
foreach(var unknown in new[]{"Original physical input sources unavailable: missing-analog:YawAxisRaw",
"Original physical input sources unavailable: unobserved-original-relative-mouse-source"})
Check.That(!NavigationRecovery.CanRetry(new InvalidOperationException(unknown),default));
}
[Test] public async Task LoadedLegacyOutwardPolicyUsesExistingInputOnlyAfterSoftwareSafetyChecks()
{
foreach(var mode in new[]{0,1,2})
{
var safe=mode!=1;var transition=mode==2;
var id=Random.Shared.Next(110000000,119000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
var at=DateTimeOffset.UtcNow;
var frame=new FlightFrame{Sequence=1,ObservedAt=at,GameSha256=hash,GeometryVerified=true,AdaptiveFlightControl=true,JumpCommitInterlockVerified=true,NativeAllowsCharging=true,
NativeFullChargePeak=safe?80:101,Fsd=FsdPhase.Idle,SystemAddress=1,TargetSystemAddress=2,HeatPercent=40,FuelMain=20,FuelRequiredForJump=5,FuelNeededToNextRefuel=6,
Position=new(150,0,0),Forward=new Vector3d(-1,2,0).Unit(),Up=new(0,0,1),TargetPosition=new(-10000,20000,0),CurrentStar=new(1,"K",Vector3d.Zero,100,null,null)};
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated reference fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=1,selected=true,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{frame}},timeout.Token,Remember);
await Serve(name,new{ok=false,error="Native departure must be outward before FSD charging"},timeout.Token,Remember);
if(safe)
{
await Serve(name,new{ok=true,result=new{accepted=true,predictedPeak=80,throttleCommand=0}},timeout.Token,Remember);
if(transition)
{
await Serve(name,new{ok=false,error="Hyperspace calibration requires a matching locked system target and idle FSD"},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{fresh=true,ownedCharging=false,state=2,targetValid=true,targetAddress="2"}},timeout.Token,Remember);
}
else await Serve(name,new{ok=true,result=new{accepted=true,generation=1}},timeout.Token,Remember);
}
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var next=new NavWaypoint(2,"Next",new(1,2,0),"K");var driver=new NativeFlightControlDriver(id,hash);
await driver.PreflightAsync(timeout.Token);await driver.SelectTargetAsync(next,timeout.Token);await driver.ReadAsync(timeout.Token);
var decision=new FlightDecision("Align and charge",frame.Forward,1,true,false,false,80,"Independent fixture");
if(transition)await Rejects<FlightTelemetryUnavailableException>(()=>driver.ApplyAsync(decision,1,next,at.AddMilliseconds(300),timeout.Token));
else if(safe)await driver.ApplyAsync(decision,1,next,at.AddMilliseconds(300),timeout.Token);
else await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(decision,1,next,at.AddMilliseconds(300),timeout.Token));
await driver.ReleaseAsync();await server;
var primitive=requests.Where(r=>r.GetProperty("op").GetString()=="calibrate.fsd").ToArray();
Check.Equal(safe?1:0,primitive.Length);
if(safe)
{
Check.Equal("beginHyperspace",primitive[0].GetProperty("desired").GetString());
Check.Equal(90,primitive[0].GetProperty("heatCeilingPercent").GetInt32());
Check.That(!requests.Where(r=>r.GetProperty("op").GetString()=="apply").Last().GetProperty("decision").GetProperty("beginCharging").GetBoolean());
}
}
}
[Test] public async Task ObscuringPlanetPreparesSupercruiseAwayFromPlanetInsteadOfDistantStar()
{
var id=Random.Shared.Next(90000000,99000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(8));var requests=new List<JsonElement>();
void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,LoadoutFingerprint="fit",SystemAddress=1,
Position=new(100000,0,0),TargetPosition=new(1e8,0,0),Forward=new(-1,0,0),Up=new(0,0,1),CurrentStar=new(1,"G",Vector3d.Zero,100,null,null),
Obstacles=[new(11,"Unknown",new(110000,0,0),5000,null,null)],
InSupercruise=false,FuelMain=30,FuelNeededToNextRefuel=10,NativeAllowsCharging=true,SpeedFraction=1,HeatPercent=30,Fsd=FsdPhase.Idle,OutwardDepartureRequired=false};
var thermal=new ShipThermalCalibration{GameSha256=hash,LoadoutFingerprint="fit",StarBodyId=1,Verified=true,ExpiresAt=DateTimeOffset.UtcNow.AddMinutes(2),
MaximumHeat=150,ChargeDurationUpperSeconds=15,ChargingGainUpperPerSecond=.1,ChargedHoldGainUpperPerSecond=.1,DistanceBands=[new(99900,3e12,1,1)],Evidence="Isolated thermal fixture"};
var next=new NavWaypoint(2,"Next",new(1,0,0),"G");
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated reference fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=1,selected=true,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{frame=frame with{ObservedAt=DateTimeOffset.UtcNow},thermal}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{ready=true,magnitude=0}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{accepted=true}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{frame=frame with{InSupercruise=true,Sequence=2,ObservedAt=DateTimeOffset.UtcNow},thermal}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true,Navigation=new(){CurrentSystemAddress=1}});
await driver.PreflightAsync(timeout.Token);await driver.SelectTargetAsync(next,timeout.Token);await driver.PrepareForNavigationAsync(timeout.Token);await server;
var decision=requests.Single(r=>r.GetProperty("op").GetString()=="apply").GetProperty("decision");
Check.That(decision.GetProperty("enterSupercruise").GetBoolean());Check.That(decision.GetProperty("beginCharging").GetBoolean());
Check.Equal(-1d,decision.GetProperty("direction").GetProperty("x").GetDouble());
}
}
using System.Diagnostics;
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Plugins;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NativeNavigationTests
{
private static async Task Serve(string name,object response,CancellationToken token,Action<string>? request=null)
{
await using var server=new NamedPipeServerStream(name,PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous);
await server.WaitForConnectionAsync(token);
var buffer=new byte[4096];var count=await server.ReadAsync(buffer,token);
request?.Invoke(Encoding.UTF8.GetString(buffer,0,count));
await server.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(response,ToolboxSettings.JsonOptions),token);
await server.WriteAsync(new byte[]{(byte)'\n'},token);
server.WaitForPipeDrain();
}
private static async Task Rejects<T>(Func<Task> operation) where T:Exception
{try{await operation();}catch(T){return;}throw new InvalidOperationException("Expected "+typeof(T).Name);}
[Test] public async Task StatusCannotActivateNativeHooksAndObservationRequiresSeparateCommand()
{
var id=Random.Shared.Next(20000000,29000000);using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var commands=new List<string>();
var server=Task.Run(async()=>
{
await Serve($"XFEEDToolBox.Plugin.{id}",new{schema=1,name=GamePluginManager.PluginId,processId=id,loaded=true,active=true,observationStarted=false},timeout.Token,commands.Add);
await Serve($"XFEEDToolBox.Plugin.{id}",new{schema=1,name=GamePluginManager.PluginId,processId=id,loaded=true,active=true,observationStarted=true},timeout.Token,commands.Add);
});
Check.That(!(await GamePluginManager.ProbeAsync(id,token:timeout.Token)).ObservationStarted);
Check.That((await GamePluginManager.StartNavigationObservationAsync(id,timeout.Token)).ObservationStarted);
await server;Check.Equal("status\n",commands[0]);Check.Equal("navigation.observe.start\n",commands[1]);
}
[Test] public async Task IncompleteNativeCapabilitiesCannotReadOrWriteFlightControls()
{
var id=Random.Shared.Next(30000000,39000000);var hash=new string('a',64);
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var server=Serve($"XFEEDToolBox.Navigation.{id}",new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=false,continuousPose=true,continuousHeat=true,evidence="Observation only"}},timeout.Token);
var driver=new NativeFlightControlDriver(id,hash);
Check.That(!(await driver.PreflightAsync(timeout.Token)).Ready);await server;
await Rejects<InvalidOperationException>(async()=>{await driver.ReadAsync(timeout.Token);});
await driver.ReleaseAsync();
}
[Test] public async Task BuildMismatchIsRejectedBeforeAnyControlOperation()
{
var id=Random.Shared.Next(40000000,49000000);
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var server=Serve($"XFEEDToolBox.Navigation.{id}",new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=new string('b',64)}},timeout.Token);
var driver=new NativeFlightControlDriver(id,new string('a',64));
await Rejects<InvalidDataException>(async()=>{await driver.PreflightAsync(timeout.Token);});await server;
await Rejects<InvalidDataException>(()=>NavigationRecovery.PreflightWithRecoveryAsync(driver,null,timeout.Token));
await Rejects<InvalidOperationException>(async()=>{await driver.ReadAsync(timeout.Token);});
Check.That(!NavigationRecovery.CanRetry(new InvalidOperationException("原生导航进程、构建或协议不匹配。"),default));
}
[Test] public async Task HungNativeReplyTimesOutAndReleasesClientForNextConnection()
{
var id=Random.Shared.Next(50000000,59000000);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var server=Task.Run(async()=>
{
await using(var blocked=new NamedPipeServerStream(name,PipeDirection.InOut,1,PipeTransmissionMode.Byte,PipeOptions.Asynchronous))
{await blocked.WaitForConnectionAsync(timeout.Token);await blocked.ReadAsync(new byte[256],timeout.Token);await Task.Delay(750,timeout.Token);}
await Serve(name,new{ok=true,result=new{recovered=true}},timeout.Token);
});
var client=new NavigationPipeClient(id);var clock=Stopwatch.StartNew();
await Rejects<OperationCanceledException>(async()=>{await client.CallAsync(new{op="describe"},timeout.Token);});
Check.That(clock.Elapsed<TimeSpan.FromSeconds(2));
await Task.Delay(300,timeout.Token);
Check.That((await client.CallAsync(new{op="describe"},timeout.Token)).GetProperty("recovered").GetBoolean());
await server;
}
[Test] public async Task ReleaseWaitsForGameThreadCancellationAndKeepsTheSameOwner()
{
var id=Random.Shared.Next(60000000,69000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));
var requests=new List<JsonElement>();
void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated IPC test"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{accepted=true}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=false,cancellationPending=true}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true,cancellationPending=false}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash);
Check.That((await driver.PreflightAsync(timeout.Token)).Ready);
await driver.ApplyAsync(new("Test",null,0,false,true,false,null,"Cancellation"),1,
new(123,"Test",new(0,0,0),"G"),DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token);
await driver.ReleaseAsync();await server;
await driver.ReleaseAsync();
Check.Equal(4,requests.Count);
Check.Equal("release",requests[2].GetProperty("op").GetString()!);
Check.Equal("release",requests[3].GetProperty("op").GetString()!);
Check.Equal(requests[1].GetProperty("owner").GetString()!,requests[2].GetProperty("owner").GetString()!);
Check.Equal(requests[2].GetProperty("owner").GetString()!,requests[3].GetProperty("owner").GetString()!);
}
[Test] public async Task RoutePlanningUsesOriginalGameRequestAndPreserves64BitAddressesWithoutFlightInput()
{
var id=Random.Shared.Next(80000000,89000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
const long target=9007199254740995;using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));
var requests=new List<JsonElement>();void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{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 route IPC fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=71,applied=false,pending=true}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=71,applied=true,pending=false,failed=false,destinationAddress="9007199254740995",targetAddress="123",targetFresh=true,topologyFresh=true,waypointCount=2,routeFingerprint="18446744073709551615"}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);
var generation=await driver.RequestGameRouteAsync(new(target,"Destination",new(1,0,0),"G"),timeout.Token);
var status=await driver.ReadGameRouteStatusAsync(generation,target,timeout.Token);
await driver.ReleaseAsync();await server;
Check.Equal(3,requests.Count);Check.Equal("route.plan",requests[1].GetProperty("op").GetString()!);
Check.Equal(target,requests[1].GetProperty("destinationSystemAddress").GetInt64());Check.Equal(71L,generation);
Check.Equal("route.status",requests[2].GetProperty("op").GetString()!);Check.Equal(target,status.DestinationAddress);
Check.Equal(ulong.MaxValue,status.RouteFingerprint);
Check.Equal(requests[1].GetProperty("owner").GetString()!,requests[2].GetProperty("owner").GetString()!);
}
[Test] public async Task BankedAxesUseTheExistingGuardedFlightLeaseAndRequireAnExplicitCapability()
{
foreach(var supported in new[]{true,false})
{
var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
void Remember(string text){using var doc=JsonDocument.Parse(text);requests.Add(doc.RootElement.Clone());}
var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,GeometryVerified=true,AdaptiveFlightControl=true,
SystemAddress=1,TargetSystemAddress=2,Forward=new(0,0,1),Up=new(0,1,0)};
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=supported,evidence="Isolated three-axis IPC fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{frame}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=1,selected=true,targetAddress="2"}},timeout.Token,Remember);
if(supported)await Serve(name,new{ok=true,result=new{accepted=true,steering=new[]{0d,0,-.125},throttleCommand=0}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);await driver.ReadAsync(timeout.Token);
var next=new NavWaypoint(2,"Next",new(1,0,0),"K");await driver.SelectTargetAsync(next,timeout.Token);
var orbit=new FlightDecision("Orbit",frame.Forward,0,false,false,false,null,"Banked orbit"){UpDirection=new(1,0,0)};
if(supported)
{
await driver.ApplyAsync(orbit,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token);
Check.Equal(-.125,driver.LastAppliedControl!.Roll!.Value);
var input=requests.Single(r=>r.GetProperty("op").GetString()=="apply");var axes=input.GetProperty("steeringAxes");
Check.Equal(3,axes.GetArrayLength());Check.That(axes[2].GetDouble()<0&&Math.Abs(axes[2].GetDouble())<=.1,"An unmeasured ship starts with conservative roll authority.");
}
else await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(orbit,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
await driver.ReleaseAsync();await server;
Check.That(requests.All(r=>!r.GetProperty("op").GetString()!.StartsWith("calibrate.")));
}
}
[Test] public async Task RoutePlanningWaitsThroughArrivalCooldownWithoutFlightInputsButRejectsActiveCharging()
{
foreach(var (state,owned,fresh,retry) in new[]{(1,false,true,true),(4,false,true,true),(3,false,true,false),(1,true,true,false),(1,false,false,false)})
{
var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
void Remember(string text){using var doc=JsonDocument.Parse(text);requests.Add(doc.RootElement.Clone());}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{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 cooldown route fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=false,error="Game route planning requires a fresh idle FSD"},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{fresh,ownedCharging=owned,state}},timeout.Token,Remember);
if(retry)await Serve(name,new{ok=true,result=new{generation=72,applied=false,pending=true}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true,Navigation=new(){LastJumpAt=DateTimeOffset.UtcNow.AddSeconds(2)}});
await driver.PreflightAsync(timeout.Token);var destination=new NavWaypoint(2,"Destination",new(1,0,0),"G");
if(retry)Check.Equal(72L,await driver.RequestGameRouteAsync(destination,timeout.Token));
else await Rejects<InvalidOperationException>(()=>driver.RequestGameRouteAsync(destination,timeout.Token));
await driver.ReleaseAsync();await server;
Check.Equal(retry?4:3,requests.Count);
Check.That(requests.All(r=>r.GetProperty("op").GetString() is "describe" or "route.plan" or "fsd.describe"));
if(retry)Check.Equal(requests[1].GetProperty("owner").GetString()!,requests[3].GetProperty("owner").GetString()!);
}
}
[Test] public async Task DestinationIsAcknowledgedBeforeJumpAndFullThrottleIsRequired()
{
var id=Random.Shared.Next(70000000,79000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
const long target=9007199254740995;using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));
var requests=new List<JsonElement>();
void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated IPC fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=1,selected=false,pending=true,targetAddress="0"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=1,selected=true,pending=false,targetAddress="9007199254740995"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{frame=new FlightFrame{Sequence=1,ObservedAt=DateTimeOffset.UtcNow,GameSha256=hash,Forward=new(0,0,1),Up=new(0,1,0)}}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{accepted=true}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);
var next=new NavWaypoint(target,"Next",new(1,0,0),"G");var jump=new FlightDecision("Jump",new(1,0,0),1,false,false,true,60,"Ready");
await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(jump,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
await driver.SelectTargetAsync(next,timeout.Token);
await driver.ReadAsync(timeout.Token);
await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(jump with{Throttle=.75},1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
await driver.ApplyAsync(jump,1,next,DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token);
await driver.ReleaseAsync();await server;
Check.Equal("target.select",requests[1].GetProperty("op").GetString()!);Check.Equal(target,requests[1].GetProperty("targetSystemAddress").GetInt64());
Check.Equal("target.status",requests[2].GetProperty("op").GetString()!);Check.Equal("read",requests[3].GetProperty("op").GetString()!);Check.Equal("apply",requests[4].GetProperty("op").GetString()!);
Check.Equal(1d,requests[4].GetProperty("decision").GetProperty("throttle").GetDouble());
}
private static PlayerState TargetArrivalPlayer(DateTimeOffset now)=>new(){InGame=true,CommanderId="F1",CurrentShipId=8,GameMode="Solo",LoadGameAt=now.AddHours(-1),
Navigation=new(){CurrentSystemAddress=1,TargetSystemAddress=2,CurrentSystemAt=now.AddSeconds(-1),LastJumpAt=now.AddSeconds(-1),Route=[new(1,"Here",new(0,0,0),"G"),new(2,"Next",new(1,0,0),"G")]}};
private static object TargetArrivalRaw(int state,bool manual=false,bool emergency=false)
{
var at=DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
return new{sequence=7,observedAtUnixMilliseconds=at,entityId="42",referenceWorldAddress="1",localPlayerShipVerified=true,inGame=true,playerOverride=manual,rawFsdState=state,
shipHealth=new{verified=true,sequence=7,observedAtUnixMilliseconds=at,entityId="42",hullFraction=1,emergency,damageObserved=false}};
}
[Test]public async Task JournalArrivalBeforeNativeJumpTailWaitsWithoutAnyFlightInput()
{
var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List<JsonElement>();
void Remember(string text){using var doc=JsonDocument.Parse(text);requests.Add(doc.RootElement.Clone());}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated arrival-tail fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=false,error="Select the destination before starting FSD charging"},timeout.Token,Remember);
foreach(var (phase,owned) in new[]{(4,false),(3,true),(2,false)})
{
await Serve(name,new{ok=true,result=new{fresh=true,entityId="42",ownedCharging=owned,state=phase,targetValid=true,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=TargetArrivalRaw(phase)},timeout.Token,Remember);
}
await Serve(name,new{ok=true,result=new{generation=7,selected=false,pending=true,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=7,selected=true,pending=false,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var player=TargetArrivalPlayer(DateTimeOffset.UtcNow);var driver=new NativeFlightControlDriver(id,hash,()=>player);
var observations=new List<JsonElement>();driver.TargetSelectionObserved+=observations.Add;
await driver.PreflightAsync(timeout.Token);await driver.SelectTargetAsync(new(2,"Next",new(1,0,0),"G"),timeout.Token);await driver.ReleaseAsync();await server;
Check.That(requests.All(r=>r.GetProperty("op").GetString() is "describe" or "target.select" or "target.status" or "fsd.describe" or "read" or "release"));
Check.Equal(2,requests.Count(r=>r.GetProperty("op").GetString()=="target.select"));
Check.Equal("acknowledged",observations[^1].GetProperty("phase").GetString()!);
Check.That(observations.Any(o=>o.GetProperty("diagnostic").TryGetProperty("ownedCharging",out var owned)&&owned.ValueKind==JsonValueKind.True));
}
[Test]public async Task ArrivalSelectionManualOrHealthRejectionCannotIssueAnotherTargetOrFlightRequest()
{
foreach(var manual in new[]{true,false})
{
var id=Random.Shared.Next(100000000,190000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var operations=new List<string>();
void Remember(string text){using var doc=JsonDocument.Parse(text);operations.Add(doc.RootElement.GetProperty("op").GetString()!);}
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,continuousPose=true,continuousHeat=true,
starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,evidence="Isolated arrival safety fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=false,error="Select the destination before starting FSD charging"},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{fresh=true,entityId="42",ownedCharging=false,state=2,targetValid=true,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=TargetArrivalRaw(2,manual,!manual)},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var player=TargetArrivalPlayer(DateTimeOffset.UtcNow);var driver=new NativeFlightControlDriver(id,hash,()=>player);await driver.PreflightAsync(timeout.Token);
await Rejects<InvalidOperationException>(()=>driver.SelectTargetAsync(new(2,"Next",new(1,0,0),"G"),timeout.Token));await driver.ReleaseAsync();await server;
Check.Equal(1,operations.Count(o=>o=="target.select"));Check.That(!operations.Any(o=>o is "apply" or "calibrate.fsd" or "target.status"));
}
}
[Test] public async Task ClearNormalSpaceDepartureDoesNotEnterSupercruiseOrWaitForSpeed()
{
var id=Random.Shared.Next(80000000,89000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(6));var requests=new List<string>();
var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,
Position=new(1e6,0,0),TargetPosition=new(0,1e7,0),Forward=new(0,1,0),Up=new(0,0,1),CurrentStar=new(1,"G",Vector3d.Zero,100,null,null),
InSupercruise=false,FuelMain=30,FuelNeededToNextRefuel=10,NativeAllowsCharging=true,SpeedMetresPerSecond=0,SpeedFraction=0,OutwardDepartureRequired=false};
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated reference fixture"}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{frame}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{ready=false,issue="original-flight-contexts-disabled-unconfirmed"}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{ready=true,magnitude=0}},timeout.Token,requests.Add);
});
var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true});await driver.PreflightAsync(timeout.Token);
await driver.PrepareForNavigationAsync(timeout.Token);await server;
Check.Equal(4,requests.Count);Check.That(requests.All(r=>!r.Contains("apply")&&!r.Contains("target.select")));
}
[Test] public async Task UnknownPhysicalSourceAndHeldRealInputRefuseLegacyPreparationBeforeAnyFlightCommand()
{
foreach(var missing in new[]{true,false})
{
var id=Random.Shared.Next(120000000,129000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<string>();
var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,Position=new(1e6,0,0),
TargetPosition=new(0,1e7,0),Forward=new(0,1,0),Up=new(0,0,1),CurrentStar=new(1,"G",Vector3d.Zero,100,null,null),
FuelMain=30,FuelNeededToNextRefuel=10,NativeAllowsCharging=true};
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,evidence="Isolated physical-input fixture"}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{frame}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{ready=!missing,magnitude=missing?0:1,issue=missing?"missing-or-invalid-button:YawLeftButton":null}},timeout.Token,requests.Add);
});
var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true});await driver.PreflightAsync(timeout.Token);
await Rejects<InvalidOperationException>(()=>driver.PrepareForNavigationAsync(timeout.Token));await server;
Check.Equal(3,requests.Count);Check.That(requests.All(request=>Check.Json(request).GetProperty("op").GetString() is "describe" or "read" or "input.physical.read"));
}
}
[Test] public async Task ANewPhysicalInputSceneWaitsForItsProofThenRequiresANewControlFrame()
{
var id=Random.Shared.Next(130000000,139000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<string>();
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,evidence="Isolated scene fixture"}},timeout.Token,requests.Add);
await Serve(name,new{ok=false,error="Original physical input sources unavailable: original-flight-contexts-disabled-unconfirmed"},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{ready=false,issue="original-flight-contexts-disabled-unconfirmed"}},timeout.Token,requests.Add);
await Serve(name,new{ok=true,result=new{ready=true,magnitude=0}},timeout.Token,requests.Add);
});
var driver=new NativeFlightControlDriver(id,hash);await driver.PreflightAsync(timeout.Token);
await Rejects<FlightTelemetryUnavailableException>(()=>driver.ApplyAsync(new("Align",null,0,false,false,false,null,"Scene proof"),1,
new(2,"Next",new(0,0,1),"G"),DateTimeOffset.UtcNow.AddMilliseconds(250),timeout.Token));
await server;
Check.Equal(1,requests.Count(request=>Check.Json(request).GetProperty("op").GetString()=="apply"));
Check.Equal("release",Check.Json(requests[2]).GetProperty("op").GetString()!);
foreach(var unknown in new[]{"Original physical input sources unavailable: missing-analog:YawAxisRaw",
"Original physical input sources unavailable: unobserved-original-relative-mouse-source"})
Check.That(!NavigationRecovery.CanRetry(new InvalidOperationException(unknown),default));
}
[Test] public async Task LoadedLegacyOutwardPolicyUsesExistingInputOnlyAfterSoftwareSafetyChecks()
{
foreach(var mode in new[]{0,1,2})
{
var safe=mode!=1;var transition=mode==2;
var id=Random.Shared.Next(110000000,119000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(5));var requests=new List<JsonElement>();
void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
var at=DateTimeOffset.UtcNow;
var frame=new FlightFrame{Sequence=1,ObservedAt=at,GameSha256=hash,GeometryVerified=true,AdaptiveFlightControl=true,JumpCommitInterlockVerified=true,NativeAllowsCharging=true,
NativeFullChargePeak=safe?80:101,Fsd=FsdPhase.Idle,SystemAddress=1,TargetSystemAddress=2,HeatPercent=40,FuelMain=20,FuelRequiredForJump=5,FuelNeededToNextRefuel=6,
Position=new(150,0,0),Forward=new Vector3d(-1,2,0).Unit(),Up=new(0,0,1),TargetPosition=new(-10000,20000,0),CurrentStar=new(1,"K",Vector3d.Zero,100,null,null)};
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated reference fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=1,selected=true,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{frame}},timeout.Token,Remember);
await Serve(name,new{ok=false,error="Native departure must be outward before FSD charging"},timeout.Token,Remember);
if(safe)
{
await Serve(name,new{ok=true,result=new{accepted=true,predictedPeak=80,throttleCommand=0}},timeout.Token,Remember);
if(transition)
{
await Serve(name,new{ok=false,error="Hyperspace calibration requires a matching locked system target and idle FSD"},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{fresh=true,ownedCharging=false,state=2,targetValid=true,targetAddress="2"}},timeout.Token,Remember);
}
else await Serve(name,new{ok=true,result=new{accepted=true,generation=1}},timeout.Token,Remember);
}
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var next=new NavWaypoint(2,"Next",new(1,2,0),"K");var driver=new NativeFlightControlDriver(id,hash);
await driver.PreflightAsync(timeout.Token);await driver.SelectTargetAsync(next,timeout.Token);await driver.ReadAsync(timeout.Token);
var decision=new FlightDecision("Align and charge",frame.Forward,1,true,false,false,80,"Independent fixture");
if(transition)await Rejects<FlightTelemetryUnavailableException>(()=>driver.ApplyAsync(decision,1,next,at.AddMilliseconds(300),timeout.Token));
else if(safe)await driver.ApplyAsync(decision,1,next,at.AddMilliseconds(300),timeout.Token);
else await Rejects<InvalidOperationException>(()=>driver.ApplyAsync(decision,1,next,at.AddMilliseconds(300),timeout.Token));
await driver.ReleaseAsync();await server;
var primitive=requests.Where(r=>r.GetProperty("op").GetString()=="calibrate.fsd").ToArray();
Check.Equal(safe?1:0,primitive.Length);
if(safe)
{
Check.Equal("beginHyperspace",primitive[0].GetProperty("desired").GetString());
Check.Equal(90,primitive[0].GetProperty("heatCeilingPercent").GetInt32());
Check.That(!requests.Where(r=>r.GetProperty("op").GetString()=="apply").Last().GetProperty("decision").GetProperty("beginCharging").GetBoolean());
}
}
}
[Test] public async Task ObscuringPlanetPreparesSupercruiseAwayFromPlanetInsteadOfDistantStar()
{
var id=Random.Shared.Next(90000000,99000000);var hash=new string('a',64);var name=$"XFEEDToolBox.Navigation.{id}";
using var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(8));var requests=new List<JsonElement>();
void Remember(string text){using var document=JsonDocument.Parse(text);requests.Add(document.RootElement.Clone());}
var frame=new FlightFrame{Sequence=1,GameSha256=hash,ObservedAt=DateTimeOffset.UtcNow,LoadoutFingerprint="fit",SystemAddress=1,
Position=new(100000,0,0),TargetPosition=new(1e8,0,0),Forward=new(-1,0,0),Up=new(0,0,1),CurrentStar=new(1,"G",Vector3d.Zero,100,null,null),
Obstacles=[new(11,"Unknown",new(110000,0,0),5000,null,null)],
InSupercruise=false,FuelMain=30,FuelNeededToNextRefuel=10,NativeAllowsCharging=true,SpeedFraction=1,HeatPercent=30,Fsd=FsdPhase.Idle,OutwardDepartureRequired=false};
var thermal=new ShipThermalCalibration{GameSha256=hash,LoadoutFingerprint="fit",StarBodyId=1,Verified=true,ExpiresAt=DateTimeOffset.UtcNow.AddMinutes(2),
MaximumHeat=150,ChargeDurationUpperSeconds=15,ChargingGainUpperPerSecond=.1,ChargedHoldGainUpperPerSecond=.1,DistanceBands=[new(99900,3e12,1,1)],Evidence="Isolated thermal fixture"};
var next=new NavWaypoint(2,"Next",new(1,0,0),"G");
var server=Task.Run(async()=>
{
await Serve(name,new{ok=true,result=new{schema=1,name="XFEEDToolBox.Navigation",processId=id,gameSha256=hash,verified=true,
continuousPose=true,continuousHeat=true,starEnvelopes=true,nativeSteering=true,chargeAndCommitInterlock=true,applicationSteeringAxes=true,evidence="Isolated reference fixture"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{generation=1,selected=true,targetAddress="2"}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{frame=frame with{ObservedAt=DateTimeOffset.UtcNow},thermal}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{ready=true,magnitude=0}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{accepted=true}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{frame=frame with{InSupercruise=true,Sequence=2,ObservedAt=DateTimeOffset.UtcNow},thermal}},timeout.Token,Remember);
await Serve(name,new{ok=true,result=new{released=true}},timeout.Token,Remember);
});
var driver=new NativeFlightControlDriver(id,hash,()=>new(){InGame=true,Navigation=new(){CurrentSystemAddress=1}});
await driver.PreflightAsync(timeout.Token);await driver.SelectTargetAsync(next,timeout.Token);await driver.PrepareForNavigationAsync(timeout.Token);await server;
var decision=requests.Single(r=>r.GetProperty("op").GetString()=="apply").GetProperty("decision");
Check.That(decision.GetProperty("enterSupercruise").GetBoolean());Check.That(decision.GetProperty("beginCharging").GetBoolean());
Check.Equal(-1d,decision.GetProperty("direction").GetProperty("x").GetDouble());
}
}