using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
/// <summary>A bounded, explicitly started one-hop acceptance. It never claims complete navigation acceptance.</summary>
internal static class HyperspaceAcceptance
{
private static IEnumerable<JsonElement> JournalEvents(string path)
{
using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
using var reader=new StreamReader(stream);
while(reader.ReadLine() is {} line)
{
// A writer may be appending the last event; use only complete JSON objects.
if(!line.TrimEnd().EndsWith('}'))continue;
using var document=JsonDocument.Parse(line);yield return document.RootElement.Clone();
}
}
public static async Task<bool> RunAsync(int pid,string profilePath,string reportPath,string mode,string? explicitDestination=null)
{
if(mode is not ("hold" or "jump"))throw new ArgumentException("Expected hold or jump.");
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new evidence path.");
using var profile=JsonDocument.Parse(await File.ReadAllTextAsync(profilePath));
var image=profile.RootElement.GetProperty("image").GetString()!;
var hash=profile.RootElement.GetProperty("sha256").GetString();
await using(var file=File.OpenRead(image))if(Convert.ToHexStringLower(await SHA256.HashDataAsync(file))!=hash)throw new InvalidDataException("Game build changed.");
using var game=Process.GetProcessById(pid);var started=game.StartTime.ToUniversalTime().Ticks;
if(!string.Equals(game.MainModule?.FileName,image,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("Selected game changed.");
var settingsPath=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json");
var settings=JsonSerializer.Deserialize<ToolboxSettings>(await File.ReadAllTextAsync(settingsPath),ToolboxSettings.JsonOptions)!;
var destination=explicitDestination??settings.NavigationDestination;
if(string.IsNullOrWhiteSpace(destination))throw new InvalidOperationException("A saved navigation destination is required.");
var journal=Directory.GetFiles(settings.JournalDirectory,"Journal.*.log").OrderBy(File.GetLastWriteTimeUtc).Last();
var startedAt=DateTimeOffset.UtcNow;var journalOffset=new FileInfo(journal).Length;
using var status=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(settings.JournalDirectory,"Status.json")));
var flags=status.RootElement.GetProperty("Flags").GetUInt64();
var supercruise=(flags&16)!=0;
if((flags&(1UL|1048576UL|2097152UL))!=0||status.RootElement.GetProperty("GuiFocus").GetInt32()!=0||mode=="hold"&&supercruise)throw new InvalidOperationException("Requires undocked controllable space without danger or UI focus.");
var pipe=new NavigationPipeClient(pid);var owner=Guid.NewGuid().ToString("N");var clock=Stopwatch.StartNew();
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
async Task Log(object row){await log.WriteLineAsync(JsonSerializer.Serialize(row));await log.FlushAsync();}
static Vector3d V(JsonElement value,string key){var a=value.GetProperty(key);return new(a[0].GetDouble(),a[1].GetDouble(),a[2].GetDouble());}
var frames=0;var peak=0.0;var sawCharging=false;var sawFull=false;var sawJump=false;var fullThrottleBeforeJump=false;var commitRequested=false;var chargeRequested=false;var released=false;var arrived=false;
double? filledAt=null,alignedAt=null,jumpAt=null,invalidTargetAt=null,telemetryGapAt=null;string? failure=null;ulong target=0;JsonElement? arrival=null;
async Task Axes(JsonElement frame,AttitudeInput aim,double throttle)
{
// Holding at rest does not need a throttle override; retain the game's ordinary input mode.
if(mode=="hold")throttle=-1;
await pipe.CallAsync(new{op="calibrate.axes",owner,sequence=frame.GetProperty("sequence").GetInt64(),ttlMilliseconds=250,throttle,
axes=new[]{0.0,0,0,aim.Yaw,aim.Pitch,aim.Roll}});
}
async Task Fsd(JsonElement frame,string desired)
{
await pipe.CallAsync(new{op="calibrate.fsd",owner,sequence=frame.GetProperty("sequence").GetInt64(),ttlMilliseconds=250,desired,targetSystemAddress=target,heatCeilingPercent=90});
}
async Task Release()
{
var stop=Stopwatch.StartNew();
while(stop.Elapsed.TotalSeconds<3)
{
var result=await pipe.CallAsync(new{op="release",owner});await Log(new{kind="release",seconds=clock.Elapsed.TotalSeconds,result});
if(result.GetProperty("released").GetBoolean()){released=true;return;}
await Task.Delay(40);
}
throw new TimeoutException("Control release was not acknowledged.");
}
async Task PauseTelemetry(string reason)
{
telemetryGapAt??=clock.Elapsed.TotalSeconds;
await Log(new{kind="telemetry-pause",seconds=clock.Elapsed.TotalSeconds,reason});
await Release();
if(clock.Elapsed.TotalSeconds-telemetryGapAt>2)throw new InvalidOperationException("Flight telemetry remained unavailable for more than two seconds.");
await Task.Delay(50);
}
try
{
var hello=await pipe.CallAsync(new{op="describe"});
if(hello.GetProperty("version").GetString()!="0.3.1-dev"||hello.GetProperty("gameSha256").GetString()!=hash||hello.GetProperty("processId").GetInt32()!=pid)throw new InvalidDataException("Native acceptance protocol mismatch.");
var initial=await pipe.CallAsync(new{op="read"});var fsd=await pipe.CallAsync(new{op="fsd.describe"});
if(!initial.GetProperty("localPlayerShipVerified").GetBoolean()||fsd.GetProperty("entityId").GetString()!=initial.GetProperty("entityId").GetString())throw new InvalidDataException("Pose and FSD must belong to the original game's local PlayerShip reference.");
if(!supercruise&&initial.GetProperty("speedMetresPerSecond").GetDouble()>=1||initial.GetProperty("heatPercent").GetDouble()>=(supercruise?82:60)||fsd.GetProperty("state").GetInt32()!=2||!fsd.GetProperty("fresh").GetBoolean()||!fsd.GetProperty("targetValid").GetBoolean())throw new InvalidOperationException("Ship/target/FSD initial conditions are not ready.");
target=ulong.Parse(fsd.GetProperty("targetAddress").GetString()!);
JsonElement? locked=null;
foreach(var row in JournalEvents(journal))
{
if(row.GetProperty("event").GetString()=="FSDTarget")locked=row;
}
if(locked is not {} selected||selected.GetProperty("SystemAddress").GetUInt64()!=target||!string.Equals(selected.GetProperty("Name").GetString(),destination,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("The actual game target does not match the saved destination.");
var geometry=await pipe.CallAsync(new{op="geometry.describe"});
var position=V(initial,"position");var ray=V(fsd,"targetDirection").Unit();var along=Vector3d.Dot(position,ray);
var clearance=along<0?Math.Sqrt(Math.Max(0,position.Length*position.Length-along*along)):position.Length;
if(!geometry.GetProperty("fresh").GetBoolean()||geometry.GetProperty("bodyKind").GetInt32()!=1||clearance<=geometry.GetProperty("escapeRadius").GetDouble()*1.2)throw new InvalidOperationException("This one-hop test requires an already unobscured stellar departure.");
if(supercruise&&Vector3d.Dot(position.Unit(),ray)<.95)throw new InvalidOperationException("This bounded SC acceptance only permits an outward departure.");
var escapeRadius=geometry.GetProperty("escapeRadius").GetDouble();
await Log(new{kind="preflight",mode,supercruise,startedAt,pid,target,destination,locked,initial,fsd,geometry,clearance});
// An explicit new run acquires its own lease; current physical input still cancels it natively.
await Axes(initial,default,mode=="hold"?-1:0);await Task.Delay(50);
var phase=mode=="jump"?"prealign":"charge";
while(clock.Elapsed.TotalSeconds<300)
{
try
{
using var current=Process.GetProcessById(pid);
if(current.StartTime.ToUniversalTime().Ticks!=started)throw new InvalidOperationException("Process identity changed.");
if(sawJump)
{
foreach(var e in JournalEvents(journal))
{
if(e.GetProperty("event").GetString()=="FSDJump"&&e.GetProperty("timestamp").GetDateTimeOffset()>=startedAt.AddSeconds(-1)&&e.GetProperty("SystemAddress").GetUInt64()==target){arrival=e.Clone();arrived=true;break;}
}
if(arrived)break;
if(clock.Elapsed.TotalSeconds-jumpAt>75)throw new TimeoutException("No actual Journal FSDJump arrival after native jump.");
await Task.Delay(200);continue;
}
JsonElement frame;
try{frame=await pipe.CallAsync(new{op="read"});fsd=await pipe.CallAsync(new{op="fsd.describe"});}
catch(InvalidOperationException error)when(error.Message=="Native player telemetry unavailable or stale")
{await PauseTelemetry(error.Message);continue;}
frames++;
peak=Math.Max(peak,frame.GetProperty("heatPercent").GetDouble());
var state=fsd.GetProperty("state").GetInt32();var charge=fsd.GetProperty("clock").GetDouble();var duration=fsd.GetProperty("duration").GetDouble();
if(state==4)
{
sawFull|=duration>0&&charge>=duration;
await Log(new{kind="commit-observed",seconds=clock.Elapsed.TotalSeconds,frame,fsd});
fullThrottleBeforeJump|=commitRequested&&fsd.GetProperty("throttle").GetDouble()>=.999;
if(mode!="jump"||!fullThrottleBeforeJump)failure="Unexpected jump before verified full throttle.";
sawJump=true;jumpAt=clock.Elapsed.TotalSeconds;await Release();continue;
}
if(frame.GetProperty("playerOverride").GetBoolean())
{await Log(new{kind="guard-observation",seconds=clock.Elapsed.TotalSeconds,frame,fsd});throw new InvalidOperationException("Flight state became stale or was overridden.");}
if(!frame.GetProperty("inGame").GetBoolean()||!fsd.GetProperty("fresh").GetBoolean())
{await Log(new{kind="transient-observation",seconds=clock.Elapsed.TotalSeconds,frame,fsd});await PauseTelemetry("Current ship or FSD sample is temporarily unavailable");continue;}
telemetryGapAt=null;
var observedTarget=ulong.Parse(fsd.GetProperty("targetAddress").GetString()!);
if(observedTarget!=0&&observedTarget!=target)
{await Log(new{kind="target-changed",seconds=clock.Elapsed.TotalSeconds,frame,fsd});throw new InvalidOperationException("The locked destination changed.");}
if(!fsd.GetProperty("targetValid").GetBoolean())
{
invalidTargetAt??=clock.Elapsed.TotalSeconds;
await Log(new{kind="target-temporarily-unavailable",seconds=clock.Elapsed.TotalSeconds,frame,fsd});
await Release();
if(clock.Elapsed.TotalSeconds-invalidTargetAt>2)throw new InvalidOperationException("The destination telemetry remained unavailable.");
alignedAt=null;await Task.Delay(50);continue;
}
invalidTargetAt=null;
if(released){released=false;if(state==2&&chargeRequested){sawCharging=false;chargeRequested=false;filledAt=null;sawFull=false;phase=frame.GetProperty("heatPercent").GetDouble()>=60?"cool":"prealign";}}
var aim=AttitudeController.Aim(V(frame,"forward"),V(frame,"up"),V(fsd,"targetDirection"),.6);
await Log(new{kind="sample",seconds=clock.Elapsed.TotalSeconds,phase,aim,frame,fsd});
if(peak>=89.5)throw new InvalidOperationException("Reached the bounded thermal ceiling; cancelling charging.");
if(phase=="cool")
{
await Axes(frame,default,0);
if(frame.GetProperty("heatPercent").GetDouble()<55)phase="prealign";
}
else if(phase=="prealign")
{
await Axes(frame,aim,0);
if(aim.ErrorDegrees<=8)phase=supercruise&&frame.GetProperty("heatPercent").GetDouble()>=55?"escape":"charge-start";
}
else if(phase=="escape")
{
await Axes(frame,default,1);
if(frame.GetProperty("heatPercent").GetDouble()<55&&V(frame,"position").Length>=escapeRadius*3)phase="charge-start";
}
else if(phase=="charge-start")
{
// Full throttle and charge are requested together. Current speed need not
// have reached its maximum; the original FSD still enforces its own speed check.
await Axes(frame,default,1);
phase="charge";await Fsd(frame,"beginHyperspace");chargeRequested=true;
await Log(new{kind="full-throttle-and-charge-requested",seconds=clock.Elapsed.TotalSeconds,frame,fsd});
}
else
{
if(mode=="jump"&&fsd.GetProperty("throttle").GetDouble()>=.999)
{
fullThrottleBeforeJump=true;
}
if(!sawCharging&&state==2)
{
if(!chargeRequested){await Fsd(frame,"beginHyperspace");chargeRequested=true;}
else if(fsd.GetProperty("beginRejected").GetBoolean())throw new InvalidOperationException("Original game eligibility rejected hyperspace.");
}
if(state==3){sawCharging=true;}
if(sawCharging&&state==2)throw new InvalidOperationException("Original charging stopped before acceptance.");
if(state==3&&duration>0&&charge>=duration){sawFull=true;filledAt??=clock.Elapsed.TotalSeconds;}
if(mode=="hold")
{
await Axes(frame,default,0);
if(filledAt is {} filled&&clock.Elapsed.TotalSeconds-filled>=1){await Fsd(frame,"cancel");await Release();break;}
}
else
{
var late=state==3&&duration>0&&charge>=duration-4;
if(late&&aim.ErrorDegrees<.7)alignedAt??=clock.Elapsed.TotalSeconds;else alignedAt=null;
var commit=sawFull&&alignedAt is {} aligned&&clock.Elapsed.TotalSeconds-aligned>=.3;
await Axes(frame,late?aim:default,1);
commitRequested|=commit;
if(commit&&fsd.GetProperty("throttle").GetDouble()>=.999)fullThrottleBeforeJump=true;
}
if(sawCharging)await Fsd(frame,"keep");
}
await Task.Delay(40);
}
catch(InvalidOperationException error)when(error.Message=="Navigation reference frame expired; read fresh telemetry and retry")
{await PauseTelemetry(error.Message);}
}
if(!sawCharging||!sawFull||mode=="jump"&&(!arrived||!fullThrottleBeforeJump))throw new InvalidOperationException("Requested hyperspace acceptance did not complete.");
}
catch(Exception error){failure=error.ToString();}
finally
{
if(!released)try{await Release();}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;}
}
var passed=failure is null&&released&&sawCharging&&sawFull&&(mode=="hold"?!sawJump:arrived&&fullThrottleBeforeJump);
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{mode,pid,hash,target,destination,startedAt,journal,journalOffset,frames,peak,sawCharging,sawFull,sawJump,fullThrottleBeforeJump,released,arrived,arrival,passed,failure,screenUse=false,osInputSent=false,completeAutomaticNavigationAccepted=false},new JsonSerializerOptions{WriteIndented=true}));
Console.WriteLine(JsonSerializer.Serialize(new{mode,passed,target,destination,frames,peak,sawCharging,sawFull,sawJump,fullThrottleBeforeJump,released,arrived,failure}));
return passed;
}
}
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
/// <summary>A bounded, explicitly started one-hop acceptance. It never claims complete navigation acceptance.</summary>
internal static class HyperspaceAcceptance
{
private static IEnumerable<JsonElement> JournalEvents(string path)
{
using var stream=new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.ReadWrite|FileShare.Delete);
using var reader=new StreamReader(stream);
while(reader.ReadLine() is {} line)
{
// A writer may be appending the last event; use only complete JSON objects.
if(!line.TrimEnd().EndsWith('}'))continue;
using var document=JsonDocument.Parse(line);yield return document.RootElement.Clone();
}
}
public static async Task<bool> RunAsync(int pid,string profilePath,string reportPath,string mode,string? explicitDestination=null)
{
if(mode is not ("hold" or "jump"))throw new ArgumentException("Expected hold or jump.");
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new evidence path.");
using var profile=JsonDocument.Parse(await File.ReadAllTextAsync(profilePath));
var image=profile.RootElement.GetProperty("image").GetString()!;
var hash=profile.RootElement.GetProperty("sha256").GetString();
await using(var file=File.OpenRead(image))if(Convert.ToHexStringLower(await SHA256.HashDataAsync(file))!=hash)throw new InvalidDataException("Game build changed.");
using var game=Process.GetProcessById(pid);var started=game.StartTime.ToUniversalTime().Ticks;
if(!string.Equals(game.MainModule?.FileName,image,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("Selected game changed.");
var settingsPath=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json");
var settings=JsonSerializer.Deserialize<ToolboxSettings>(await File.ReadAllTextAsync(settingsPath),ToolboxSettings.JsonOptions)!;
var destination=explicitDestination??settings.NavigationDestination;
if(string.IsNullOrWhiteSpace(destination))throw new InvalidOperationException("A saved navigation destination is required.");
var journal=Directory.GetFiles(settings.JournalDirectory,"Journal.*.log").OrderBy(File.GetLastWriteTimeUtc).Last();
var startedAt=DateTimeOffset.UtcNow;var journalOffset=new FileInfo(journal).Length;
using var status=JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(settings.JournalDirectory,"Status.json")));
var flags=status.RootElement.GetProperty("Flags").GetUInt64();
var supercruise=(flags&16)!=0;
if((flags&(1UL|1048576UL|2097152UL))!=0||status.RootElement.GetProperty("GuiFocus").GetInt32()!=0||mode=="hold"&&supercruise)throw new InvalidOperationException("Requires undocked controllable space without danger or UI focus.");
var pipe=new NavigationPipeClient(pid);var owner=Guid.NewGuid().ToString("N");var clock=Stopwatch.StartNew();
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
async Task Log(object row){await log.WriteLineAsync(JsonSerializer.Serialize(row));await log.FlushAsync();}
static Vector3d V(JsonElement value,string key){var a=value.GetProperty(key);return new(a[0].GetDouble(),a[1].GetDouble(),a[2].GetDouble());}
var frames=0;var peak=0.0;var sawCharging=false;var sawFull=false;var sawJump=false;var fullThrottleBeforeJump=false;var commitRequested=false;var chargeRequested=false;var released=false;var arrived=false;
double? filledAt=null,alignedAt=null,jumpAt=null,invalidTargetAt=null,telemetryGapAt=null;string? failure=null;ulong target=0;JsonElement? arrival=null;
async Task Axes(JsonElement frame,AttitudeInput aim,double throttle)
{
// Holding at rest does not need a throttle override; retain the game's ordinary input mode.
if(mode=="hold")throttle=-1;
await pipe.CallAsync(new{op="calibrate.axes",owner,sequence=frame.GetProperty("sequence").GetInt64(),ttlMilliseconds=250,throttle,
axes=new[]{0.0,0,0,aim.Yaw,aim.Pitch,aim.Roll}});
}
async Task Fsd(JsonElement frame,string desired)
{
await pipe.CallAsync(new{op="calibrate.fsd",owner,sequence=frame.GetProperty("sequence").GetInt64(),ttlMilliseconds=250,desired,targetSystemAddress=target,heatCeilingPercent=90});
}
async Task Release()
{
var stop=Stopwatch.StartNew();
while(stop.Elapsed.TotalSeconds<3)
{
var result=await pipe.CallAsync(new{op="release",owner});await Log(new{kind="release",seconds=clock.Elapsed.TotalSeconds,result});
if(result.GetProperty("released").GetBoolean()){released=true;return;}
await Task.Delay(40);
}
throw new TimeoutException("Control release was not acknowledged.");
}
async Task PauseTelemetry(string reason)
{
telemetryGapAt??=clock.Elapsed.TotalSeconds;
await Log(new{kind="telemetry-pause",seconds=clock.Elapsed.TotalSeconds,reason});
await Release();
if(clock.Elapsed.TotalSeconds-telemetryGapAt>2)throw new InvalidOperationException("Flight telemetry remained unavailable for more than two seconds.");
await Task.Delay(50);
}
try
{
var hello=await pipe.CallAsync(new{op="describe"});
if(hello.GetProperty("version").GetString()!="0.3.1-dev"||hello.GetProperty("gameSha256").GetString()!=hash||hello.GetProperty("processId").GetInt32()!=pid)throw new InvalidDataException("Native acceptance protocol mismatch.");
var initial=await pipe.CallAsync(new{op="read"});var fsd=await pipe.CallAsync(new{op="fsd.describe"});
if(!initial.GetProperty("localPlayerShipVerified").GetBoolean()||fsd.GetProperty("entityId").GetString()!=initial.GetProperty("entityId").GetString())throw new InvalidDataException("Pose and FSD must belong to the original game's local PlayerShip reference.");
if(!supercruise&&initial.GetProperty("speedMetresPerSecond").GetDouble()>=1||initial.GetProperty("heatPercent").GetDouble()>=(supercruise?82:60)||fsd.GetProperty("state").GetInt32()!=2||!fsd.GetProperty("fresh").GetBoolean()||!fsd.GetProperty("targetValid").GetBoolean())throw new InvalidOperationException("Ship/target/FSD initial conditions are not ready.");
target=ulong.Parse(fsd.GetProperty("targetAddress").GetString()!);
JsonElement? locked=null;
foreach(var row in JournalEvents(journal))
{
if(row.GetProperty("event").GetString()=="FSDTarget")locked=row;
}
if(locked is not {} selected||selected.GetProperty("SystemAddress").GetUInt64()!=target||!string.Equals(selected.GetProperty("Name").GetString(),destination,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("The actual game target does not match the saved destination.");
var geometry=await pipe.CallAsync(new{op="geometry.describe"});
var position=V(initial,"position");var ray=V(fsd,"targetDirection").Unit();var along=Vector3d.Dot(position,ray);
var clearance=along<0?Math.Sqrt(Math.Max(0,position.Length*position.Length-along*along)):position.Length;
if(!geometry.GetProperty("fresh").GetBoolean()||geometry.GetProperty("bodyKind").GetInt32()!=1||clearance<=geometry.GetProperty("escapeRadius").GetDouble()*1.2)throw new InvalidOperationException("This one-hop test requires an already unobscured stellar departure.");
if(supercruise&&Vector3d.Dot(position.Unit(),ray)<.95)throw new InvalidOperationException("This bounded SC acceptance only permits an outward departure.");
var escapeRadius=geometry.GetProperty("escapeRadius").GetDouble();
await Log(new{kind="preflight",mode,supercruise,startedAt,pid,target,destination,locked,initial,fsd,geometry,clearance});
// An explicit new run acquires its own lease; current physical input still cancels it natively.
await Axes(initial,default,mode=="hold"?-1:0);await Task.Delay(50);
var phase=mode=="jump"?"prealign":"charge";
while(clock.Elapsed.TotalSeconds<300)
{
try
{
using var current=Process.GetProcessById(pid);
if(current.StartTime.ToUniversalTime().Ticks!=started)throw new InvalidOperationException("Process identity changed.");
if(sawJump)
{
foreach(var e in JournalEvents(journal))
{
if(e.GetProperty("event").GetString()=="FSDJump"&&e.GetProperty("timestamp").GetDateTimeOffset()>=startedAt.AddSeconds(-1)&&e.GetProperty("SystemAddress").GetUInt64()==target){arrival=e.Clone();arrived=true;break;}
}
if(arrived)break;
if(clock.Elapsed.TotalSeconds-jumpAt>75)throw new TimeoutException("No actual Journal FSDJump arrival after native jump.");
await Task.Delay(200);continue;
}
JsonElement frame;
try{frame=await pipe.CallAsync(new{op="read"});fsd=await pipe.CallAsync(new{op="fsd.describe"});}
catch(InvalidOperationException error)when(error.Message=="Native player telemetry unavailable or stale")
{await PauseTelemetry(error.Message);continue;}
frames++;
peak=Math.Max(peak,frame.GetProperty("heatPercent").GetDouble());
var state=fsd.GetProperty("state").GetInt32();var charge=fsd.GetProperty("clock").GetDouble();var duration=fsd.GetProperty("duration").GetDouble();
if(state==4)
{
sawFull|=duration>0&&charge>=duration;
await Log(new{kind="commit-observed",seconds=clock.Elapsed.TotalSeconds,frame,fsd});
fullThrottleBeforeJump|=commitRequested&&fsd.GetProperty("throttle").GetDouble()>=.999;
if(mode!="jump"||!fullThrottleBeforeJump)failure="Unexpected jump before verified full throttle.";
sawJump=true;jumpAt=clock.Elapsed.TotalSeconds;await Release();continue;
}
if(frame.GetProperty("playerOverride").GetBoolean())
{await Log(new{kind="guard-observation",seconds=clock.Elapsed.TotalSeconds,frame,fsd});throw new InvalidOperationException("Flight state became stale or was overridden.");}
if(!frame.GetProperty("inGame").GetBoolean()||!fsd.GetProperty("fresh").GetBoolean())
{await Log(new{kind="transient-observation",seconds=clock.Elapsed.TotalSeconds,frame,fsd});await PauseTelemetry("Current ship or FSD sample is temporarily unavailable");continue;}
telemetryGapAt=null;
var observedTarget=ulong.Parse(fsd.GetProperty("targetAddress").GetString()!);
if(observedTarget!=0&&observedTarget!=target)
{await Log(new{kind="target-changed",seconds=clock.Elapsed.TotalSeconds,frame,fsd});throw new InvalidOperationException("The locked destination changed.");}
if(!fsd.GetProperty("targetValid").GetBoolean())
{
invalidTargetAt??=clock.Elapsed.TotalSeconds;
await Log(new{kind="target-temporarily-unavailable",seconds=clock.Elapsed.TotalSeconds,frame,fsd});
await Release();
if(clock.Elapsed.TotalSeconds-invalidTargetAt>2)throw new InvalidOperationException("The destination telemetry remained unavailable.");
alignedAt=null;await Task.Delay(50);continue;
}
invalidTargetAt=null;
if(released){released=false;if(state==2&&chargeRequested){sawCharging=false;chargeRequested=false;filledAt=null;sawFull=false;phase=frame.GetProperty("heatPercent").GetDouble()>=60?"cool":"prealign";}}
var aim=AttitudeController.Aim(V(frame,"forward"),V(frame,"up"),V(fsd,"targetDirection"),.6);
await Log(new{kind="sample",seconds=clock.Elapsed.TotalSeconds,phase,aim,frame,fsd});
if(peak>=89.5)throw new InvalidOperationException("Reached the bounded thermal ceiling; cancelling charging.");
if(phase=="cool")
{
await Axes(frame,default,0);
if(frame.GetProperty("heatPercent").GetDouble()<55)phase="prealign";
}
else if(phase=="prealign")
{
await Axes(frame,aim,0);
if(aim.ErrorDegrees<=8)phase=supercruise&&frame.GetProperty("heatPercent").GetDouble()>=55?"escape":"charge-start";
}
else if(phase=="escape")
{
await Axes(frame,default,1);
if(frame.GetProperty("heatPercent").GetDouble()<55&&V(frame,"position").Length>=escapeRadius*3)phase="charge-start";
}
else if(phase=="charge-start")
{
// Full throttle and charge are requested together. Current speed need not
// have reached its maximum; the original FSD still enforces its own speed check.
await Axes(frame,default,1);
phase="charge";await Fsd(frame,"beginHyperspace");chargeRequested=true;
await Log(new{kind="full-throttle-and-charge-requested",seconds=clock.Elapsed.TotalSeconds,frame,fsd});
}
else
{
if(mode=="jump"&&fsd.GetProperty("throttle").GetDouble()>=.999)
{
fullThrottleBeforeJump=true;
}
if(!sawCharging&&state==2)
{
if(!chargeRequested){await Fsd(frame,"beginHyperspace");chargeRequested=true;}
else if(fsd.GetProperty("beginRejected").GetBoolean())throw new InvalidOperationException("Original game eligibility rejected hyperspace.");
}
if(state==3){sawCharging=true;}
if(sawCharging&&state==2)throw new InvalidOperationException("Original charging stopped before acceptance.");
if(state==3&&duration>0&&charge>=duration){sawFull=true;filledAt??=clock.Elapsed.TotalSeconds;}
if(mode=="hold")
{
await Axes(frame,default,0);
if(filledAt is {} filled&&clock.Elapsed.TotalSeconds-filled>=1){await Fsd(frame,"cancel");await Release();break;}
}
else
{
var late=state==3&&duration>0&&charge>=duration-4;
if(late&&aim.ErrorDegrees<.7)alignedAt??=clock.Elapsed.TotalSeconds;else alignedAt=null;
var commit=sawFull&&alignedAt is {} aligned&&clock.Elapsed.TotalSeconds-aligned>=.3;
await Axes(frame,late?aim:default,1);
commitRequested|=commit;
if(commit&&fsd.GetProperty("throttle").GetDouble()>=.999)fullThrottleBeforeJump=true;
}
if(sawCharging)await Fsd(frame,"keep");
}
await Task.Delay(40);
}
catch(InvalidOperationException error)when(error.Message=="Navigation reference frame expired; read fresh telemetry and retry")
{await PauseTelemetry(error.Message);}
}
if(!sawCharging||!sawFull||mode=="jump"&&(!arrived||!fullThrottleBeforeJump))throw new InvalidOperationException("Requested hyperspace acceptance did not complete.");
}
catch(Exception error){failure=error.ToString();}
finally
{
if(!released)try{await Release();}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;}
}
var passed=failure is null&&released&&sawCharging&&sawFull&&(mode=="hold"?!sawJump:arrived&&fullThrottleBeforeJump);
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{mode,pid,hash,target,destination,startedAt,journal,journalOffset,frames,peak,sawCharging,sawFull,sawJump,fullThrottleBeforeJump,released,arrived,arrival,passed,failure,screenUse=false,osInputSent=false,completeAutomaticNavigationAccepted=false},new JsonSerializerOptions{WriteIndented=true}));
Console.WriteLine(JsonSerializer.Serialize(new{mode,passed,target,destination,frames,peak,sawCharging,sawFull,sawJump,fullThrottleBeforeJump,released,arrived,failure}));
return passed;
}
}