using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
internal static class DirectNavigationAcceptance
{
public static async Task<bool> RunAsync(int pid,string profilePath,string reportPath,string destination,string? catalogPath=null,double? reserveOverride=null)
{
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new acceptance 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 input=File.OpenRead(image))if(Convert.ToHexStringLower(await SHA256.HashDataAsync(input))!=hash)throw new InvalidDataException("Game build changed.");
using var game=Process.GetProcessById(pid);if(!string.Equals(game.MainModule?.FileName,image,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("Game instance changed.");
var processStarted=game.StartTime.ToUniversalTime();
var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
NavWaypoint? target=null;string? targetSource=null;
foreach(var path in Directory.GetFiles(settings.JournalDirectory,"Journal.*.log").OrderBy(File.GetLastWriteTimeUtc))
{
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)
{
if(!line.TrimEnd().EndsWith('}'))continue;
using var doc=JsonDocument.Parse(line);var row=doc.RootElement;
if(!row.TryGetProperty("event",out var kind)||kind.GetString() is not ("Location" or "FSDJump"))continue;
if(!row.TryGetProperty("StarSystem",out var name)||!name.GetString()!.Equals(destination,StringComparison.OrdinalIgnoreCase))continue;
if(!row.TryGetProperty("StarPos",out var p)||!row.TryGetProperty("SystemAddress",out var id))continue;
target=new(id.GetInt64(),name.GetString()!,new(p[0].GetDouble(),p[1].GetDouble(),p[2].GetDouble()),"Unknown");targetSource=path;
}
}
if(target is null&&catalogPath is not null)
{
using var catalog=JsonDocument.Parse(await File.ReadAllTextAsync(catalogPath));var row=catalog.RootElement;
if(row.GetProperty("name").GetString()!=destination||row.GetProperty("id64").GetInt64()<=0)throw new InvalidDataException("Destination catalogue identity mismatch.");
var c=row.GetProperty("coords");var starClass=row.GetProperty("primaryStar").GetProperty("type").GetString()!;
target=new(row.GetProperty("id64").GetInt64(),destination,new(c.GetProperty("x").GetDouble(),c.GetProperty("y").GetDouble(),c.GetProperty("z").GetDouble()),starClass[..1]);targetSource=Path.GetFullPath(catalogPath);
}
if(target is null)throw new InvalidOperationException("Acceptance destination needs an observed Journal or explicitly recorded catalogue identity and galactic position.");
var configuredReserve=reserveOverride??settings.NavigationFuelReserve;
if(!double.IsFinite(configuredReserve)||configuredReserve<0||configuredReserve>100)throw new ArgumentException("Invalid acceptance reserve.");
var started=DateTimeOffset.UtcNow;var frames=0;var peak=0d;var charged=false;var commit=false;var thermalEscape=false;var orbit=false;var scoop=false;var scEntry=false;var stop=false;string? failure=null;
bool? initialObscured=null,initialFuelShort=null;var peakFuel=0d;var initialBudget=0d;var illegalHyperspaceCharge=false;
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
var logGate=new object();
void Log(object row){lock(logGate){log.WriteLine(JsonSerializer.Serialize(row, new JsonSerializerOptions{PropertyNamingPolicy=JsonNamingPolicy.CamelCase}));log.Flush();}}
await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync();
var initial=journal.State;var driver=new NativeFlightControlDriver(pid,hash,()=>journal.State,()=>configuredReserve);
var runner=new NavigationRunner(()=>journal.State);var progress=new List<NavigationProgress>();
runner.Progress+=p=>{progress.Add(p);Log(new{kind="progress",at=DateTimeOffset.UtcNow,progress=p});if(p.State!=NavigationRunState.Running)Console.WriteLine(JsonSerializer.Serialize(p));};
driver.Observed+=(observation,decision)=>
{
var f=observation.Frame;frames++;peak=Math.Max(peak,f.HeatPercent??0);
if(f.CurrentStar is {} star)
{
var obscured=FlightGeometry.Obscures(star,f.Position,f.TargetPosition);initialObscured??=obscured;
initialFuelShort??=f.FuelMain<f.FuelNeededToNextRefuel;if(initialBudget==0)initialBudget=f.FuelNeededToNextRefuel??0;
illegalHyperspaceCharge|=decision?.BeginCharging==true&&!decision.EnterSupercruise&&(obscured||f.NativeAllowsCharging==false||f.FuelMain<f.FuelNeededToNextRefuel);
}
peakFuel=Math.Max(peakFuel,f.FuelMain??0);
charged|=f.Fsd is FsdPhase.Charging or FsdPhase.Charged;commit|=decision?.PermitJump==true;
thermalEscape|=decision?.State is "先脱离再充能" or "散热脱离";orbit|=decision?.State.Contains("绕") ==true;scoop|=f.ScoopActive;scEntry|=decision?.EnterSupercruise==true;
Log(new{kind="frame",at=DateTimeOffset.UtcNow,frame=f,decision,thermal=observation.Thermal});
};
journal.JournalEvent+=e=>{var kind=e.Data.GetProperty("event").GetString();if(kind is "StartJump" or "FSDJump" or "SupercruiseEntry" or "FuelScoop" or "HeatWarning" or "HeatDamage")Log(new{kind="actual-journal",sourceSequence=e.Sequence,e.ObservedAt,data=e.Data});};
Log(new{kind="preflight",pid,processStarted,hash,target,targetSource,started,configuredReserve,initialFuel=initial.Status?.FuelMain,initialSystem=initial.Navigation.CurrentSystemAddress,initialName=initial.SystemName,initialFlags=initial.Status?.Flags,planOrigin="Explicit direct single-hop plan from recorded coordinates; native target acknowledgement required; no NavRoute or Journal writes"});
try
{
using var deadline=new CancellationTokenSource(TimeSpan.FromMinutes(12));
using var stopMonitor=new Timer(_=>{if(File.Exists(reportPath+".stop"))runner.Stop();},null,100,100);
await runner.RunAsync(target.Name,driver,deadline.Token);
using var current=Process.GetProcessById(pid);if(current.StartTime.ToUniversalTime()!=processStarted)throw new InvalidOperationException("Game process changed during acceptance.");
stop=true;
if(runner.State!=NavigationRunState.Completed)throw new InvalidOperationException(progress.LastOrDefault()?.Message??"Core navigation did not complete.");
if(journal.State.Navigation.CurrentSystemAddress!=target.SystemAddress||journal.State.Navigation.LastJumpAt<started.AddSeconds(-1))throw new InvalidOperationException("No new actual Journal arrival at the intended system.");
if(!charged||!commit)throw new InvalidOperationException("Actual charge and explicit jump commit were not both observed.");
if(illegalHyperspaceCharge)throw new InvalidOperationException("A hyperspace charge was requested before its geometric, fuel or thermal gate passed.");
if(initialObscured==true&&!orbit)throw new InvalidOperationException("The initial stellar obstruction did not exercise automatic avoidance.");
if(initialFuelShort==true&&(!scoop||peakFuel<initialBudget))throw new InvalidOperationException("Actual scoop and fuel-budget completion were not observed.");
}
catch(Exception error){failure=error.ToString();}
finally{try{await driver.ReleaseAsync();}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;}}
var passed=failure is null&&stop&&peak<100&&journal.State.Navigation.CurrentSystemAddress==target.SystemAddress;
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{pid,processStarted,hash,target,targetSource,started,completedAt=DateTimeOffset.UtcNow,frames,peak,charged,commit,thermalEscape,orbit,scoop,scEntry,
configuredReserve,initialObscured,initialFuelShort,initialBudget,peakFuel,illegalHyperspaceCharge,initialFuel=initial.Status?.FuelMain,finalFuel=journal.State.Status?.FuelMain,actualArrivalAt=journal.State.Navigation.LastJumpAt,actualSystem=journal.State.Navigation.CurrentSystemAddress,finalFlags=journal.State.Status?.Flags,state=runner.State,passed,failure,
coreRunnerUsed=true,productionNativeDriverUsed=true,screenUse=false,osInputSent=false,navRouteWrites=0,journalWrites=0},new JsonSerializerOptions{WriteIndented=true}));
Console.WriteLine(JsonSerializer.Serialize(new{passed,frames,peak,charged,commit,thermalEscape,orbit,scoop,scEntry,state=runner.State,failure}));return passed;
}
}
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
internal static class DirectNavigationAcceptance
{
public static async Task<bool> RunAsync(int pid,string profilePath,string reportPath,string destination,string? catalogPath=null,double? reserveOverride=null)
{
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new acceptance 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 input=File.OpenRead(image))if(Convert.ToHexStringLower(await SHA256.HashDataAsync(input))!=hash)throw new InvalidDataException("Game build changed.");
using var game=Process.GetProcessById(pid);if(!string.Equals(game.MainModule?.FileName,image,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("Game instance changed.");
var processStarted=game.StartTime.ToUniversalTime();
var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
NavWaypoint? target=null;string? targetSource=null;
foreach(var path in Directory.GetFiles(settings.JournalDirectory,"Journal.*.log").OrderBy(File.GetLastWriteTimeUtc))
{
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)
{
if(!line.TrimEnd().EndsWith('}'))continue;
using var doc=JsonDocument.Parse(line);var row=doc.RootElement;
if(!row.TryGetProperty("event",out var kind)||kind.GetString() is not ("Location" or "FSDJump"))continue;
if(!row.TryGetProperty("StarSystem",out var name)||!name.GetString()!.Equals(destination,StringComparison.OrdinalIgnoreCase))continue;
if(!row.TryGetProperty("StarPos",out var p)||!row.TryGetProperty("SystemAddress",out var id))continue;
target=new(id.GetInt64(),name.GetString()!,new(p[0].GetDouble(),p[1].GetDouble(),p[2].GetDouble()),"Unknown");targetSource=path;
}
}
if(target is null&&catalogPath is not null)
{
using var catalog=JsonDocument.Parse(await File.ReadAllTextAsync(catalogPath));var row=catalog.RootElement;
if(row.GetProperty("name").GetString()!=destination||row.GetProperty("id64").GetInt64()<=0)throw new InvalidDataException("Destination catalogue identity mismatch.");
var c=row.GetProperty("coords");var starClass=row.GetProperty("primaryStar").GetProperty("type").GetString()!;
target=new(row.GetProperty("id64").GetInt64(),destination,new(c.GetProperty("x").GetDouble(),c.GetProperty("y").GetDouble(),c.GetProperty("z").GetDouble()),starClass[..1]);targetSource=Path.GetFullPath(catalogPath);
}
if(target is null)throw new InvalidOperationException("Acceptance destination needs an observed Journal or explicitly recorded catalogue identity and galactic position.");
var configuredReserve=reserveOverride??settings.NavigationFuelReserve;
if(!double.IsFinite(configuredReserve)||configuredReserve<0||configuredReserve>100)throw new ArgumentException("Invalid acceptance reserve.");
var started=DateTimeOffset.UtcNow;var frames=0;var peak=0d;var charged=false;var commit=false;var thermalEscape=false;var orbit=false;var scoop=false;var scEntry=false;var stop=false;string? failure=null;
bool? initialObscured=null,initialFuelShort=null;var peakFuel=0d;var initialBudget=0d;var illegalHyperspaceCharge=false;
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
var logGate=new object();
void Log(object row){lock(logGate){log.WriteLine(JsonSerializer.Serialize(row, new JsonSerializerOptions{PropertyNamingPolicy=JsonNamingPolicy.CamelCase}));log.Flush();}}
await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync();
var initial=journal.State;var driver=new NativeFlightControlDriver(pid,hash,()=>journal.State,()=>configuredReserve);
var runner=new NavigationRunner(()=>journal.State);var progress=new List<NavigationProgress>();
runner.Progress+=p=>{progress.Add(p);Log(new{kind="progress",at=DateTimeOffset.UtcNow,progress=p});if(p.State!=NavigationRunState.Running)Console.WriteLine(JsonSerializer.Serialize(p));};
driver.Observed+=(observation,decision)=>
{
var f=observation.Frame;frames++;peak=Math.Max(peak,f.HeatPercent??0);
if(f.CurrentStar is {} star)
{
var obscured=FlightGeometry.Obscures(star,f.Position,f.TargetPosition);initialObscured??=obscured;
initialFuelShort??=f.FuelMain<f.FuelNeededToNextRefuel;if(initialBudget==0)initialBudget=f.FuelNeededToNextRefuel??0;
illegalHyperspaceCharge|=decision?.BeginCharging==true&&!decision.EnterSupercruise&&(obscured||f.NativeAllowsCharging==false||f.FuelMain<f.FuelNeededToNextRefuel);
}
peakFuel=Math.Max(peakFuel,f.FuelMain??0);
charged|=f.Fsd is FsdPhase.Charging or FsdPhase.Charged;commit|=decision?.PermitJump==true;
thermalEscape|=decision?.State is "先脱离再充能" or "散热脱离";orbit|=decision?.State.Contains("绕") ==true;scoop|=f.ScoopActive;scEntry|=decision?.EnterSupercruise==true;
Log(new{kind="frame",at=DateTimeOffset.UtcNow,frame=f,decision,thermal=observation.Thermal});
};
journal.JournalEvent+=e=>{var kind=e.Data.GetProperty("event").GetString();if(kind is "StartJump" or "FSDJump" or "SupercruiseEntry" or "FuelScoop" or "HeatWarning" or "HeatDamage")Log(new{kind="actual-journal",sourceSequence=e.Sequence,e.ObservedAt,data=e.Data});};
Log(new{kind="preflight",pid,processStarted,hash,target,targetSource,started,configuredReserve,initialFuel=initial.Status?.FuelMain,initialSystem=initial.Navigation.CurrentSystemAddress,initialName=initial.SystemName,initialFlags=initial.Status?.Flags,planOrigin="Explicit direct single-hop plan from recorded coordinates; native target acknowledgement required; no NavRoute or Journal writes"});
try
{
using var deadline=new CancellationTokenSource(TimeSpan.FromMinutes(12));
using var stopMonitor=new Timer(_=>{if(File.Exists(reportPath+".stop"))runner.Stop();},null,100,100);
await runner.RunAsync(target.Name,driver,deadline.Token);
using var current=Process.GetProcessById(pid);if(current.StartTime.ToUniversalTime()!=processStarted)throw new InvalidOperationException("Game process changed during acceptance.");
stop=true;
if(runner.State!=NavigationRunState.Completed)throw new InvalidOperationException(progress.LastOrDefault()?.Message??"Core navigation did not complete.");
if(journal.State.Navigation.CurrentSystemAddress!=target.SystemAddress||journal.State.Navigation.LastJumpAt<started.AddSeconds(-1))throw new InvalidOperationException("No new actual Journal arrival at the intended system.");
if(!charged||!commit)throw new InvalidOperationException("Actual charge and explicit jump commit were not both observed.");
if(illegalHyperspaceCharge)throw new InvalidOperationException("A hyperspace charge was requested before its geometric, fuel or thermal gate passed.");
if(initialObscured==true&&!orbit)throw new InvalidOperationException("The initial stellar obstruction did not exercise automatic avoidance.");
if(initialFuelShort==true&&(!scoop||peakFuel<initialBudget))throw new InvalidOperationException("Actual scoop and fuel-budget completion were not observed.");
}
catch(Exception error){failure=error.ToString();}
finally{try{await driver.ReleaseAsync();}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;}}
var passed=failure is null&&stop&&peak<100&&journal.State.Navigation.CurrentSystemAddress==target.SystemAddress;
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{pid,processStarted,hash,target,targetSource,started,completedAt=DateTimeOffset.UtcNow,frames,peak,charged,commit,thermalEscape,orbit,scoop,scEntry,
configuredReserve,initialObscured,initialFuelShort,initialBudget,peakFuel,illegalHyperspaceCharge,initialFuel=initial.Status?.FuelMain,finalFuel=journal.State.Status?.FuelMain,actualArrivalAt=journal.State.Navigation.LastJumpAt,actualSystem=journal.State.Navigation.CurrentSystemAddress,finalFlags=journal.State.Status?.Flags,state=runner.State,passed,failure,
coreRunnerUsed=true,productionNativeDriverUsed=true,screenUse=false,osInputSent=false,navRouteWrites=0,journalWrites=0},new JsonSerializerOptions{WriteIndented=true}));
Console.WriteLine(JsonSerializer.Serialize(new{passed,frames,peak,charged,commit,thermalEscape,orbit,scoop,scEntry,state=runner.State,failure}));return passed;
}
}