using XFEEDToolBox.NativeSupport;
using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Services;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Presentation;
using XFEEDToolBox.Core.Session;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
private CancellationTokenSource? automaticTradeStop;
private string? automaticTradeConfigurationPath;
private AutomaticTradeConfiguration? automaticTradeParameters;
[ObservableProperty] public partial StationLiveRow? AutomaticDestination { get; set; }
[ObservableProperty,NotifyPropertyChangedFor(nameof(AutomaticCgVisibility))] public partial int AutomaticModeIndex { get; set; }
[ObservableProperty] public partial string AutomaticCommodities { get; set; }="";
[ObservableProperty] public partial bool AutomaticReturnTrade { get; set; }=true;
[ObservableProperty,NotifyPropertyChangedFor(nameof(AutomaticUpdateVisibility))] public partial bool AutomaticPluginUpdateRequired { get; set; }
public Microsoft.UI.Xaml.Visibility AutomaticUpdateVisibility=>AutomaticPluginUpdateRequired?Microsoft.UI.Xaml.Visibility.Visible:Microsoft.UI.Xaml.Visibility.Collapsed;
public Microsoft.UI.Xaml.Visibility AutomaticCgVisibility=>AutomaticModeIndex==1?Microsoft.UI.Xaml.Visibility.Visible:Microsoft.UI.Xaml.Visibility.Collapsed;
public string AutomaticShipSummary=>$"当前飞船 {CurrentPlayer.Ship} · 实际货舱 {CurrentPlayer.CargoCapacity:N0}t · 重购费 {CurrentPlayer.Rebuy:N0} Cr;启动时重新核对。";
partial void OnAutomaticDestinationChanged(StationLiveRow? value){NoteManualTradeConfiguration();InvalidateTradeCandidates();NotifyTradePlanningSummary();RefreshTradePlanCommands();}
partial void OnAutomaticModeIndexChanged(int value){NoteManualTradeConfiguration();TradePlanningModeChanged(value);RefreshTradePlanCommands();}
private string AutomaticRouteFile=>Path.Combine(runtime.DataDirectory,"automatic-trade-route.json");
private void ApplyAutomaticParameters(AutomaticTradeConfiguration value)
{
var rows=Stations.Select(s=>s.Record).Append(value.Source).Append(value.Destination).DistinctBy(s=>s.MarketId).ToArray();
Stations.Apply(rows);SelectedStart=Stations.First(s=>s.MarketId==value.Source.MarketId);
AutomaticDestination=Stations.First(s=>s.MarketId==value.Destination.MarketId);
AutomaticModeIndex=(int)value.Mode;AutomaticCommodities=string.Join(", ",value.Commodities);AutomaticReturnTrade=value.ReturnTrade;
automaticTradeParameters=value;
}
private async Task InitializeAutomaticTradeAsync()
{
if(!File.Exists(AutomaticRouteFile))return;
try
{
var value=await Task.Run(()=>JsonSerializer.Deserialize<AutomaticTradeConfiguration>(File.ReadAllText(AutomaticRouteFile),ToolboxSettings.JsonOptions),lifetime.Token);
if(value is not null){value.Validate();ApplyAutomaticParameters(value);}
}
catch(Exception error){runtime.Log("跑商路线",error.Message);}
}
private AutomaticTradeConfiguration SelectedAutomaticParameters(string[] names)
{
if(SelectedStart is null||AutomaticDestination is null)throw new InvalidOperationException("请选择起点和目的站点。");
if(!investmentFollowsBalance&&(!double.IsFinite(TradeInvestment)||TradeInvestment<=0))throw new InvalidOperationException("请填写大于零的投入资金。");
if(names.Any(n=>!NativeTradeBatchPolicy.AllowsFormal(n)))throw new InvalidOperationException("商品名称未被识别,请使用行情列表中的名称。");
if(AutomaticModeIndex==1&&names.Length==0)throw new InvalidOperationException("请根据CG活动说明填写接收的商品。");
var value=new AutomaticTradeConfiguration{Source=SelectedStart.Record,Destination=AutomaticDestination.Record,
WorkingDirectory=automaticTradeParameters?.WorkingDirectory??Path.Combine(runtime.DataDirectory,"automatic-trading","active"),
CargoCapacity=0,ReserveCredits=Math.Max(runtime.Player.Rebuy,Math.Max(0,(decimal)Reserve)),
InvestmentCredits=investmentFollowsBalance?null:(decimal)TradeInvestment,Mode=(AutomaticTradeMode)AutomaticModeIndex,Commodities=names,
CommunityGoalId=automaticTradeParameters?.Destination.MarketId==AutomaticDestination.MarketId?automaticTradeParameters.CommunityGoalId:null,
ReturnTrade=AutomaticReturnTrade,EmptyValidationVisits=0,ValidateMenuReentry=false,
SourcePlanning=BuildRequest(),SourceCenterFollowsPlayer=TradeRegionCenterText.Trim().Length==0};
value.Validate();return value;
}
private async Task RunSelectedAutomaticTradeAsync()
{
var text=AutomaticCommodities;
var names=await Task.Run(()=>text.Split([',',',',';',';'],StringSplitOptions.TrimEntries|StringSplitOptions.RemoveEmptyEntries)
.Select(CommodityCatalog.SelectionKey).Distinct(StringComparer.Ordinal).ToArray(),lifetime.Token);
var value=SelectedAutomaticParameters(names);
Directory.CreateDirectory(Path.GetDirectoryName(AutomaticRouteFile)!);
await File.WriteAllTextAsync(AutomaticRouteFile,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),lifetime.Token);
await RunAutomaticParametersAsync(value,AutomaticRouteFile);
}
[RelayCommand(CanExecute=nameof(CanExecuteTradePlan))]
private async Task UpdateAndStartAutomaticTradeAsync()
{
var text=AutomaticCommodities;
var names=await Task.Run(()=>text.Split([',',',',';',';'],StringSplitOptions.TrimEntries|StringSplitOptions.RemoveEmptyEntries)
.Select(CommodityCatalog.SelectionKey).Distinct(StringComparer.Ordinal).ToArray(),lifetime.Token);
var value=SelectedAutomaticParameters(names);var succeeded=false;
if(RunActive||automaticTradeStop is not null)throw new InvalidOperationException("工具箱已有跑商或恢复流程。");
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);automaticTradeStop=stop;RunActive=true;HasError=false;
try
{
var executable=PluginGameExecutable;var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,executable,NativeSupportPackage.Directory,Path.Combine(value.WorkingDirectory,"operations"));
bootstrap.Progress+=message=>dispatcher.TryEnqueue(()=>{RunStatus="更新并恢复";RunMessage=message;runtime.Log("跑商更新",message);});
await Task.Run(()=>bootstrap.UpgradeAndEnterAsync(stop.Token),stop.Token);succeeded=true;AutomaticPluginUpdateRequired=false;
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){RunStatus="已取消";RunMessage="更新或恢复已取消,已完成步骤保留。";}
catch(Exception error){RunStatus="需要处理";RunMessage=error.Message;ShowError(error.Message);}
finally{automaticTradeStop=null;RunActive=false;RefreshTradePlanCommands();}
if(succeeded)
{
await ReadPluginStatusAsync();
await File.WriteAllTextAsync(AutomaticRouteFile,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),lifetime.Token);
await RunAutomaticParametersAsync(value,AutomaticRouteFile);
}
}
internal async Task UpdateAndRunAutomaticTradeAsync(string configurationPath)
{
var value=await Task.Run(()=>JsonSerializer.Deserialize<AutomaticTradeConfiguration>(File.ReadAllText(configurationPath),ToolboxSettings.JsonOptions),lifetime.Token)
??throw new InvalidDataException("跑商参数为空。");
value.Validate();ApplyAutomaticParameters(value);await UpdateAndStartAutomaticTradeAsync();
}
internal async Task UpgradeNativeSupportOnlyAsync(string configurationPath,string reportPath)
{
if(RunActive||automaticTradeStop is not null||runtime.FlightAutomation.IsActive||NavigationActive||SystemFlightActive)throw new InvalidOperationException("请先结束当前驾驶流程再更新。");
var value=JsonSerializer.Deserialize<AutomaticTradeConfiguration>(await File.ReadAllTextAsync(configurationPath,lifetime.Token),ToolboxSettings.JsonOptions)
??throw new InvalidDataException("维护参数为空。");value.Validate();
var before=runtime.Player;RunActive=true;
try
{
var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,Path.Combine(value.WorkingDirectory,"operations"));
bootstrap.Progress+=message=>dispatcher.TryEnqueue(()=>{RunStatus="更新底层支持";RunMessage=message;runtime.Log("底层维护",message);});
await Task.Run(()=>bootstrap.UpgradeAndEnterAsync(lifetime.Token),lifetime.Token);
var after=runtime.Player;
if(before.Docked&&(!after.Docked||before.MarketId!=after.MarketId))throw new InvalidDataException("更新后的停靠地点未与原行程一致,未继续任何驾驶。");
await ReadPluginStatusAsync();RunStatus="已更新";RunMessage="底层支持已更新,保留当前行程;未启动跑商。";
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{passed=true,at=DateTimeOffset.UtcNow,automaticTradeStarted=false,
before=new{before.CommanderId,before.CurrentShipId,before.MarketId,before.GameMode,before.CargoCount},
after=new{after.CommanderId,after.CurrentShipId,after.MarketId,after.GameMode,after.CargoCount}},ToolboxSettings.JsonOptions),lifetime.Token);
}
catch(Exception error)
{
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{passed=false,at=DateTimeOffset.UtcNow,automaticTradeStarted=false,error=error.Message},ToolboxSettings.JsonOptions));throw;
}
finally{RunActive=false;RefreshTradePlanCommands();}
}
internal async Task RunAutomaticTradeAsync(string configurationPath,Action? beforeStart=null)
{
var value=await Task.Run(()=>JsonSerializer.Deserialize<AutomaticTradeConfiguration>(File.ReadAllText(configurationPath),ToolboxSettings.JsonOptions),lifetime.Token)
??throw new InvalidDataException("跑商参数为空。");
value.Validate();ApplyAutomaticParameters(value);
await File.WriteAllTextAsync(AutomaticRouteFile,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),lifetime.Token);
// Configuration preparation can yield after an upstream authorization
// check. Reconfirm immediately before the new run owns any control;
// its long-lived token still belongs to this viewmodel, not the caller.
lifetime.Token.ThrowIfCancellationRequested();beforeStart?.Invoke();
await RunAutomaticParametersAsync(value,configurationPath);
}
private async Task RunAutomaticParametersAsync(AutomaticTradeConfiguration parameters,string path)
{
if(RunActive||automaticTradeStop is not null)throw new InvalidOperationException("工具箱已有跑商流程。");
automaticTradeConfigurationPath=Path.GetFullPath(path);automaticTradeParameters=parameters;
AutomaticSupplySummary="";
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);automaticTradeStop=stop;RunActive=true;HasError=false;
NativeTradeCommandDriver? driver=null;
AutomaticTradeRunSession? financialSession=null;
int? financialRoundsBaseline=null;
DateTimeOffset lastProtectionRecovery=default;
var sceneRecoveries=0;
async Task EnterWithConnectionSupervisionAsync(WindowsTradingBootstrap bootstrap)
{
var expected=runtime.Player;
await StartupConnectionRecovery.SuperviseAsync(cancellation=>Task.Run(()=>bootstrap.UpgradeAndEnterAsync(cancellation),cancellation),
async(failure,count,cancellation)=>
{
var current=runtime.Player;
if(current.CommanderId.Length>0&¤t.CommanderId!=expected.CommanderId
||current.CurrentShipId is {} ship&&ship!=expected.CurrentShipId||current.GameMode.Length>0&¤t.GameMode!=expected.GameMode)
throw new InvalidDataException("等待连接恢复期间指挥官、飞船或原模式改变,已停止自动跑商。");
var delay=TimeSpan.FromMinutes(Math.Min(10,count*2));
RunStatus="等待连接恢复";RunMessage=$"原版连接失败已完成本轮有界恢复,将在 {delay.TotalMinutes:N0} 分钟后核对并续接原行程;原订单、回执和累计收益保留,可随时取消。";HasError=false;
runtime.Log("连接恢复等待",$"第 {count} 轮:{failure.Message}");
await Task.Delay(delay,cancellation);
current=runtime.Player;
if(current.CommanderId.Length>0&¤t.CommanderId!=expected.CommanderId
||current.CurrentShipId is {} nextShip&&nextShip!=expected.CurrentShipId||current.GameMode.Length>0&¤t.GameMode!=expected.GameMode)
throw new InvalidDataException("等待连接恢复期间身份改变,已保留客户端和原订单。");
},stop.Token);
}
bool ProtectionRecoveryPending()
{
// Navigation may reject the new Status before the health poll's
// next tick. Consume that same fresh witness before handling failure.
shipProtection?.ObserveInterdiction(runtime.Player,DateTimeOffset.UtcNow);
return shipProtection?.LastTrigger is{} trigger&&(trigger.Reason is ShipProtectionReason.Interdicted or ShipProtectionReason.ShieldsLost or ShipProtectionReason.HullBelowHalf)
&&trigger.At>lastProtectionRecovery;
}
try
{
while(!stop.IsCancellationRequested)
{
try
{
var existingSession=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,Path.Combine(parameters.WorkingDirectory,"operations")){ResumeExistingCargo=true};
existingSession.Progress+=text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("跑商会话恢复",text);});
await EnterWithConnectionSupervisionAsync(existingSession);
await ReadPluginStatusAsync();
while(!runtime.Player.InGame||runtime.Player.CurrentShipId is null||runtime.Player.CargoCapacity<=0||runtime.Player.Rebuy<=0||!runtime.Player.CargoReliable||!runtime.Player.CreditsReliable)
{
RunStatus="等待游戏就绪";RunMessage="正在等待游戏载入及当前飞船、货舱和余额就绪,完成后自动继续。";HasError=false;
await Task.Delay(1000,stop.Token);
}
var current=runtime.Player;
var reserve=Math.Max(parameters.ReserveCredits,current.Rebuy);
RunStatus="正在启动";RunMessage="正在按当前飞船核对接口、货舱和保留资金。";
PlanProfit="正在读取实时货单";PlanHourly="货单就绪后估算";PlanDuration="正在估算航程";ProfitLabel="本次跑商已确认净利润";
driver=await CreateNativeTradeDriverAsync(new(),stop.Token,new(current.CargoCapacity,reserve),Path.Combine(parameters.WorkingDirectory,"operations"));
var flight=(NativeFlightControlDriver)runtime.FlightDriver!;var pid=flight.ProcessId;
async Task Recover(CancellationToken token)
{
await WindowsFlightRecovery.RecoverAsync(pid,NativeRelogChannel.SupportedGameSha256,()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,
ReleaseProtectionForControlledExitAsync,text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("跑商会话恢复",text);}),token);
var refreshed=await new WindowsGameSessionDriver(()=>runtime.Player,()=>PluginGameExecutable).FindTargetAsync(token);
if(refreshed.ProcessId!=pid)throw new SavingSceneRecoveryRequiredException(new IOException("已保护重启客户端,原订单等待在新实例接续。"));
}
async Task ValidateFlight(CancellationToken token)
{
var capability=await new NativeCruiseControlChannel(pid,NativeRelogChannel.SupportedGameSha256).PrepareAsync(Path.Combine(NativeSupportPackage.Directory,"CruiseControl"),token);
if(parameters.RequireSupercruiseOvercharge&&!capability.Available)
throw new NotSupportedException("当前飞船的原生SCO温度、燃油和启停接口尚未就绪,未采购或起飞。");
}
var engine=new AutomaticTradeEngine(parameters,driver,new NativeCommerceChannel(pid),()=>runtime.Player,
token=>EnsureNativeFlightProtectionAsync(pid,NativeRelogChannel.SupportedGameSha256,token),runtime.Settings.JournalDirectory,Recover,
(station,rows,token)=>runtime.Markets.SaveSnapshotAsync(new(station,rows,DateTimeOffset.UtcNow,DataSource.Journal),token),ValidateFlight,SelectAutomaticSupplyAsync,ProtectionRecoveryPending);
engine.Progress+=value=>
{
financialSession??=AutomaticTradeRunSession.Open(parameters.WorkingDirectory,value.NetProfit);
if(financialRoundsBaseline is null)
{
try{financialRoundsBaseline=AutomaticRoundsBaseline(parameters.WorkingDirectory,financialSession,value.CompletedRounds);}
catch(Exception error)when(error is IOException or UnauthorizedAccessException or JsonException){financialRoundsBaseline=value.CompletedRounds;runtime.Log("跑商统计",error.Message);}
}
var sessionProfit=financialSession.Profit(value.NetProfit);
dispatcher.TryEnqueue(()=>{
tradeExecutionStartedAt=financialSession.StartedAt;CompletedTradeRounds=Math.Max(0,value.CompletedRounds-financialRoundsBaseline.Value);
TradeConfirmedProfit=$"{sessionProfit:N0} Cr";RefreshTradeElapsed();
RunStatus=value.Phase switch{"requires-recovery"=>"需要处理","stopped"=>"已取消","safety-pause"=>"等待安全停靠","recovering-safety"=>"安全维修恢复中",_=>"自动跑商中"};
RunMessage=value.Message;NextDestination=value.NextDestination;ActualProfit=$"{sessionProfit:N0} Cr";ProfitLabel="本次跑商已确认净利润";
if(value.SupplySelection is{} supply)AutomaticSupplySummary=$"CG 货源:{supply.Source.DisplayName} · {supply.Order.Name} {supply.Order.Quantity:N0}/{supply.FreeCapacity:N0} t · 库存 {supply.Supply:N0} t · 预计 {supply.CreditsPerHour:N0} Cr/h · 风险 {supply.Delivery.RiskDisplay}";
ProfitDetails=$"自 {financialSession.StartedAt.ToLocalTime():MM-dd HH:mm} 起累计,自动重连和修复重启延续本次累计。";
if(value.Estimate is{} expected){PlanProfit=$"{expected.NetProfit:N0} Cr";PlanHourly=$"{expected.Hourly:N0} Cr/h";PlanDuration=$"约 {expected.Minutes:F1} 分钟";PlanWarnings=expected.Basis;}
runtime.Log("自动跑商",value.Message);
});
};
// This task and its leases are owned by the application, not by the caller or agent.
await Task.Run(()=>engine.RunAsync(stop.Token),stop.Token);
break;
}
catch(Exception)when(!stop.IsCancellationRequested&&ProtectionRecoveryPending())
{
var protection=shipProtection!;lastProtectionRecovery=protection.LastTrigger!.At;
if(driver is not null){try{await driver.ReleaseAllAsync();}catch(Exception error)when(error is IOException or InvalidOperationException or OperationCanceledException){}driver=null;}
RunStatus="飞船保护恢复中";RunMessage="飞船保护已退出客户端,正在通过官方启动器恢复原模式和原订单;健康保护继续生效。";HasError=false;
await protection.Completion.WaitAsync(stop.Token);
if(protection.RestartTarget is null)throw new IOException("拦截已保护退出,但尚未确认官方启动器的新游戏实例。");
// The next iteration enters the original mode in the verified
// replacement process and resumes its durable cargo checkpoint.
}
catch(Exception error)when(!stop.IsCancellationRequested&&FlightConnectionRecoveryRequiredException.Find(error) is not null)
{
var failure=FlightConnectionRecoveryRequiredException.Find(error)!;var binding=failure.Binding;
FlightConnectionRecovery.RequireCurrentIdentity(binding,runtime.Player,stop.Token);
if(driver is not null){await driver.ReleaseAllAsync();driver=null;}
var lifecycle=new WindowsGameSessionDriver(()=>runtime.Player,()=>PluginGameExecutable);
var failedClient=await lifecycle.FindLaunchTargetAsync(stop.Token);
FlightConnectionRecovery.RequireOriginalClient(binding,failedClient,PluginGameExecutable);
if(binding.GameSha256!=NativeRelogChannel.SupportedGameSha256)throw new InvalidDataException("原飞行连接故障的构建身份不匹配。");
FlightConnectionRecovery.RequireCurrentIdentity(binding,runtime.Player,stop.Token);
var operations=Path.Combine(parameters.WorkingDirectory,"operations");Directory.CreateDirectory(operations);
await File.WriteAllTextAsync(Path.Combine(operations,$"flight-connection-recovery-{binding.ProcessId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.json"),
JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,binding,client=failedClient,session=failure.Menu,navigation=failure.Navigation,
originalOrdersUnchanged=true,confirmedReceiptsPreserved=true,originalFailure=error.ToString()},ToolboxSettings.JsonOptions),stop.Token);
FlightConnectionRecovery.RequireCurrentIdentity(binding,runtime.Player,stop.Token);
RunStatus="正在恢复网络连接";RunMessage="原版已持续确认连接失败,正在按原模式有界恢复;先核对原订单,再续接原航程。";HasError=false;
var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,operations)
{ResumeExistingCargo=true,ExpectedConnectionFailureClient=failedClient,ExpectedFlightConnection=binding};
bootstrap.Progress+=text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("飞行连接恢复",text);});
await EnterWithConnectionSupervisionAsync(bootstrap);
await ReadPluginStatusAsync();
// The next engine instance reconciles durable intent/receipts
// before it can issue anything. Keep this run's financial base.
}
catch(Exception error)when(NativeOrderReconciliationRequiredException.Present(error)&&!stop.IsCancellationRequested)
{
if(driver is not null){await driver.ReleaseAllAsync();driver=null;}
RunStatus="正在核对原订单";RunMessage="原始请求返回服务器错误,正在通过官方会话重新核对货舱和余额。";HasError=false;
var recoveryCommander=runtime.Player.CommanderId;var recoveryShip=runtime.Player.CurrentShipId;
while(!runtime.Player.InGame||!runtime.Player.Docked||!runtime.Player.CargoReliable)
{
RunMessage="正在等待当前航程停靠,随后自动恢复官方会话并核对原订单;取消按钮仍可使用。";
await Task.Delay(1000,stop.Token);
}
if(runtime.Player.CommanderId!=recoveryCommander||runtime.Player.CurrentShipId!=recoveryShip)throw new InvalidDataException("等待订单核对期间指挥官或飞船已改变。");
var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,Path.Combine(parameters.WorkingDirectory,"operations")){ReconcileOriginalOrders=true};
bootstrap.Progress+=text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("交易会话核对",text);});
await EnterWithConnectionSupervisionAsync(bootstrap);
await ReadPluginStatusAsync();
}
catch(Exception error)when(!stop.IsCancellationRequested&&SavingSceneRecoveryRequiredException.Present(error)&&FlightJourneyFailure.CanRecoverScene(error)&&sceneRecoveries<2)
{
sceneRecoveries++;
if(driver is not null){await driver.ReleaseAllAsync();driver=null;}
RunStatus="正在恢复游戏场景";RunMessage="已保留原订单,正在核对原版保存状态;未重复提交交易。";HasError=false;
// The next iteration observes the exact original transition for
// 90 seconds, then verifies the replacement session and cargo.
}
}
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){RunStatus="已取消";RunMessage="已停止新增操作,原始订单与恢复记录保留。";}
catch(Exception error){AutomaticPluginUpdateRequired=error is NativeTradeSdkUpgradeRequiredException;RunStatus="需要处理";RunMessage=error.Message;ShowError(error.Message);runtime.Log("自动跑商",error.ToString());}
finally
{
if(driver is not null)try{await driver.ReleaseAllAsync();}catch(Exception error){ShowError(error.Message);}
automaticTradeStop=null;RunActive=false;RefreshTradePlanCommands();CalculateCommand.NotifyCanExecuteChanged();
}
}
}
using XFEEDToolBox.NativeSupport;
using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Services;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Presentation;
using XFEEDToolBox.Core.Session;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
private CancellationTokenSource? automaticTradeStop;
private string? automaticTradeConfigurationPath;
private AutomaticTradeConfiguration? automaticTradeParameters;
[ObservableProperty] public partial StationLiveRow? AutomaticDestination { get; set; }
[ObservableProperty,NotifyPropertyChangedFor(nameof(AutomaticCgVisibility))] public partial int AutomaticModeIndex { get; set; }
[ObservableProperty] public partial string AutomaticCommodities { get; set; }="";
[ObservableProperty] public partial bool AutomaticReturnTrade { get; set; }=true;
[ObservableProperty,NotifyPropertyChangedFor(nameof(AutomaticUpdateVisibility))] public partial bool AutomaticPluginUpdateRequired { get; set; }
public Microsoft.UI.Xaml.Visibility AutomaticUpdateVisibility=>AutomaticPluginUpdateRequired?Microsoft.UI.Xaml.Visibility.Visible:Microsoft.UI.Xaml.Visibility.Collapsed;
public Microsoft.UI.Xaml.Visibility AutomaticCgVisibility=>AutomaticModeIndex==1?Microsoft.UI.Xaml.Visibility.Visible:Microsoft.UI.Xaml.Visibility.Collapsed;
public string AutomaticShipSummary=>$"当前飞船 {CurrentPlayer.Ship} · 实际货舱 {CurrentPlayer.CargoCapacity:N0}t · 重购费 {CurrentPlayer.Rebuy:N0} Cr;启动时重新核对。";
partial void OnAutomaticDestinationChanged(StationLiveRow? value){NoteManualTradeConfiguration();InvalidateTradeCandidates();NotifyTradePlanningSummary();RefreshTradePlanCommands();}
partial void OnAutomaticModeIndexChanged(int value){NoteManualTradeConfiguration();TradePlanningModeChanged(value);RefreshTradePlanCommands();}
private string AutomaticRouteFile=>Path.Combine(runtime.DataDirectory,"automatic-trade-route.json");
private void ApplyAutomaticParameters(AutomaticTradeConfiguration value)
{
var rows=Stations.Select(s=>s.Record).Append(value.Source).Append(value.Destination).DistinctBy(s=>s.MarketId).ToArray();
Stations.Apply(rows);SelectedStart=Stations.First(s=>s.MarketId==value.Source.MarketId);
AutomaticDestination=Stations.First(s=>s.MarketId==value.Destination.MarketId);
AutomaticModeIndex=(int)value.Mode;AutomaticCommodities=string.Join(", ",value.Commodities);AutomaticReturnTrade=value.ReturnTrade;
automaticTradeParameters=value;
}
private async Task InitializeAutomaticTradeAsync()
{
if(!File.Exists(AutomaticRouteFile))return;
try
{
var value=await Task.Run(()=>JsonSerializer.Deserialize<AutomaticTradeConfiguration>(File.ReadAllText(AutomaticRouteFile),ToolboxSettings.JsonOptions),lifetime.Token);
if(value is not null){value.Validate();ApplyAutomaticParameters(value);}
}
catch(Exception error){runtime.Log("跑商路线",error.Message);}
}
private AutomaticTradeConfiguration SelectedAutomaticParameters(string[] names)
{
if(SelectedStart is null||AutomaticDestination is null)throw new InvalidOperationException("请选择起点和目的站点。");
if(!investmentFollowsBalance&&(!double.IsFinite(TradeInvestment)||TradeInvestment<=0))throw new InvalidOperationException("请填写大于零的投入资金。");
if(names.Any(n=>!NativeTradeBatchPolicy.AllowsFormal(n)))throw new InvalidOperationException("商品名称未被识别,请使用行情列表中的名称。");
if(AutomaticModeIndex==1&&names.Length==0)throw new InvalidOperationException("请根据CG活动说明填写接收的商品。");
var value=new AutomaticTradeConfiguration{Source=SelectedStart.Record,Destination=AutomaticDestination.Record,
WorkingDirectory=automaticTradeParameters?.WorkingDirectory??Path.Combine(runtime.DataDirectory,"automatic-trading","active"),
CargoCapacity=0,ReserveCredits=Math.Max(runtime.Player.Rebuy,Math.Max(0,(decimal)Reserve)),
InvestmentCredits=investmentFollowsBalance?null:(decimal)TradeInvestment,Mode=(AutomaticTradeMode)AutomaticModeIndex,Commodities=names,
CommunityGoalId=automaticTradeParameters?.Destination.MarketId==AutomaticDestination.MarketId?automaticTradeParameters.CommunityGoalId:null,
ReturnTrade=AutomaticReturnTrade,EmptyValidationVisits=0,ValidateMenuReentry=false,
SourcePlanning=BuildRequest(),SourceCenterFollowsPlayer=TradeRegionCenterText.Trim().Length==0};
value.Validate();return value;
}
private async Task RunSelectedAutomaticTradeAsync()
{
var text=AutomaticCommodities;
var names=await Task.Run(()=>text.Split([',',',',';',';'],StringSplitOptions.TrimEntries|StringSplitOptions.RemoveEmptyEntries)
.Select(CommodityCatalog.SelectionKey).Distinct(StringComparer.Ordinal).ToArray(),lifetime.Token);
var value=SelectedAutomaticParameters(names);
Directory.CreateDirectory(Path.GetDirectoryName(AutomaticRouteFile)!);
await File.WriteAllTextAsync(AutomaticRouteFile,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),lifetime.Token);
await RunAutomaticParametersAsync(value,AutomaticRouteFile);
}
[RelayCommand(CanExecute=nameof(CanExecuteTradePlan))]
private async Task UpdateAndStartAutomaticTradeAsync()
{
var text=AutomaticCommodities;
var names=await Task.Run(()=>text.Split([',',',',';',';'],StringSplitOptions.TrimEntries|StringSplitOptions.RemoveEmptyEntries)
.Select(CommodityCatalog.SelectionKey).Distinct(StringComparer.Ordinal).ToArray(),lifetime.Token);
var value=SelectedAutomaticParameters(names);var succeeded=false;
if(RunActive||automaticTradeStop is not null)throw new InvalidOperationException("工具箱已有跑商或恢复流程。");
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);automaticTradeStop=stop;RunActive=true;HasError=false;
try
{
var executable=PluginGameExecutable;var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,executable,NativeSupportPackage.Directory,Path.Combine(value.WorkingDirectory,"operations"));
bootstrap.Progress+=message=>dispatcher.TryEnqueue(()=>{RunStatus="更新并恢复";RunMessage=message;runtime.Log("跑商更新",message);});
await Task.Run(()=>bootstrap.UpgradeAndEnterAsync(stop.Token),stop.Token);succeeded=true;AutomaticPluginUpdateRequired=false;
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){RunStatus="已取消";RunMessage="更新或恢复已取消,已完成步骤保留。";}
catch(Exception error){RunStatus="需要处理";RunMessage=error.Message;ShowError(error.Message);}
finally{automaticTradeStop=null;RunActive=false;RefreshTradePlanCommands();}
if(succeeded)
{
await ReadPluginStatusAsync();
await File.WriteAllTextAsync(AutomaticRouteFile,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),lifetime.Token);
await RunAutomaticParametersAsync(value,AutomaticRouteFile);
}
}
internal async Task UpdateAndRunAutomaticTradeAsync(string configurationPath)
{
var value=await Task.Run(()=>JsonSerializer.Deserialize<AutomaticTradeConfiguration>(File.ReadAllText(configurationPath),ToolboxSettings.JsonOptions),lifetime.Token)
??throw new InvalidDataException("跑商参数为空。");
value.Validate();ApplyAutomaticParameters(value);await UpdateAndStartAutomaticTradeAsync();
}
internal async Task UpgradeNativeSupportOnlyAsync(string configurationPath,string reportPath)
{
if(RunActive||automaticTradeStop is not null||runtime.FlightAutomation.IsActive||NavigationActive||SystemFlightActive)throw new InvalidOperationException("请先结束当前驾驶流程再更新。");
var value=JsonSerializer.Deserialize<AutomaticTradeConfiguration>(await File.ReadAllTextAsync(configurationPath,lifetime.Token),ToolboxSettings.JsonOptions)
??throw new InvalidDataException("维护参数为空。");value.Validate();
var before=runtime.Player;RunActive=true;
try
{
var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,Path.Combine(value.WorkingDirectory,"operations"));
bootstrap.Progress+=message=>dispatcher.TryEnqueue(()=>{RunStatus="更新底层支持";RunMessage=message;runtime.Log("底层维护",message);});
await Task.Run(()=>bootstrap.UpgradeAndEnterAsync(lifetime.Token),lifetime.Token);
var after=runtime.Player;
if(before.Docked&&(!after.Docked||before.MarketId!=after.MarketId))throw new InvalidDataException("更新后的停靠地点未与原行程一致,未继续任何驾驶。");
await ReadPluginStatusAsync();RunStatus="已更新";RunMessage="底层支持已更新,保留当前行程;未启动跑商。";
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{passed=true,at=DateTimeOffset.UtcNow,automaticTradeStarted=false,
before=new{before.CommanderId,before.CurrentShipId,before.MarketId,before.GameMode,before.CargoCount},
after=new{after.CommanderId,after.CurrentShipId,after.MarketId,after.GameMode,after.CargoCount}},ToolboxSettings.JsonOptions),lifetime.Token);
}
catch(Exception error)
{
await File.WriteAllTextAsync(reportPath,JsonSerializer.Serialize(new{passed=false,at=DateTimeOffset.UtcNow,automaticTradeStarted=false,error=error.Message},ToolboxSettings.JsonOptions));throw;
}
finally{RunActive=false;RefreshTradePlanCommands();}
}
internal async Task RunAutomaticTradeAsync(string configurationPath,Action? beforeStart=null)
{
var value=await Task.Run(()=>JsonSerializer.Deserialize<AutomaticTradeConfiguration>(File.ReadAllText(configurationPath),ToolboxSettings.JsonOptions),lifetime.Token)
??throw new InvalidDataException("跑商参数为空。");
value.Validate();ApplyAutomaticParameters(value);
await File.WriteAllTextAsync(AutomaticRouteFile,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions),lifetime.Token);
// Configuration preparation can yield after an upstream authorization
// check. Reconfirm immediately before the new run owns any control;
// its long-lived token still belongs to this viewmodel, not the caller.
lifetime.Token.ThrowIfCancellationRequested();beforeStart?.Invoke();
await RunAutomaticParametersAsync(value,configurationPath);
}
private async Task RunAutomaticParametersAsync(AutomaticTradeConfiguration parameters,string path)
{
if(RunActive||automaticTradeStop is not null)throw new InvalidOperationException("工具箱已有跑商流程。");
automaticTradeConfigurationPath=Path.GetFullPath(path);automaticTradeParameters=parameters;
AutomaticSupplySummary="";
using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token);automaticTradeStop=stop;RunActive=true;HasError=false;
NativeTradeCommandDriver? driver=null;
AutomaticTradeRunSession? financialSession=null;
int? financialRoundsBaseline=null;
DateTimeOffset lastProtectionRecovery=default;
var sceneRecoveries=0;
async Task EnterWithConnectionSupervisionAsync(WindowsTradingBootstrap bootstrap)
{
var expected=runtime.Player;
await StartupConnectionRecovery.SuperviseAsync(cancellation=>Task.Run(()=>bootstrap.UpgradeAndEnterAsync(cancellation),cancellation),
async(failure,count,cancellation)=>
{
var current=runtime.Player;
if(current.CommanderId.Length>0&¤t.CommanderId!=expected.CommanderId
||current.CurrentShipId is {} ship&&ship!=expected.CurrentShipId||current.GameMode.Length>0&¤t.GameMode!=expected.GameMode)
throw new InvalidDataException("等待连接恢复期间指挥官、飞船或原模式改变,已停止自动跑商。");
var delay=TimeSpan.FromMinutes(Math.Min(10,count*2));
RunStatus="等待连接恢复";RunMessage=$"原版连接失败已完成本轮有界恢复,将在 {delay.TotalMinutes:N0} 分钟后核对并续接原行程;原订单、回执和累计收益保留,可随时取消。";HasError=false;
runtime.Log("连接恢复等待",$"第 {count} 轮:{failure.Message}");
await Task.Delay(delay,cancellation);
current=runtime.Player;
if(current.CommanderId.Length>0&¤t.CommanderId!=expected.CommanderId
||current.CurrentShipId is {} nextShip&&nextShip!=expected.CurrentShipId||current.GameMode.Length>0&¤t.GameMode!=expected.GameMode)
throw new InvalidDataException("等待连接恢复期间身份改变,已保留客户端和原订单。");
},stop.Token);
}
bool ProtectionRecoveryPending()
{
// Navigation may reject the new Status before the health poll's
// next tick. Consume that same fresh witness before handling failure.
shipProtection?.ObserveInterdiction(runtime.Player,DateTimeOffset.UtcNow);
return shipProtection?.LastTrigger is{} trigger&&(trigger.Reason is ShipProtectionReason.Interdicted or ShipProtectionReason.ShieldsLost or ShipProtectionReason.HullBelowHalf)
&&trigger.At>lastProtectionRecovery;
}
try
{
while(!stop.IsCancellationRequested)
{
try
{
var existingSession=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,Path.Combine(parameters.WorkingDirectory,"operations")){ResumeExistingCargo=true};
existingSession.Progress+=text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("跑商会话恢复",text);});
await EnterWithConnectionSupervisionAsync(existingSession);
await ReadPluginStatusAsync();
while(!runtime.Player.InGame||runtime.Player.CurrentShipId is null||runtime.Player.CargoCapacity<=0||runtime.Player.Rebuy<=0||!runtime.Player.CargoReliable||!runtime.Player.CreditsReliable)
{
RunStatus="等待游戏就绪";RunMessage="正在等待游戏载入及当前飞船、货舱和余额就绪,完成后自动继续。";HasError=false;
await Task.Delay(1000,stop.Token);
}
var current=runtime.Player;
var reserve=Math.Max(parameters.ReserveCredits,current.Rebuy);
RunStatus="正在启动";RunMessage="正在按当前飞船核对接口、货舱和保留资金。";
PlanProfit="正在读取实时货单";PlanHourly="货单就绪后估算";PlanDuration="正在估算航程";ProfitLabel="本次跑商已确认净利润";
driver=await CreateNativeTradeDriverAsync(new(),stop.Token,new(current.CargoCapacity,reserve),Path.Combine(parameters.WorkingDirectory,"operations"));
var flight=(NativeFlightControlDriver)runtime.FlightDriver!;var pid=flight.ProcessId;
async Task Recover(CancellationToken token)
{
await WindowsFlightRecovery.RecoverAsync(pid,NativeRelogChannel.SupportedGameSha256,()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,
ReleaseProtectionForControlledExitAsync,text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("跑商会话恢复",text);}),token);
var refreshed=await new WindowsGameSessionDriver(()=>runtime.Player,()=>PluginGameExecutable).FindTargetAsync(token);
if(refreshed.ProcessId!=pid)throw new SavingSceneRecoveryRequiredException(new IOException("已保护重启客户端,原订单等待在新实例接续。"));
}
async Task ValidateFlight(CancellationToken token)
{
var capability=await new NativeCruiseControlChannel(pid,NativeRelogChannel.SupportedGameSha256).PrepareAsync(Path.Combine(NativeSupportPackage.Directory,"CruiseControl"),token);
if(parameters.RequireSupercruiseOvercharge&&!capability.Available)
throw new NotSupportedException("当前飞船的原生SCO温度、燃油和启停接口尚未就绪,未采购或起飞。");
}
var engine=new AutomaticTradeEngine(parameters,driver,new NativeCommerceChannel(pid),()=>runtime.Player,
token=>EnsureNativeFlightProtectionAsync(pid,NativeRelogChannel.SupportedGameSha256,token),runtime.Settings.JournalDirectory,Recover,
(station,rows,token)=>runtime.Markets.SaveSnapshotAsync(new(station,rows,DateTimeOffset.UtcNow,DataSource.Journal),token),ValidateFlight,SelectAutomaticSupplyAsync,ProtectionRecoveryPending);
engine.Progress+=value=>
{
financialSession??=AutomaticTradeRunSession.Open(parameters.WorkingDirectory,value.NetProfit);
if(financialRoundsBaseline is null)
{
try{financialRoundsBaseline=AutomaticRoundsBaseline(parameters.WorkingDirectory,financialSession,value.CompletedRounds);}
catch(Exception error)when(error is IOException or UnauthorizedAccessException or JsonException){financialRoundsBaseline=value.CompletedRounds;runtime.Log("跑商统计",error.Message);}
}
var sessionProfit=financialSession.Profit(value.NetProfit);
dispatcher.TryEnqueue(()=>{
tradeExecutionStartedAt=financialSession.StartedAt;CompletedTradeRounds=Math.Max(0,value.CompletedRounds-financialRoundsBaseline.Value);
TradeConfirmedProfit=$"{sessionProfit:N0} Cr";RefreshTradeElapsed();
RunStatus=value.Phase switch{"requires-recovery"=>"需要处理","stopped"=>"已取消","safety-pause"=>"等待安全停靠","recovering-safety"=>"安全维修恢复中",_=>"自动跑商中"};
RunMessage=value.Message;NextDestination=value.NextDestination;ActualProfit=$"{sessionProfit:N0} Cr";ProfitLabel="本次跑商已确认净利润";
if(value.SupplySelection is{} supply)AutomaticSupplySummary=$"CG 货源:{supply.Source.DisplayName} · {supply.Order.Name} {supply.Order.Quantity:N0}/{supply.FreeCapacity:N0} t · 库存 {supply.Supply:N0} t · 预计 {supply.CreditsPerHour:N0} Cr/h · 风险 {supply.Delivery.RiskDisplay}";
ProfitDetails=$"自 {financialSession.StartedAt.ToLocalTime():MM-dd HH:mm} 起累计,自动重连和修复重启延续本次累计。";
if(value.Estimate is{} expected){PlanProfit=$"{expected.NetProfit:N0} Cr";PlanHourly=$"{expected.Hourly:N0} Cr/h";PlanDuration=$"约 {expected.Minutes:F1} 分钟";PlanWarnings=expected.Basis;}
runtime.Log("自动跑商",value.Message);
});
};
// This task and its leases are owned by the application, not by the caller or agent.
await Task.Run(()=>engine.RunAsync(stop.Token),stop.Token);
break;
}
catch(Exception)when(!stop.IsCancellationRequested&&ProtectionRecoveryPending())
{
var protection=shipProtection!;lastProtectionRecovery=protection.LastTrigger!.At;
if(driver is not null){try{await driver.ReleaseAllAsync();}catch(Exception error)when(error is IOException or InvalidOperationException or OperationCanceledException){}driver=null;}
RunStatus="飞船保护恢复中";RunMessage="飞船保护已退出客户端,正在通过官方启动器恢复原模式和原订单;健康保护继续生效。";HasError=false;
await protection.Completion.WaitAsync(stop.Token);
if(protection.RestartTarget is null)throw new IOException("拦截已保护退出,但尚未确认官方启动器的新游戏实例。");
// The next iteration enters the original mode in the verified
// replacement process and resumes its durable cargo checkpoint.
}
catch(Exception error)when(!stop.IsCancellationRequested&&FlightConnectionRecoveryRequiredException.Find(error) is not null)
{
var failure=FlightConnectionRecoveryRequiredException.Find(error)!;var binding=failure.Binding;
FlightConnectionRecovery.RequireCurrentIdentity(binding,runtime.Player,stop.Token);
if(driver is not null){await driver.ReleaseAllAsync();driver=null;}
var lifecycle=new WindowsGameSessionDriver(()=>runtime.Player,()=>PluginGameExecutable);
var failedClient=await lifecycle.FindLaunchTargetAsync(stop.Token);
FlightConnectionRecovery.RequireOriginalClient(binding,failedClient,PluginGameExecutable);
if(binding.GameSha256!=NativeRelogChannel.SupportedGameSha256)throw new InvalidDataException("原飞行连接故障的构建身份不匹配。");
FlightConnectionRecovery.RequireCurrentIdentity(binding,runtime.Player,stop.Token);
var operations=Path.Combine(parameters.WorkingDirectory,"operations");Directory.CreateDirectory(operations);
await File.WriteAllTextAsync(Path.Combine(operations,$"flight-connection-recovery-{binding.ProcessId}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.json"),
JsonSerializer.Serialize(new{at=DateTimeOffset.UtcNow,binding,client=failedClient,session=failure.Menu,navigation=failure.Navigation,
originalOrdersUnchanged=true,confirmedReceiptsPreserved=true,originalFailure=error.ToString()},ToolboxSettings.JsonOptions),stop.Token);
FlightConnectionRecovery.RequireCurrentIdentity(binding,runtime.Player,stop.Token);
RunStatus="正在恢复网络连接";RunMessage="原版已持续确认连接失败,正在按原模式有界恢复;先核对原订单,再续接原航程。";HasError=false;
var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,operations)
{ResumeExistingCargo=true,ExpectedConnectionFailureClient=failedClient,ExpectedFlightConnection=binding};
bootstrap.Progress+=text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("飞行连接恢复",text);});
await EnterWithConnectionSupervisionAsync(bootstrap);
await ReadPluginStatusAsync();
// The next engine instance reconciles durable intent/receipts
// before it can issue anything. Keep this run's financial base.
}
catch(Exception error)when(NativeOrderReconciliationRequiredException.Present(error)&&!stop.IsCancellationRequested)
{
if(driver is not null){await driver.ReleaseAllAsync();driver=null;}
RunStatus="正在核对原订单";RunMessage="原始请求返回服务器错误,正在通过官方会话重新核对货舱和余额。";HasError=false;
var recoveryCommander=runtime.Player.CommanderId;var recoveryShip=runtime.Player.CurrentShipId;
while(!runtime.Player.InGame||!runtime.Player.Docked||!runtime.Player.CargoReliable)
{
RunMessage="正在等待当前航程停靠,随后自动恢复官方会话并核对原订单;取消按钮仍可使用。";
await Task.Delay(1000,stop.Token);
}
if(runtime.Player.CommanderId!=recoveryCommander||runtime.Player.CurrentShipId!=recoveryShip)throw new InvalidDataException("等待订单核对期间指挥官或飞船已改变。");
var bootstrap=new WindowsTradingBootstrap(()=>runtime.Player,PluginGameExecutable,NativeSupportPackage.Directory,Path.Combine(parameters.WorkingDirectory,"operations")){ReconcileOriginalOrders=true};
bootstrap.Progress+=text=>dispatcher.TryEnqueue(()=>{RunMessage=text;runtime.Log("交易会话核对",text);});
await EnterWithConnectionSupervisionAsync(bootstrap);
await ReadPluginStatusAsync();
}
catch(Exception error)when(!stop.IsCancellationRequested&&SavingSceneRecoveryRequiredException.Present(error)&&FlightJourneyFailure.CanRecoverScene(error)&&sceneRecoveries<2)
{
sceneRecoveries++;
if(driver is not null){await driver.ReleaseAllAsync();driver=null;}
RunStatus="正在恢复游戏场景";RunMessage="已保留原订单,正在核对原版保存状态;未重复提交交易。";HasError=false;
// The next iteration observes the exact original transition for
// 90 seconds, then verifies the replacement session and cargo.
}
}
}
catch(OperationCanceledException)when(stop.IsCancellationRequested){RunStatus="已取消";RunMessage="已停止新增操作,原始订单与恢复记录保留。";}
catch(Exception error){AutomaticPluginUpdateRequired=error is NativeTradeSdkUpgradeRequiredException;RunStatus="需要处理";RunMessage=error.Message;ShowError(error.Message);runtime.Log("自动跑商",error.ToString());}
finally
{
if(driver is not null)try{await driver.ReleaseAllAsync();}catch(Exception error){ShowError(error.Message);}
automaticTradeStop=null;RunActive=false;RefreshTradePlanCommands();CalculateCommand.NotifyCanExecuteChanged();
}
}
}