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(); } }