XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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&&current.CommanderId!=expected.CommanderId
                        ||current.CurrentShipId is {} ship&&ship!=expected.CurrentShipId||current.GameMode.Length>0&&current.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&&current.CommanderId!=expected.CommanderId
                        ||current.CurrentShipId is {} nextShip&&nextShip!=expected.CurrentShipId||current.GameMode.Length>0&&current.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();
        }
    }
}