using System.Text.Json;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Views;
namespace XFEEDToolBox;
public partial class App
{
// Explicit developer acceptance command. Exercises the same viewmodel commands as the page;
// no screen capture, OS input, fabricated Journal events, or automatic invocation at normal launch.
private static async Task RunNavigationAcceptanceAsync(string[] args,string reportPath)
{
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new navigation acceptance report path.");
var destination=GetArgument(args,"--accept-destination")??throw new ArgumentException("An explicit acceptance destination is required.");
var reserve=double.Parse(GetArgument(args,"--accept-reserve")??"0.5",System.Globalization.CultureInfo.InvariantCulture);
var start=DateTimeOffset.UtcNow;var passed=false;string? failure=null;long? expectedSystem=null;var ready=false;var enabled=false;var frames=0;var peak=0d;
var gameProcessId=0;DateTime? processStart=null;var gate=new object();
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
void Log(object value){lock(gate){log.WriteLine(JsonSerializer.Serialize(value,new JsonSerializerOptions{PropertyNamingPolicy=JsonNamingPolicy.CamelCase}));log.Flush();}}
void Progress(NavigationProgress progress)=>Log(new{kind="application-progress",at=DateTimeOffset.UtcNow,progress});
void Inputs(GameInputState state)=>Log(new{kind="application-inputs",at=DateTimeOffset.UtcNow,state,statusFlags=Runtime.Player.Status?.Flags});
void ChargeInput(NativeChargeRequest request)=>Log(new{kind="application-charge-input",at=DateTimeOffset.UtcNow,request});
void Observation(FlightObservation observation,FlightDecision? decision)
{frames++;peak=Math.Max(peak,observation.Frame.HeatPercent??0);var player=Runtime.Player;
var raw=(Runtime.FlightDriver as NativeFlightControlDriver)?.LastRawTelemetry;
int? Snapshot()=>raw is {} r&&r.TryGetProperty("rawFsdState",out var value)?value.GetInt32():null;
Log(new{kind="application-frame",at=DateTimeOffset.UtcNow,frame=observation.Frame,decision,applied=decision is null?null:(Runtime.FlightDriver as NativeFlightControlDriver)?.LastAppliedControl,
snapshotFsdState=Snapshot(),
journalContext=new{player.InGame,player.Docked,player.InDanger,player.StatusAt,statusFlags=player.Status?.Flags}});}
NativeFlightControlDriver? native=null;
XFEEDToolBox.Core.Session.NativeFlightTestProtection? protection=null;
Runtime.Navigator.Progress+=Progress;
try
{
ViewModel.DemoMode=false;ViewModel.NavigationDestination=destination;ViewModel.NavigationFuelReserve=reserve;
AppShellPage.Current!.Navigate(typeof(NavigationPage));
await ViewModel.PlanNavigationCommand.ExecuteAsync(null);await ViewModel.CheckNavigationCommand.ExecuteAsync(null);await Task.Delay(100);
ready=ViewModel.PluginNavigationReady;enabled=ViewModel.StartNavigationCommand.CanExecute(null);
expectedSystem=ViewModel.NavigationLegs.LastOrDefault()?.Record.SystemAddress;
native=Runtime.FlightDriver as NativeFlightControlDriver;
if(!ready||!enabled||expectedSystem is null||native is null)throw new InvalidOperationException("Application navigation command is not ready: "+ViewModel.NavigationStatus+" / "+ViewModel.NavigationNativeStatus);
gameProcessId=native.ProcessId;using(var process=System.Diagnostics.Process.GetProcessById(gameProcessId))processStart=process.StartTime.ToUniversalTime();
var hello=await new NavigationPipeClient(gameProcessId).CallAsync(new{op="describe"});
protection=await XFEEDToolBox.Core.Session.NativeFlightTestProtection.ArmAsync(gameProcessId,hello.GetProperty("gameSha256").GetString()!,
new Services.WindowsGameSessionDriver(()=>Runtime.Player,()=>Runtime.Settings.PluginGameExecutable),()=>Runtime.Player,Runtime.Navigator.Stop);
Runtime.Journal.JournalEvent+=protection.ObserveJournal;
native.Observed+=Observation;
native.GameInputsObserved+=Inputs;
native.OriginalChargeRequested+=ChargeInput;
Log(new{kind="application-preflight",start,destination,reserve,expectedSystem,ready,enabled,gameProcessId,processStart,Runtime.DataDirectory,initialSystem=Runtime.Player.Navigation.CurrentSystemAddress});
using var timer=new Timer(_=>{if(File.Exists(reportPath+".stop")||DateTimeOffset.UtcNow-start>TimeSpan.FromMinutes(45))Runtime.Navigator.Stop();},null,100,100);
await ViewModel.StartNavigationCommand.ExecuteAsync(null);
using(var process=System.Diagnostics.Process.GetProcessById(gameProcessId))if(process.StartTime.ToUniversalTime()!=processStart)throw new InvalidOperationException("Game process changed during application acceptance.");
passed=Runtime.Navigator.State==NavigationRunState.Completed&&Runtime.Player.Navigation.CurrentSystemAddress==expectedSystem
&&Runtime.Player.Navigation.LastJumpAt>=DateTimeOffset.FromUnixTimeSeconds(start.ToUnixTimeSeconds())&&frames>0;
if(!passed)throw new InvalidOperationException("Application navigation did not reach the expected system: "+ViewModel.NavigationStatus);
}
catch(Exception error){failure=error.ToString();passed=false;}
finally
{
Runtime.Navigator.Progress-=Progress;
if(protection is not null){Runtime.Journal.JournalEvent-=protection.ObserveJournal;await protection.DisposeAsync();}
if(native is not null){native.Observed-=Observation;native.GameInputsObserved-=Inputs;native.OriginalChargeRequested-=ChargeInput;try{await native.ReleaseAsync();}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;passed=false;}}
}
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{start,completedAt=DateTimeOffset.UtcNow,destination,reserve,expectedSystem,gameProcessId,processStart,ready,enabled,frames,peak,
actualSystem=Runtime.Player.Navigation.CurrentSystemAddress,actualArrivalAt=Runtime.Player.Navigation.LastJumpAt,navigationState=Runtime.Navigator.State,passed,failure,
viewModelStartCommandUsed=true,realJournalUsed=true,screenUse=false,osInputSent=false,journalWrites=0,navRouteWrites=0},new JsonSerializerOptions{WriteIndented=true}));
MainWindow.Close();
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Views;
namespace XFEEDToolBox;
public partial class App
{
// Explicit developer acceptance command. Exercises the same viewmodel commands as the page;
// no screen capture, OS input, fabricated Journal events, or automatic invocation at normal launch.
private static async Task RunNavigationAcceptanceAsync(string[] args,string reportPath)
{
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new navigation acceptance report path.");
var destination=GetArgument(args,"--accept-destination")??throw new ArgumentException("An explicit acceptance destination is required.");
var reserve=double.Parse(GetArgument(args,"--accept-reserve")??"0.5",System.Globalization.CultureInfo.InvariantCulture);
var start=DateTimeOffset.UtcNow;var passed=false;string? failure=null;long? expectedSystem=null;var ready=false;var enabled=false;var frames=0;var peak=0d;
var gameProcessId=0;DateTime? processStart=null;var gate=new object();
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
void Log(object value){lock(gate){log.WriteLine(JsonSerializer.Serialize(value,new JsonSerializerOptions{PropertyNamingPolicy=JsonNamingPolicy.CamelCase}));log.Flush();}}
void Progress(NavigationProgress progress)=>Log(new{kind="application-progress",at=DateTimeOffset.UtcNow,progress});
void Inputs(GameInputState state)=>Log(new{kind="application-inputs",at=DateTimeOffset.UtcNow,state,statusFlags=Runtime.Player.Status?.Flags});
void ChargeInput(NativeChargeRequest request)=>Log(new{kind="application-charge-input",at=DateTimeOffset.UtcNow,request});
void Observation(FlightObservation observation,FlightDecision? decision)
{frames++;peak=Math.Max(peak,observation.Frame.HeatPercent??0);var player=Runtime.Player;
var raw=(Runtime.FlightDriver as NativeFlightControlDriver)?.LastRawTelemetry;
int? Snapshot()=>raw is {} r&&r.TryGetProperty("rawFsdState",out var value)?value.GetInt32():null;
Log(new{kind="application-frame",at=DateTimeOffset.UtcNow,frame=observation.Frame,decision,applied=decision is null?null:(Runtime.FlightDriver as NativeFlightControlDriver)?.LastAppliedControl,
snapshotFsdState=Snapshot(),
journalContext=new{player.InGame,player.Docked,player.InDanger,player.StatusAt,statusFlags=player.Status?.Flags}});}
NativeFlightControlDriver? native=null;
XFEEDToolBox.Core.Session.NativeFlightTestProtection? protection=null;
Runtime.Navigator.Progress+=Progress;
try
{
ViewModel.DemoMode=false;ViewModel.NavigationDestination=destination;ViewModel.NavigationFuelReserve=reserve;
AppShellPage.Current!.Navigate(typeof(NavigationPage));
await ViewModel.PlanNavigationCommand.ExecuteAsync(null);await ViewModel.CheckNavigationCommand.ExecuteAsync(null);await Task.Delay(100);
ready=ViewModel.PluginNavigationReady;enabled=ViewModel.StartNavigationCommand.CanExecute(null);
expectedSystem=ViewModel.NavigationLegs.LastOrDefault()?.Record.SystemAddress;
native=Runtime.FlightDriver as NativeFlightControlDriver;
if(!ready||!enabled||expectedSystem is null||native is null)throw new InvalidOperationException("Application navigation command is not ready: "+ViewModel.NavigationStatus+" / "+ViewModel.NavigationNativeStatus);
gameProcessId=native.ProcessId;using(var process=System.Diagnostics.Process.GetProcessById(gameProcessId))processStart=process.StartTime.ToUniversalTime();
var hello=await new NavigationPipeClient(gameProcessId).CallAsync(new{op="describe"});
protection=await XFEEDToolBox.Core.Session.NativeFlightTestProtection.ArmAsync(gameProcessId,hello.GetProperty("gameSha256").GetString()!,
new Services.WindowsGameSessionDriver(()=>Runtime.Player,()=>Runtime.Settings.PluginGameExecutable),()=>Runtime.Player,Runtime.Navigator.Stop);
Runtime.Journal.JournalEvent+=protection.ObserveJournal;
native.Observed+=Observation;
native.GameInputsObserved+=Inputs;
native.OriginalChargeRequested+=ChargeInput;
Log(new{kind="application-preflight",start,destination,reserve,expectedSystem,ready,enabled,gameProcessId,processStart,Runtime.DataDirectory,initialSystem=Runtime.Player.Navigation.CurrentSystemAddress});
using var timer=new Timer(_=>{if(File.Exists(reportPath+".stop")||DateTimeOffset.UtcNow-start>TimeSpan.FromMinutes(45))Runtime.Navigator.Stop();},null,100,100);
await ViewModel.StartNavigationCommand.ExecuteAsync(null);
using(var process=System.Diagnostics.Process.GetProcessById(gameProcessId))if(process.StartTime.ToUniversalTime()!=processStart)throw new InvalidOperationException("Game process changed during application acceptance.");
passed=Runtime.Navigator.State==NavigationRunState.Completed&&Runtime.Player.Navigation.CurrentSystemAddress==expectedSystem
&&Runtime.Player.Navigation.LastJumpAt>=DateTimeOffset.FromUnixTimeSeconds(start.ToUnixTimeSeconds())&&frames>0;
if(!passed)throw new InvalidOperationException("Application navigation did not reach the expected system: "+ViewModel.NavigationStatus);
}
catch(Exception error){failure=error.ToString();passed=false;}
finally
{
Runtime.Navigator.Progress-=Progress;
if(protection is not null){Runtime.Journal.JournalEvent-=protection.ObserveJournal;await protection.DisposeAsync();}
if(native is not null){native.Observed-=Observation;native.GameInputsObserved-=Inputs;native.OriginalChargeRequested-=ChargeInput;try{await native.ReleaseAsync();}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;passed=false;}}
}
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{start,completedAt=DateTimeOffset.UtcNow,destination,reserve,expectedSystem,gameProcessId,processStart,ready,enabled,frames,peak,
actualSystem=Runtime.Player.Navigation.CurrentSystemAddress,actualArrivalAt=Runtime.Player.Navigation.LastJumpAt,navigationState=Runtime.Navigator.State,passed,failure,
viewModelStartCommandUsed=true,realJournalUsed=true,screenUse=false,osInputSent=false,journalWrites=0,navRouteWrites=0},new JsonSerializerOptions{WriteIndented=true}));
MainWindow.Close();
}
}