using System.Diagnostics;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
/// <summary>Bounded SC entry, stationary-throttle observation and ordinary low-speed exit. No trade or docking.</summary>
internal static class LocalCruiseEntryAcceptance
{
public static async Task RunAsync(int pid,string exactName,string reportPath)
{
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 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 initial=journal.State;var catalogue=await source.ReadLocationsAsync();
var row=catalogue.Locations.Single(x=>x.Name==exactName);var businessMarket=row.MarketId;
if(businessMarket is not >0||initial.Docked)throw new InvalidOperationException("Entry fixture requires an undocked ship and an exact station target.");
SystemNavigationTarget MakeTarget(CurrentSystemSnapshot snapshot,SystemLocation item)=>new(snapshot.SystemAddress,item.WorldAddress,item.Name,SystemTargetKind.Station,item.MarketId)
{NativeKey=item.Key,NativeKind=item.NativeKind,NativeIdentity=item.Identity};
var target=MakeTarget(catalogue,row);
FlightTestProtectionScope? protection=null;var start=DateTimeOffset.UtcNow;var watch=Stopwatch.StartNew();var entered=false;var returned=false;var released=false;var rebound=false;
var inputs=0;var enterRequests=0;var exitRequests=0;double maxHeat=0,maxSpeed=0;string? failure=null;SystemFlightFrame? latest=null;DateTimeOffset? enteredAt=null;
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
async Task Record(object value){await log.WriteLineAsync(JsonSerializer.Serialize(value));await log.FlushAsync();}
void ValidateSession()
{
var current=journal.State;
if(!current.InGame||current.CurrentShipId!=initial.CurrentShipId||current.CommanderId!=initial.CommanderId||current.LoadGameAt!=initial.LoadGameAt
||current.Navigation.CurrentSystemAddress!=catalogue.SystemAddress||current.CargoCount!=initial.CargoCount
||current.Inventory.Any(x=>x.Count!=initial.Inventory.Where(y=>y.Key==x.Key).Sum(y=>y.Count)))
throw new InvalidOperationException("Acceptance session, ship, system or reserved cargo changed.");
}
async Task EnsureProtection(CancellationToken token)
{
if(protection?.Stopped!=true)return;
ValidateSession();var nav=journal.State.Navigation;
if(!(nav.SupercruiseEnteredAt>=start.AddSeconds(-1)||nav.SupercruiseExitedAt>=start.AddSeconds(-1)))
throw new InvalidOperationException("Cannot rearm protection without an original SC transition.");
await protection.DisposeAsync();protection=await FlightTestProtectionScope.ArmAsync(pid);rebound=true;
}
var driver=new NativeSystemFlightControlDriver(pid,hash,()=>journal.State,new NativeGameInputChannel(pid,hash,Path.GetFullPath("XFEEDToolBox/PluginPackage")),EnsureProtection);
try
{
protection=await FlightTestProtectionScope.ArmAsync(pid);using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(4));
await driver.SelectAsync(target,timeout.Token);await driver.PrepareAsync(timeout.Token);
if((await driver.ReadSystemFlightAsync(timeout.Token)).Mode!=LocalFlightMode.Normal)throw new InvalidOperationException("Entry acceptance must start in normal space.");
DateTimeOffset? unavailableSince=null;
while(watch.Elapsed<TimeSpan.FromMinutes(3.5))
{
ValidateSession();SystemFlightFrame frame;
try{frame=await driver.ReadSystemFlightAsync(timeout.Token);}
catch(Exception error)when(NavigationRecovery.CanRetry(error,timeout.Token))
{
unavailableSince??=DateTimeOffset.UtcNow;await Record(new{at=DateTimeOffset.UtcNow,recovering=error.Message});
if(DateTimeOffset.UtcNow-unavailableSince>TimeSpan.FromSeconds(30))throw;
await Task.Delay(80,timeout.Token);continue;
}
unavailableSince=null;
latest=frame;maxHeat=Math.Max(maxHeat,frame.HeatPercent??100);maxSpeed=Math.Max(maxSpeed,frame.Speed);
if(frame.PlayerOverride||frame.ExternalThreat||maxHeat>=75)throw new InvalidOperationException("Acceptance heat, threat or manual-override limit exceeded.");
try
{
if(frame.Mode==LocalFlightMode.Supercruise)
{
if(!entered)
{
entered=true;enteredAt=DateTimeOffset.UtcNow;
await File.WriteAllTextAsync(reportPath+".supercruise.json",JsonSerializer.Serialize(new{pid,at=enteredAt,frame,driver.LastTelemetry},ToolboxSettings.JsonOptions));
Console.WriteLine("Original supercruise observed; holding zero throttle for bounded read-only diagnostics.");
}
if(DateTimeOffset.UtcNow-enteredAt<TimeSpan.FromSeconds(90)||frame.Speed>100000)
await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting,"验证超巡并减速",null,0),frame,target,frame.ObservedAt.AddMilliseconds(250),timeout.Token);
else if(exitRequests==0)
{await driver.LeaveSupercruiseSlowlyAsync(frame,timeout.Token);exitRequests++;}
else await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting,"等待低速退出",null,0),frame,target,frame.ObservedAt.AddMilliseconds(250),timeout.Token);
}
else if(entered&&frame.Mode==LocalFlightMode.Normal)
{
await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting,"验证正常空间停车",null,0),frame,target,frame.ObservedAt.AddMilliseconds(250),timeout.Token);
if(exitRequests>0&&frame.Speed<1){returned=true;break;}
}
else if(!entered)
{
if(frame.TargetObscured||frame.Mode==LocalFlightMode.Docked)throw new InvalidOperationException("Entry destination is obscured or ship is docked.");
var direction=(frame.TargetPosition-frame.Position).Unit();var aligned=Vector3d.AngleDegrees(frame.Forward,direction)<3;
var action=aligned&&frame.FsdReady&&enterRequests==0?SystemNavigationAction.EnterSupercruise:SystemNavigationAction.None;
await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.EnteringSupercruise,"验证进入超巡",direction,aligned?1:0,action),frame,target,frame.ObservedAt.AddMilliseconds(250),timeout.Token);
if(action!=SystemNavigationAction.None)enterRequests++;
}
inputs++;await Record(new{at=DateTimeOffset.UtcNow,entered,returned,rebound,inputs,enterRequests,exitRequests,frame,driver.LastTelemetry});
}
catch(Exception error)when(NavigationRecovery.CanRetry(error,timeout.Token))
{await Record(new{at=DateTimeOffset.UtcNow,recovering=error.Message});await driver.SuspendSystemFlightAsync();}
await Task.Delay(50,timeout.Token);
}
if(!entered||!returned)throw new TimeoutException("Bounded SC entry/exit did not complete.");
}
catch(Exception error){failure=error.ToString();}
finally
{
try{await driver.ReleaseSystemFlightAsync();released=true;}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;}
if(protection is not null)await protection.DisposeAsync();
}
var passed=entered&&returned&&released&&failure is null;
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{pid,start,finished=DateTimeOffset.UtcNow,passed,entered,returned,released,rebound,inputs,enterRequests,exitRequests,maxHeat,maxSpeed,latest,failure,
tradeOrdersSent=0,dockingRequestsSent=0,completeNavigationAccepted=false},ToolboxSettings.JsonOptions));
Console.WriteLine(JsonSerializer.Serialize(new{passed,entered,returned,released,rebound,inputs,enterRequests,exitRequests,maxHeat,maxSpeed,failure}));
if(!passed)throw new InvalidOperationException("Local SC entry/exit acceptance failed.");
}
}
using System.Diagnostics;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
/// <summary>Bounded SC entry, stationary-throttle observation and ordinary low-speed exit. No trade or docking.</summary>
internal static class LocalCruiseEntryAcceptance
{
public static async Task RunAsync(int pid,string exactName,string reportPath)
{
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 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 initial=journal.State;var catalogue=await source.ReadLocationsAsync();
var row=catalogue.Locations.Single(x=>x.Name==exactName);var businessMarket=row.MarketId;
if(businessMarket is not >0||initial.Docked)throw new InvalidOperationException("Entry fixture requires an undocked ship and an exact station target.");
SystemNavigationTarget MakeTarget(CurrentSystemSnapshot snapshot,SystemLocation item)=>new(snapshot.SystemAddress,item.WorldAddress,item.Name,SystemTargetKind.Station,item.MarketId)
{NativeKey=item.Key,NativeKind=item.NativeKind,NativeIdentity=item.Identity};
var target=MakeTarget(catalogue,row);
FlightTestProtectionScope? protection=null;var start=DateTimeOffset.UtcNow;var watch=Stopwatch.StartNew();var entered=false;var returned=false;var released=false;var rebound=false;
var inputs=0;var enterRequests=0;var exitRequests=0;double maxHeat=0,maxSpeed=0;string? failure=null;SystemFlightFrame? latest=null;DateTimeOffset? enteredAt=null;
await using var log=new StreamWriter(new FileStream(reportPath+".jsonl",FileMode.CreateNew,FileAccess.Write,FileShare.Read));
async Task Record(object value){await log.WriteLineAsync(JsonSerializer.Serialize(value));await log.FlushAsync();}
void ValidateSession()
{
var current=journal.State;
if(!current.InGame||current.CurrentShipId!=initial.CurrentShipId||current.CommanderId!=initial.CommanderId||current.LoadGameAt!=initial.LoadGameAt
||current.Navigation.CurrentSystemAddress!=catalogue.SystemAddress||current.CargoCount!=initial.CargoCount
||current.Inventory.Any(x=>x.Count!=initial.Inventory.Where(y=>y.Key==x.Key).Sum(y=>y.Count)))
throw new InvalidOperationException("Acceptance session, ship, system or reserved cargo changed.");
}
async Task EnsureProtection(CancellationToken token)
{
if(protection?.Stopped!=true)return;
ValidateSession();var nav=journal.State.Navigation;
if(!(nav.SupercruiseEnteredAt>=start.AddSeconds(-1)||nav.SupercruiseExitedAt>=start.AddSeconds(-1)))
throw new InvalidOperationException("Cannot rearm protection without an original SC transition.");
await protection.DisposeAsync();protection=await FlightTestProtectionScope.ArmAsync(pid);rebound=true;
}
var driver=new NativeSystemFlightControlDriver(pid,hash,()=>journal.State,new NativeGameInputChannel(pid,hash,Path.GetFullPath("XFEEDToolBox/PluginPackage")),EnsureProtection);
try
{
protection=await FlightTestProtectionScope.ArmAsync(pid);using var timeout=new CancellationTokenSource(TimeSpan.FromMinutes(4));
await driver.SelectAsync(target,timeout.Token);await driver.PrepareAsync(timeout.Token);
if((await driver.ReadSystemFlightAsync(timeout.Token)).Mode!=LocalFlightMode.Normal)throw new InvalidOperationException("Entry acceptance must start in normal space.");
DateTimeOffset? unavailableSince=null;
while(watch.Elapsed<TimeSpan.FromMinutes(3.5))
{
ValidateSession();SystemFlightFrame frame;
try{frame=await driver.ReadSystemFlightAsync(timeout.Token);}
catch(Exception error)when(NavigationRecovery.CanRetry(error,timeout.Token))
{
unavailableSince??=DateTimeOffset.UtcNow;await Record(new{at=DateTimeOffset.UtcNow,recovering=error.Message});
if(DateTimeOffset.UtcNow-unavailableSince>TimeSpan.FromSeconds(30))throw;
await Task.Delay(80,timeout.Token);continue;
}
unavailableSince=null;
latest=frame;maxHeat=Math.Max(maxHeat,frame.HeatPercent??100);maxSpeed=Math.Max(maxSpeed,frame.Speed);
if(frame.PlayerOverride||frame.ExternalThreat||maxHeat>=75)throw new InvalidOperationException("Acceptance heat, threat or manual-override limit exceeded.");
try
{
if(frame.Mode==LocalFlightMode.Supercruise)
{
if(!entered)
{
entered=true;enteredAt=DateTimeOffset.UtcNow;
await File.WriteAllTextAsync(reportPath+".supercruise.json",JsonSerializer.Serialize(new{pid,at=enteredAt,frame,driver.LastTelemetry},ToolboxSettings.JsonOptions));
Console.WriteLine("Original supercruise observed; holding zero throttle for bounded read-only diagnostics.");
}
if(DateTimeOffset.UtcNow-enteredAt<TimeSpan.FromSeconds(90)||frame.Speed>100000)
await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting,"验证超巡并减速",null,0),frame,target,frame.ObservedAt.AddMilliseconds(250),timeout.Token);
else if(exitRequests==0)
{await driver.LeaveSupercruiseSlowlyAsync(frame,timeout.Token);exitRequests++;}
else await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting,"等待低速退出",null,0),frame,target,frame.ObservedAt.AddMilliseconds(250),timeout.Token);
}
else if(entered&&frame.Mode==LocalFlightMode.Normal)
{
await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.Waiting,"验证正常空间停车",null,0),frame,target,frame.ObservedAt.AddMilliseconds(250),timeout.Token);
if(exitRequests>0&&frame.Speed<1){returned=true;break;}
}
else if(!entered)
{
if(frame.TargetObscured||frame.Mode==LocalFlightMode.Docked)throw new InvalidOperationException("Entry destination is obscured or ship is docked.");
var direction=(frame.TargetPosition-frame.Position).Unit();var aligned=Vector3d.AngleDegrees(frame.Forward,direction)<3;
var action=aligned&&frame.FsdReady&&enterRequests==0?SystemNavigationAction.EnterSupercruise:SystemNavigationAction.None;
await driver.ApplySystemFlightAsync(new(SystemNavigationPhase.EnteringSupercruise,"验证进入超巡",direction,aligned?1:0,action),frame,target,frame.ObservedAt.AddMilliseconds(250),timeout.Token);
if(action!=SystemNavigationAction.None)enterRequests++;
}
inputs++;await Record(new{at=DateTimeOffset.UtcNow,entered,returned,rebound,inputs,enterRequests,exitRequests,frame,driver.LastTelemetry});
}
catch(Exception error)when(NavigationRecovery.CanRetry(error,timeout.Token))
{await Record(new{at=DateTimeOffset.UtcNow,recovering=error.Message});await driver.SuspendSystemFlightAsync();}
await Task.Delay(50,timeout.Token);
}
if(!entered||!returned)throw new TimeoutException("Bounded SC entry/exit did not complete.");
}
catch(Exception error){failure=error.ToString();}
finally
{
try{await driver.ReleaseSystemFlightAsync();released=true;}catch(Exception error){failure=(failure??"")+" Release: "+error.Message;}
if(protection is not null)await protection.DisposeAsync();
}
var passed=entered&&returned&&released&&failure is null;
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{pid,start,finished=DateTimeOffset.UtcNow,passed,entered,returned,released,rebound,inputs,enterRequests,exitRequests,maxHeat,maxSpeed,latest,failure,
tradeOrdersSent=0,dockingRequestsSent=0,completeNavigationAccepted=false},ToolboxSettings.JsonOptions));
Console.WriteLine(JsonSerializer.Serialize(new{passed,entered,returned,released,rebound,inputs,enterRequests,exitRequests,maxHeat,maxSpeed,failure}));
if(!passed)throw new InvalidOperationException("Local SC entry/exit acceptance failed.");
}
}