using System.Diagnostics;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
internal static class TradeJourneyAcceptance
{
public static async Task RunAsync(int pid,string directory)
{
directory=Path.GetFullPath(directory);Directory.CreateDirectory(directory);
var statePath=Path.Combine(directory,"journey.json");var statusPath=Path.Combine(directory,"status.json");
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;
if(!initial.InGame||initial.CurrentShipId is null||!initial.CargoReliable)throw new InvalidOperationException("Reliable live ship required.");
var state=File.Exists(statePath)?TradeJourneyCheckpoints.Load(statePath):new TradeJourneyCheckpoint(initial.CommanderId,initial.CurrentShipId.Value,DateTimeOffset.UtcNow);
if(state.Commander!=initial.CommanderId||state.ShipId!=initial.CurrentShipId)throw new InvalidOperationException("Acceptance commander or ship changed.");
using var stop=new CancellationTokenSource(TimeSpan.FromMinutes(35));
using var sentinel=new Timer(_=>{if(File.Exists(Path.Combine(directory,"stop")))stop.Cancel();},null,100,100);
using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger"));
var statusLock=new object();string phase="准备 1t 低价跑商验收";
void Publish(string message,bool? passed=null)
{
lock(statusLock)
{
phase=message;
var temporary=statusPath+".tmp";
File.WriteAllText(temporary,JsonSerializer.Serialize(new{pid,phase,passed,at=DateTimeOffset.UtcNow,profit=ledger.Snapshot,
state.StartedAt,shipId=state.ShipId,initial.CargoCapacity,destination="AD Leonis / Homer Platform",
quantity=1,commodity="biowaste",screenUse=false},ToolboxSettings.JsonOptions));
File.Move(temporary,statusPath,true);Console.WriteLine(message);
}
}
Task Save(){TradeJourneyCheckpoints.Save(statePath,state);return Task.CompletedTask;}
var channel=new NativeCommerceChannel(pid);NativeWalletObservation? wallet=null;
PlayerState Player()=>wallet?.Project(journal.State)??journal.State;
async Task Check(CancellationToken token)
{
token.ThrowIfCancellationRequested();var current=Player();
var maintenance=Path.Combine(Directory.GetParent(directory)!.FullName,"maintenance-hold.json");
if(current.Docked&&File.Exists(maintenance))
{
using var notice=JsonDocument.Parse(await File.ReadAllTextAsync(maintenance,token));
if(notice.RootElement.GetProperty("active").GetBoolean())throw new InvalidOperationException("服务器维护等待中,保持停靠;维护完成后自动继续同一笔 1t 货物验收。");
}
if(!current.InGame||current.CurrentShipId!=state.ShipId||current.CommanderId!=state.Commander||NavigationSafety.UnhealthyHull(current)||NavigationSafety.HasExternalThreat(current))
throw new InvalidOperationException("Acceptance session or ship safety changed.");
var health=await channel.CallAsync(new{op="health.read"},token);
if(!health.GetProperty("localPlayerShipVerified").GetBoolean()||!health.GetProperty("shipHealth").GetProperty("verified").GetBoolean()
||health.GetProperty("shipHealth").GetProperty("hullFraction").GetDouble()<.5)throw new InvalidOperationException("Independent ship health unavailable.");
}
async Task Travel(Station station,Func<CancellationToken,Task> beforeInput,CancellationToken token)
{
await beforeInput(token);
if(Player().Docked)
{
if(Player().MarketId==station.MarketId)return;
Publish("自动起飞并解除质量锁定");
await LocalDepartureAcceptance.RunAsync(pid,"Quimper Dock",Path.Combine(directory,"departure-"+Guid.NewGuid().ToString("N")+".json"),token);
}
await Check(token);
if(!Player().SystemName.Equals(station.SystemName,StringComparison.OrdinalIgnoreCase))
{
Publish("正在按游戏内航线前往 "+station.SystemName);
await InterstellarTravelAcceptance.RunAsync(pid,station.SystemName,Path.Combine(directory,"interstellar-"+Guid.NewGuid().ToString("N")+".json"),token);
}
await Check(token);Publish("正在超巡、呼叫空管并自动停靠 "+station.Name);
await LocalNavigationAcceptance.RunAsync(pid,station.Name,Path.Combine(directory,"arrival-"+Guid.NewGuid().ToString("N")+".json"),requireCruise:false,useNativeCruise:true,allowObscuration:true,externalToken:token);
if(!Player().Docked||Player().MarketId!=station.MarketId)throw new InvalidOperationException("Original docking did not confirm the expected destination.");
}
var driver=new NativeTradeCommandDriver(channel,Player,Path.Combine(directory,"operations"),Travel,Check);
driver.WalletObserved+=observation=>wallet=observation;
driver.ServiceConfirmed+=receipt=>{ledger.Record(receipt.ToCashEntry());Publish(phase);};
driver.Status+=message=>Publish(message);
var from=new Station{MarketId=128096504,Name="Leiber Installation",SystemName="Phanes"};
var to=new Station{MarketId=4210924803,Name="Homer Platform",SystemName="AD Leonis"};
// A small acceptance expense is allowed; this is not the production route.
// Sale uses a floor of 1 Cr and records the actual server price and net loss/profit.
var order=new TradeOrder("biowaste","Biowaste",1,40,1);
var leg=new TradeLeg{Number=1,From=from,To=to,Orders=[order]};
var policy=new TradeAcceptancePolicy();
var healthLog=Path.Combine(directory,"continuous-health-"+Guid.NewGuid().ToString("N")+".json");
var start=new ProcessStartInfo(Environment.ProcessPath!){UseShellExecute=false,CreateNoWindow=true,WindowStyle=ProcessWindowStyle.Hidden,
RedirectStandardOutput=true,RedirectStandardError=true};
foreach(var argument in new[]{"observe-health",pid.ToString(),healthLog,"2100"})start.ArgumentList.Add(argument);
using var monitor=Process.Start(start)??throw new IOException("Cannot start independent continuous ship protection.");
var monitorReady=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
monitor.OutputDataReceived+=(_,e)=>{if(e.Data?.Contains("health protection armed",StringComparison.Ordinal)==true)monitorReady.TrySetResult();};
monitor.BeginOutputReadLine();monitor.BeginErrorReadLine();
string? failure=null;Task? dockedService=null;
try
{
await monitorReady.Task.WaitAsync(TimeSpan.FromSeconds(12),stop.Token);
await Save();await driver.PreflightAsync(stop.Token);await Check(stop.Token);
if(state.Bought is null)
{
if(!Player().Docked||Player().MarketId!=from.MarketId||Player().CargoCount!=0)throw new InvalidOperationException("First acceptance purchase requires the confirmed empty hold at Leiber Installation.");
Publish("加油维修补给与 1t 低价买入同时执行");
var buy=new TradeAction(TradeActionKind.Buy,leg,order);policy.ValidateAction(buy,Player());
dockedService=driver.ServiceDockedAsync(new(from.MarketId,1),Check,stop.Token);
var purchase=driver.ExecuteTradeAsync(buy,Check,stop.Token);
var receipt=await purchase;receipt.Validate(buy,Player(),state.StartedAt);ledger.RecordTrade(receipt);
state=state with{Bought=receipt};await Save();Publish("已确认买入 1t 低价货物");
await dockedService;dockedService=null;
}
else ledger.RecordTrade(state.Bought);
if(state.Sold is null)
{
var travel=new TradeAction(TradeActionKind.Travel,leg,null);policy.ValidateAction(travel,Player());
if(!Player().Docked||Player().MarketId!=to.MarketId)
{
// Driver travel starts at the original departure station. A
// resumed in-flight acceptance continues its same destination.
await Travel(to,Check,stop.Token);
}
var sale=new TradeAction(TradeActionKind.Sell,leg,order);policy.ValidateAction(sale,Player());
Publish("到站维护与实际卖出同时执行");
dockedService=driver.ServiceDockedAsync(new(to.MarketId,1),Check,stop.Token);
var receipt=await driver.ExecuteTradeAsync(sale,Check,stop.Token);
receipt.Validate(sale,Player(),state.StartedAt);ledger.RecordTrade(receipt);state=state with{Sold=receipt};await Save();
await dockedService;dockedService=null;
}
else ledger.RecordTrade(state.Sold);
if(Player().CargoCount!=0||!Player().Docked||Player().MarketId!=to.MarketId||monitor.HasExited)
throw new InvalidOperationException("Final cargo, docking or continuous health protection not confirmed.");
Publish("1t 完整跑商实测完成",true);
}
catch(Exception error){failure=error.ToString();Publish("正在处理验收问题:"+error.Message,false);}
finally
{
stop.Cancel();
if(dockedService is not null)try{await dockedService;}catch(Exception error){failure??=error.ToString();}
await driver.ReleaseAllAsync();
await File.WriteAllTextAsync(healthLog+".stop","stop",CancellationToken.None);
try{await monitor.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(4));}catch(TimeoutException){}
await File.WriteAllTextAsync(Path.Combine(directory,"result.json"),JsonSerializer.Serialize(new{passed=failure is null,failure,state,profit=ledger.Snapshot,
finalSystem=Player().SystemName,finalStation=Player().StationName,Player().Docked,Player().CargoCount,screenUse=false},ToolboxSettings.JsonOptions));
}
if(failure is not null)throw new InvalidOperationException("1t journey is awaiting automatic repair; see its persisted receipts and result.");
}
}
using System.Diagnostics;
using System.Text.Json;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Native;
using XFEEDToolBox.Core.Navigation;
internal static class TradeJourneyAcceptance
{
public static async Task RunAsync(int pid,string directory)
{
directory=Path.GetFullPath(directory);Directory.CreateDirectory(directory);
var statePath=Path.Combine(directory,"journey.json");var statusPath=Path.Combine(directory,"status.json");
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;
if(!initial.InGame||initial.CurrentShipId is null||!initial.CargoReliable)throw new InvalidOperationException("Reliable live ship required.");
var state=File.Exists(statePath)?TradeJourneyCheckpoints.Load(statePath):new TradeJourneyCheckpoint(initial.CommanderId,initial.CurrentShipId.Value,DateTimeOffset.UtcNow);
if(state.Commander!=initial.CommanderId||state.ShipId!=initial.CurrentShipId)throw new InvalidOperationException("Acceptance commander or ship changed.");
using var stop=new CancellationTokenSource(TimeSpan.FromMinutes(35));
using var sentinel=new Timer(_=>{if(File.Exists(Path.Combine(directory,"stop")))stop.Cancel();},null,100,100);
using var ledger=new TradeProfitLedger(Path.Combine(directory,"ledger"));
var statusLock=new object();string phase="准备 1t 低价跑商验收";
void Publish(string message,bool? passed=null)
{
lock(statusLock)
{
phase=message;
var temporary=statusPath+".tmp";
File.WriteAllText(temporary,JsonSerializer.Serialize(new{pid,phase,passed,at=DateTimeOffset.UtcNow,profit=ledger.Snapshot,
state.StartedAt,shipId=state.ShipId,initial.CargoCapacity,destination="AD Leonis / Homer Platform",
quantity=1,commodity="biowaste",screenUse=false},ToolboxSettings.JsonOptions));
File.Move(temporary,statusPath,true);Console.WriteLine(message);
}
}
Task Save(){TradeJourneyCheckpoints.Save(statePath,state);return Task.CompletedTask;}
var channel=new NativeCommerceChannel(pid);NativeWalletObservation? wallet=null;
PlayerState Player()=>wallet?.Project(journal.State)??journal.State;
async Task Check(CancellationToken token)
{
token.ThrowIfCancellationRequested();var current=Player();
var maintenance=Path.Combine(Directory.GetParent(directory)!.FullName,"maintenance-hold.json");
if(current.Docked&&File.Exists(maintenance))
{
using var notice=JsonDocument.Parse(await File.ReadAllTextAsync(maintenance,token));
if(notice.RootElement.GetProperty("active").GetBoolean())throw new InvalidOperationException("服务器维护等待中,保持停靠;维护完成后自动继续同一笔 1t 货物验收。");
}
if(!current.InGame||current.CurrentShipId!=state.ShipId||current.CommanderId!=state.Commander||NavigationSafety.UnhealthyHull(current)||NavigationSafety.HasExternalThreat(current))
throw new InvalidOperationException("Acceptance session or ship safety changed.");
var health=await channel.CallAsync(new{op="health.read"},token);
if(!health.GetProperty("localPlayerShipVerified").GetBoolean()||!health.GetProperty("shipHealth").GetProperty("verified").GetBoolean()
||health.GetProperty("shipHealth").GetProperty("hullFraction").GetDouble()<.5)throw new InvalidOperationException("Independent ship health unavailable.");
}
async Task Travel(Station station,Func<CancellationToken,Task> beforeInput,CancellationToken token)
{
await beforeInput(token);
if(Player().Docked)
{
if(Player().MarketId==station.MarketId)return;
Publish("自动起飞并解除质量锁定");
await LocalDepartureAcceptance.RunAsync(pid,"Quimper Dock",Path.Combine(directory,"departure-"+Guid.NewGuid().ToString("N")+".json"),token);
}
await Check(token);
if(!Player().SystemName.Equals(station.SystemName,StringComparison.OrdinalIgnoreCase))
{
Publish("正在按游戏内航线前往 "+station.SystemName);
await InterstellarTravelAcceptance.RunAsync(pid,station.SystemName,Path.Combine(directory,"interstellar-"+Guid.NewGuid().ToString("N")+".json"),token);
}
await Check(token);Publish("正在超巡、呼叫空管并自动停靠 "+station.Name);
await LocalNavigationAcceptance.RunAsync(pid,station.Name,Path.Combine(directory,"arrival-"+Guid.NewGuid().ToString("N")+".json"),requireCruise:false,useNativeCruise:true,allowObscuration:true,externalToken:token);
if(!Player().Docked||Player().MarketId!=station.MarketId)throw new InvalidOperationException("Original docking did not confirm the expected destination.");
}
var driver=new NativeTradeCommandDriver(channel,Player,Path.Combine(directory,"operations"),Travel,Check);
driver.WalletObserved+=observation=>wallet=observation;
driver.ServiceConfirmed+=receipt=>{ledger.Record(receipt.ToCashEntry());Publish(phase);};
driver.Status+=message=>Publish(message);
var from=new Station{MarketId=128096504,Name="Leiber Installation",SystemName="Phanes"};
var to=new Station{MarketId=4210924803,Name="Homer Platform",SystemName="AD Leonis"};
// A small acceptance expense is allowed; this is not the production route.
// Sale uses a floor of 1 Cr and records the actual server price and net loss/profit.
var order=new TradeOrder("biowaste","Biowaste",1,40,1);
var leg=new TradeLeg{Number=1,From=from,To=to,Orders=[order]};
var policy=new TradeAcceptancePolicy();
var healthLog=Path.Combine(directory,"continuous-health-"+Guid.NewGuid().ToString("N")+".json");
var start=new ProcessStartInfo(Environment.ProcessPath!){UseShellExecute=false,CreateNoWindow=true,WindowStyle=ProcessWindowStyle.Hidden,
RedirectStandardOutput=true,RedirectStandardError=true};
foreach(var argument in new[]{"observe-health",pid.ToString(),healthLog,"2100"})start.ArgumentList.Add(argument);
using var monitor=Process.Start(start)??throw new IOException("Cannot start independent continuous ship protection.");
var monitorReady=new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
monitor.OutputDataReceived+=(_,e)=>{if(e.Data?.Contains("health protection armed",StringComparison.Ordinal)==true)monitorReady.TrySetResult();};
monitor.BeginOutputReadLine();monitor.BeginErrorReadLine();
string? failure=null;Task? dockedService=null;
try
{
await monitorReady.Task.WaitAsync(TimeSpan.FromSeconds(12),stop.Token);
await Save();await driver.PreflightAsync(stop.Token);await Check(stop.Token);
if(state.Bought is null)
{
if(!Player().Docked||Player().MarketId!=from.MarketId||Player().CargoCount!=0)throw new InvalidOperationException("First acceptance purchase requires the confirmed empty hold at Leiber Installation.");
Publish("加油维修补给与 1t 低价买入同时执行");
var buy=new TradeAction(TradeActionKind.Buy,leg,order);policy.ValidateAction(buy,Player());
dockedService=driver.ServiceDockedAsync(new(from.MarketId,1),Check,stop.Token);
var purchase=driver.ExecuteTradeAsync(buy,Check,stop.Token);
var receipt=await purchase;receipt.Validate(buy,Player(),state.StartedAt);ledger.RecordTrade(receipt);
state=state with{Bought=receipt};await Save();Publish("已确认买入 1t 低价货物");
await dockedService;dockedService=null;
}
else ledger.RecordTrade(state.Bought);
if(state.Sold is null)
{
var travel=new TradeAction(TradeActionKind.Travel,leg,null);policy.ValidateAction(travel,Player());
if(!Player().Docked||Player().MarketId!=to.MarketId)
{
// Driver travel starts at the original departure station. A
// resumed in-flight acceptance continues its same destination.
await Travel(to,Check,stop.Token);
}
var sale=new TradeAction(TradeActionKind.Sell,leg,order);policy.ValidateAction(sale,Player());
Publish("到站维护与实际卖出同时执行");
dockedService=driver.ServiceDockedAsync(new(to.MarketId,1),Check,stop.Token);
var receipt=await driver.ExecuteTradeAsync(sale,Check,stop.Token);
receipt.Validate(sale,Player(),state.StartedAt);ledger.RecordTrade(receipt);state=state with{Sold=receipt};await Save();
await dockedService;dockedService=null;
}
else ledger.RecordTrade(state.Sold);
if(Player().CargoCount!=0||!Player().Docked||Player().MarketId!=to.MarketId||monitor.HasExited)
throw new InvalidOperationException("Final cargo, docking or continuous health protection not confirmed.");
Publish("1t 完整跑商实测完成",true);
}
catch(Exception error){failure=error.ToString();Publish("正在处理验收问题:"+error.Message,false);}
finally
{
stop.Cancel();
if(dockedService is not null)try{await dockedService;}catch(Exception error){failure??=error.ToString();}
await driver.ReleaseAllAsync();
await File.WriteAllTextAsync(healthLog+".stop","stop",CancellationToken.None);
try{await monitor.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(4));}catch(TimeoutException){}
await File.WriteAllTextAsync(Path.Combine(directory,"result.json"),JsonSerializer.Serialize(new{passed=failure is null,failure,state,profit=ledger.Snapshot,
finalSystem=Player().SystemName,finalStation=Player().StationName,Player().Docked,Player().CargoCount,screenUse=false},ToolboxSettings.JsonOptions));
}
if(failure is not null)throw new InvalidOperationException("1t journey is awaiting automatic repair; see its persisted receipts and result.");
}
}