using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
internal static class LocalDepartureAcceptance
{
public static async Task RunAsync(int pid,string exactName,string reportPath,CancellationToken externalToken=default,bool stopAfterDeparture=true)
{
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new report path.");
var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync();var initial=journal.State;var pipe=new NavigationPipeClient(pid);
var hash=(await pipe.CallAsync(new{op="describe"})).GetProperty("gameSha256").GetString()!;
using var source=new NativeCurrentSystemSource(pid,hash,()=>journal.State);var catalogue=await source.ReadLocationsAsync();var row=catalogue.Locations.Single(x=>x.Name==exactName);
if(!initial.Docked||initial.MarketId==row.MarketId||!NavigationAssists.From(initial).AdvancedDocking)throw new InvalidOperationException("Departure acceptance requires a different destination and an enabled advanced docking computer.");
var target=new SystemNavigationTarget(catalogue.SystemAddress,row.WorldAddress,row.Name,SystemTargetKind.Station,row.MarketId){NativeKey=row.Key,NativeKind=row.NativeKind,NativeIdentity=row.Identity};
await using var protection=await FlightTestProtectionScope.ArmAsync(pid);
var driver=new NativeSystemFlightControlDriver(pid,hash,()=>journal.State,new NativeGameInputChannel(pid,hash,Path.GetFullPath("XFEEDToolBox/PluginPackage")));
using var stop=CancellationTokenSource.CreateLinkedTokenSource(externalToken);stop.CancelAfter(TimeSpan.FromMinutes(7));var start=DateTimeOffset.UtcNow;var engaged=false;var prepared=false;var released=false;double maxHeat=0;string? failure=null;
Task? preparation=null;SystemFlightFrame? final=null;
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
try
{
await driver.SelectAsync(target,stop.Token);preparation=driver.PrepareAsync(stop.Token);
while(!preparation.IsCompleted)
{
stop.Token.ThrowIfCancellationRequested();var state=journal.State;
if(state.CurrentShipId!=initial.CurrentShipId||state.CommanderId!=initial.CommanderId||state.LoadGameAt!=initial.LoadGameAt||state.CargoCount!=initial.CargoCount||NavigationSafety.HasExternalThreat(state))
throw new InvalidOperationException("Departure session, ship or reserved cargo changed.");
try
{
var raw=await pipe.CallAsync(new{op="read"},stop.Token);var local=await pipe.CallAsync(new{op="local.read"},stop.Token);
maxHeat=Math.Max(maxHeat,raw.GetProperty("heatPercent").GetDouble());if(maxHeat>=75)throw new InvalidOperationException("Departure heat bound exceeded.");
engaged|=local.GetProperty("docking").GetProperty("nativeLaunchActive").GetBoolean();
await log.WriteLineAsync(JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,state.Docked,raw,local,engaged}));await log.FlushAsync();
}
catch(Exception error)when(NavigationRecovery.CanRetry(error,stop.Token))
{await log.WriteLineAsync(JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,recovering=error.Message}));await log.FlushAsync();}
await Task.Delay(80,stop.Token);
}
await preparation;prepared=true;
while(true)
{
final=await driver.ReadSystemFlightAsync(stop.Token);
if(final.PlayerOverride||final.ExternalThreat||!target.SameIdentity(final.SelectedTarget!))throw new InvalidOperationException("Departure did not retain the requested destination.");
if(!stopAfterDeparture)break;
await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting,"完成离场并停车",null,0),final,target,final.ObservedAt.AddMilliseconds(250),stop.Token);
if(final.Speed<1)break;await Task.Delay(80,stop.Token);
}
if(!engaged||journal.State.Docked||journal.State.Navigation.UndockedAt<start.AddSeconds(-1)||!final.FsdReady)throw new InvalidOperationException("Departure did not prove native launch, real undock and release from mass lock.");
}
catch(Exception error){failure=error.ToString();}
finally
{
stop.Cancel();if(preparation is not null)try{await preparation;}catch{}
try{await driver.ReleaseSystemFlightAsync();released=true;}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;}
}
var passed=prepared&&engaged&&released&&failure is null;
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{pid,start,finished=DateTimeOffset.UtcNow,passed,prepared,engaged,released,maxHeat,final,failure,stopAfterDeparture,tradeOrdersSent=0},ToolboxSettings.JsonOptions));
Console.WriteLine(JsonSerializer.Serialize(new{passed,prepared,engaged,released,maxHeat,failure}));
if(!passed)throw new InvalidOperationException("Production departure acceptance failed.");
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
internal static class LocalDepartureAcceptance
{
public static async Task RunAsync(int pid,string exactName,string reportPath,CancellationToken externalToken=default,bool stopAfterDeparture=true)
{
if(File.Exists(reportPath)||File.Exists(reportPath+".jsonl"))throw new IOException("Use a new report path.");
var settings=ToolboxSettings.Load(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"XFEstudio","XFEEDToolBox","settings.json"));
await using var journal=new JournalMonitor(settings.JournalDirectory);await journal.StartAsync();var initial=journal.State;var pipe=new NavigationPipeClient(pid);
var hash=(await pipe.CallAsync(new{op="describe"})).GetProperty("gameSha256").GetString()!;
using var source=new NativeCurrentSystemSource(pid,hash,()=>journal.State);var catalogue=await source.ReadLocationsAsync();var row=catalogue.Locations.Single(x=>x.Name==exactName);
if(!initial.Docked||initial.MarketId==row.MarketId||!NavigationAssists.From(initial).AdvancedDocking)throw new InvalidOperationException("Departure acceptance requires a different destination and an enabled advanced docking computer.");
var target=new SystemNavigationTarget(catalogue.SystemAddress,row.WorldAddress,row.Name,SystemTargetKind.Station,row.MarketId){NativeKey=row.Key,NativeKind=row.NativeKind,NativeIdentity=row.Identity};
await using var protection=await FlightTestProtectionScope.ArmAsync(pid);
var driver=new NativeSystemFlightControlDriver(pid,hash,()=>journal.State,new NativeGameInputChannel(pid,hash,Path.GetFullPath("XFEEDToolBox/PluginPackage")));
using var stop=CancellationTokenSource.CreateLinkedTokenSource(externalToken);stop.CancelAfter(TimeSpan.FromMinutes(7));var start=DateTimeOffset.UtcNow;var engaged=false;var prepared=false;var released=false;double maxHeat=0;string? failure=null;
Task? preparation=null;SystemFlightFrame? final=null;
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
try
{
await driver.SelectAsync(target,stop.Token);preparation=driver.PrepareAsync(stop.Token);
while(!preparation.IsCompleted)
{
stop.Token.ThrowIfCancellationRequested();var state=journal.State;
if(state.CurrentShipId!=initial.CurrentShipId||state.CommanderId!=initial.CommanderId||state.LoadGameAt!=initial.LoadGameAt||state.CargoCount!=initial.CargoCount||NavigationSafety.HasExternalThreat(state))
throw new InvalidOperationException("Departure session, ship or reserved cargo changed.");
try
{
var raw=await pipe.CallAsync(new{op="read"},stop.Token);var local=await pipe.CallAsync(new{op="local.read"},stop.Token);
maxHeat=Math.Max(maxHeat,raw.GetProperty("heatPercent").GetDouble());if(maxHeat>=75)throw new InvalidOperationException("Departure heat bound exceeded.");
engaged|=local.GetProperty("docking").GetProperty("nativeLaunchActive").GetBoolean();
await log.WriteLineAsync(JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,state.Docked,raw,local,engaged}));await log.FlushAsync();
}
catch(Exception error)when(NavigationRecovery.CanRetry(error,stop.Token))
{await log.WriteLineAsync(JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,recovering=error.Message}));await log.FlushAsync();}
await Task.Delay(80,stop.Token);
}
await preparation;prepared=true;
while(true)
{
final=await driver.ReadSystemFlightAsync(stop.Token);
if(final.PlayerOverride||final.ExternalThreat||!target.SameIdentity(final.SelectedTarget!))throw new InvalidOperationException("Departure did not retain the requested destination.");
if(!stopAfterDeparture)break;
await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting,"完成离场并停车",null,0),final,target,final.ObservedAt.AddMilliseconds(250),stop.Token);
if(final.Speed<1)break;await Task.Delay(80,stop.Token);
}
if(!engaged||journal.State.Docked||journal.State.Navigation.UndockedAt<start.AddSeconds(-1)||!final.FsdReady)throw new InvalidOperationException("Departure did not prove native launch, real undock and release from mass lock.");
}
catch(Exception error){failure=error.ToString();}
finally
{
stop.Cancel();if(preparation is not null)try{await preparation;}catch{}
try{await driver.ReleaseSystemFlightAsync();released=true;}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;}
}
var passed=prepared&&engaged&&released&&failure is null;
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{pid,start,finished=DateTimeOffset.UtcNow,passed,prepared,engaged,released,maxHeat,final,failure,stopAfterDeparture,tradeOrdersSent=0},ToolboxSettings.JsonOptions));
Console.WriteLine(JsonSerializer.Serialize(new{passed,prepared,engaged,released,maxHeat,failure}));
if(!passed)throw new InvalidOperationException("Production departure acceptance failed.");
}
}