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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Collections.ObjectModel;
using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Localization;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
using XFEEDToolBox.Core.Planning;

namespace XFEEDToolBox.ViewModels;

public sealed class TradeRouteCandidateViewModel(RoutePlan plan,int rank,IReadOnlyList<CommunityGoalInfo>? matchedGoals=null,bool syntheticFixture=false,DateTimeOffset? verifiedAt=null):ObservableObject
{
    public RoutePlan Plan{get;}=plan;
    private readonly IReadOnlyList<CommunityGoalInfo> communityGoals=matchedGoals??[];
    private readonly DateTimeOffset communityGoalReadAt=verifiedAt??DateTimeOffset.UtcNow;
    internal IReadOnlyList<CommunityGoalInfo> MatchedCommunityGoals=>communityGoals;
    private bool communityGoalActive=matchedGoals?.Any(goal=>goal.Active&&goal.Expires>DateTimeOffset.UtcNow)==true;
    private bool selected;
    public bool IsSelected
    {
        get=>selected;
        set{if(SetProperty(ref selected,value)){OnPropertyChanged(nameof(CardBorder));OnPropertyChanged(nameof(SelectedVisibility));}}
    }
    public Brush CardBorder=>new SolidColorBrush(IsCommunityGoal
        ?IsSelected?Windows.UI.Color.FromArgb(255,195,166,255):Windows.UI.Color.FromArgb(255,102,80,144)
        :IsSelected?Windows.UI.Color.FromArgb(255,255,174,72):Windows.UI.Color.FromArgb(255,37,52,72));
    public bool IsCommunityGoal=>communityGoalActive;
    public Brush RouteAccentBrush=>new SolidColorBrush(IsCommunityGoal?Windows.UI.Color.FromArgb(255,195,166,255):Windows.UI.Color.FromArgb(255,255,174,72));
    public Brush CommunityGoalBadgeBackground=>new SolidColorBrush(Windows.UI.Color.FromArgb(40,166,120,245));
    public Visibility CommunityGoalVisibility=>IsCommunityGoal?Visibility.Visible:Visibility.Collapsed;
    public string CommunityGoalLabel=>syntheticFixture?"CG 活动交付 · 合成夹具":"含 CG 活动交付货物";
    public string CommunityGoalDescription=>string.Join(Environment.NewLine,communityGoals.Select(goal=>$"{goal.Title} · {goal.StationName} · 截止 {goal.Expires.ToLocalTime():MM-dd HH:mm}"))
        +(syntheticFixture?"\n离线合成界面夹具,不是真实活动、资金、行情或交易。":$"\n原目录核验于 {communityGoalReadAt.ToLocalTime():HH:mm:ss};货单包含目标活动接收的商品。额外CG奖励未计入预计收益,执行前仍须重新核验。");
    internal void RefreshCommunityGoalValidity(DateTimeOffset now)
    {
        var active=now>=communityGoalReadAt&&now-communityGoalReadAt<=TimeSpan.FromMinutes(2)&&communityGoals.Any(goal=>goal.Active&&goal.Expires>now);
        if(active==communityGoalActive)return;communityGoalActive=active;
        foreach(var name in new[]{nameof(IsCommunityGoal),nameof(CardBorder),nameof(RouteAccentBrush),nameof(CommunityGoalVisibility)})OnPropertyChanged(name);
    }
    public Visibility SelectedVisibility=>IsSelected?Visibility.Visible:Visibility.Collapsed;
    public string Rank=>$"{rank:00}";
    public string Recommendation=>syntheticFixture?"合成夹具":rank==1?"推荐":"备选";
    public string Origin=>Plan.Legs[0].From.Name;
    public string Destination=>Plan.Legs[0].To.Name;
    public string OriginDetail=>Plan.Legs[0].From.SystemName;
    public string DestinationDetail=>Plan.Legs[0].To.SystemName;
    public string RouteKind=>Plan.IsLoop?(Plan.Legs.Count>2?$"{Plan.Legs.Count} 段环线":"往返环线"):$"{Plan.Legs.Count} 段航线";
    public string RouteSummary=>string.Join(" → ",new[]{Plan.Legs[0].From.Name}.Concat(Plan.Legs.Select(leg=>leg.To.Name)));
    public string Hourly=>Plan.CreditsPerHour>=1_000_000?$"{Plan.CreditsPerHour/1_000_000:F1}M":$"{Plan.CreditsPerHour:N0}";
    public string HourlyExact=>$"{Plan.CreditsPerHour:N0} Cr/h";
    public string Risk=>Plan.RiskTier==TradeRiskTier.Unknown?"风险信息不足":Plan.RiskDisplay+"风险";
    public Brush RiskBrush=>new SolidColorBrush(Plan.RiskTier switch
    {
        TradeRiskTier.Low=>Windows.UI.Color.FromArgb(255,84,222,200),
        TradeRiskTier.Moderate=>Windows.UI.Color.FromArgb(255,255,174,72),
        TradeRiskTier.High or TradeRiskTier.VeryHigh=>Windows.UI.Color.FromArgb(255,255,134,121),
        _=>Windows.UI.Color.FromArgb(255,145,163,185)
    });
    public string Profit=>$"{Plan.NetProfit:N0} Cr";
    public string Duration=>$"{Plan.Minutes:F1} 分钟";
    public string Distance=>$"{Plan.DeadheadDistanceLy+Plan.Legs.Sum(leg=>leg.DistanceLy):F1} ly";
    public string Jumps=>$"{Plan.DeadheadJumps+Plan.Legs.Sum(leg=>leg.EstimatedJumps)} 跳";
    public string Cargo=>string.Join(Environment.NewLine,Plan.Legs.Select((leg,index)=>
        $"{CargoLabel(index)}  {(leg.Orders.Count==0?"空载转场":leg.Orders[0].Summary)}{(leg.Orders.Count>1?$" · 另 {leg.Orders.Count-1} 种商品":"")}"));
    public string CargoDetails=>string.Join(Environment.NewLine,Plan.Legs.Select((leg,index)=>
        $"{CargoLabel(index)} · {leg.From.Name} → {leg.To.Name}:{leg.CargoSummary}"));
    private string CargoLabel(int index)=>Plan.Legs.Count==1?"货物":Plan.IsLoop&&Plan.Legs.Count==2?(index==0?"去程":"返程"):$"第 {index+1} 段";
    public string Approach=>Plan.DeadheadMinutes>0?$"含前往起点 {Plan.DeadheadDistanceLy:F1} ly / {Plan.DeadheadJumps} 跳":"包含各站停靠与航行时间";
}

public partial class ToolboxViewModel
{
    private bool updatingInvestment,investmentFollowsBalance=true,tradeInputsInitialized,selectingTradeCandidate,manualTradeConfigured;
    private DispatcherQueueTimer? tradeExecutionTimer;
    private DateTimeOffset? tradeExecutionStartedAt;
    private bool plannedRoundCounted;
    private sealed record TradeRunMetrics(string SessionId,int BaselineRounds);

    [ObservableProperty,NotifyPropertyChangedFor(nameof(TradeAdvancedVisibility),nameof(TradeEditLabel))]
    public partial bool TradeAdvancedEditing{get;set;}
    [ObservableProperty] public partial double TradeInvestment{get;set;}
    [ObservableProperty] public partial string TradePlanningStatus{get;set;}="输入投入资金,比较可行路线。";
    [ObservableProperty] public partial string TradeEmptyTitle{get;set;}="规划下一趟航程";
    [ObservableProperty] public partial string TradeEmptyDescription{get;set;}="飞船、货舱与余额会自动读取,点击规划路线即可比较方案。";
    [ObservableProperty] public partial string TradeCommunityGoalStatus{get;set;}="CG 标记需核实有效活动、目标站和接收商品;额外奖励不计入预计收益。";
    internal object? TradeCommunityGoalReadForProbe{get;private set;}
    private DateTimeOffset? tradeCommunityGoalVerifiedAt;
    internal TradePlanningDiagnostics? TradePlanningDiagnosticForProbe{get;private set;}
    [ObservableProperty] public partial int CompletedTradeRounds{get;set;}
    [ObservableProperty] public partial string TradeConfirmedProfit{get;set;}="0 Cr";
    [ObservableProperty] public partial string TradeElapsed{get;set;}="00:00:00";
    [ObservableProperty] public partial TradeRouteCandidateViewModel? SelectedTradeCandidate{get;set;}
    public ObservableCollection<TradeRouteCandidateViewModel> TradeCandidates{get;}=[];
    public Visibility TradeAdvancedVisibility=>TradeAdvancedEditing?Visibility.Visible:Visibility.Collapsed;
    public string TradeEditLabel=>TradeAdvancedEditing?"收起高级搜索选项":"高级搜索选项";
    public Visibility TradeEmptyVisibility=>TradeCandidates.Count==0?Visibility.Visible:Visibility.Collapsed;
    public Visibility TradePlanVisibility=>Plan?.Legs.Count>0?Visibility.Visible:Visibility.Collapsed;
    public bool TradeSelectionEnabled=>!RunActive&&!IsBusy;
    public string TradeShipName=>CurrentPlayer.Ships.FirstOrDefault(ship=>ship.ShipId==CurrentPlayer.CurrentShipId)?.DisplayName
        ??(string.IsNullOrWhiteSpace(CurrentPlayer.Ship)?"等待飞船数据":ChineseNames.Ship(CurrentPlayer.Ship));
    public string TradeCargoSummary=>CurrentPlayer.CargoCapacity>0?$"可用货舱 {CurrentPlayer.FreeCargo:N0} / {CurrentPlayer.CargoCapacity:N0} t":"等待货舱数据";
    public string TradeJumpSummary=>CurrentPlayer.MaxJumpRange is >0?$"估算满载跳距 {CurrentPlayer.MaxJumpRange.Value*.8:F1} ly":"等待跳距数据";
    public string TradeOriginSummary=>string.IsNullOrWhiteSpace(CurrentPlayer.SystemName)?"等待当前位置":"从 "+CurrentPlayer.SystemName+" 出发 · 自动比较范围内货源";
    private double EffectiveTradeReserve=>double.IsFinite(Reserve)?Math.Max(0,Reserve):(double)CurrentPlayer.Rebuy;
    public string TradeFundsSummary=>CurrentPlayer.Credits is {} credits
        ?$"可投入 {Math.Max(0,credits-(decimal)EffectiveTradeReserve):N0} Cr · 已留出 {EffectiveTradeReserve:N0} Cr 运营与保险资金"
        :"等待读取当前余额,投入金额将自动填入。";
    public string TradeInvestmentMode=>investmentFollowsBalance?"全部可用资金":"自定义投入";
    public string TradeSelectedRouteSummary=>RunActive&&automaticTradeStop is not null&&automaticTradeParameters is{} automatic
        ?$"{(automatic.Mode==XFEEDToolBox.Core.Execution.AutomaticTradeMode.CommunityGoal?"CG 重复跑商":"重复跑商")} · 交付站 {automatic.Destination.DisplayName}"
        :SelectedTradeCandidate?.RouteSummary
        ??(manualTradeConfigured&&SelectedStart is not null&&AutomaticDestination is not null?SelectedStart.Record.Name+" → "+AutomaticDestination.Record.Name:"选择方案后即可开始执行");
    public bool TradeHasExistingCargo=>SelectedTradeCandidate is {} candidate&&(candidate.Plan.Request.OccupiedCargo>0||CurrentPlayer.CargoCount>0);
    public bool TradeExecutesFullPlan=>SelectedTradeCandidate is {} candidate&&(candidate.Plan.IsCargoAlternative||!candidate.Plan.IsLoop||candidate.Plan.Legs.Count!=2
        ||candidate.Plan.Request.OccupiedCargo>0||candidate.Plan.Legs[0].Orders.Count==0
        ||candidate.Plan.Legs.SelectMany(leg=>new[]{leg.From.MarketId,leg.To.MarketId}).Distinct().Count()>2);
    public string TradeExecutionActionLabel=>SelectedTradeCandidate is null?"开始执行":TradeExecutesFullPlan?"执行此路线":"开始循环";
    public string TradeExecutionPolicy=>TradeHasExistingCargo?"已保留现有货物用于规划;自动执行前请先处理现有货物。":SelectedTradeCandidate is null?(manualTradeConfigured?"按手动站点与跑商方式执行,收益只统计已确认成交。":"收益按实际成交与费用回执累计。")
        :TradeExecutesFullPlan?"完整路线执行一遍后停止;需先停靠起点。":"按所选两站循环执行,采购前重新核对行情;可随时取消。";

    [RelayCommand] private void ToggleTradeAdvanced()=>TradeAdvancedEditing=!TradeAdvancedEditing;
    [RelayCommand] private void UseAllTradeFunds()
    {
        investmentFollowsBalance=true;SetTradeInvestment(Math.Max(0,Cash-EffectiveTradeReserve));
        OnPropertyChanged(nameof(TradeInvestmentMode));InvalidateTradeCandidates();
    }
    private void SetTradeInvestment(double value)
    {updatingInvestment=true;try{TradeInvestment=value;}finally{updatingInvestment=false;}}
    partial void OnTradeInvestmentChanged(double value)
    {if(!updatingInvestment){investmentFollowsBalance=!double.IsFinite(value);OnPropertyChanged(nameof(TradeInvestmentMode));InvalidateTradeCandidates();}}
    partial void OnReserveChanged(double value)
    {
        if(investmentFollowsBalance)SetTradeInvestment(Math.Max(0,Cash-EffectiveTradeReserve));
        OnPropertyChanged(nameof(TradeFundsSummary));
        InvalidateTradeCandidates();
    }
    partial void OnCashChanged(double value)
    {if(investmentFollowsBalance)SetTradeInvestment(Math.Max(0,value-EffectiveTradeReserve));InvalidateTradeCandidates();}
    partial void OnTradeAdvancedEditingChanged(bool value)
    {if(value&&SelectedStart is not null&&AutomaticDestination is not null)manualTradeConfigured=true;RefreshTradePlanCommands();}
    private void NoteManualTradeConfiguration()
    {if(TradeAdvancedEditing&&!selectingTradeCandidate)manualTradeConfigured=true;}
    partial void OnSelectedTradeCandidateChanged(TradeRouteCandidateViewModel? value)
    {
        foreach(var candidate in TradeCandidates)candidate.IsSelected=ReferenceEquals(candidate,value);
        if(value is null)return;
        selectingTradeCandidate=true;
        try
        {
        var plan=value.Plan;
        var rows=Stations.Select(station=>station.Record).Concat(plan.Legs.SelectMany(leg=>new[]{leg.From,leg.To})).DistinctBy(station=>station.MarketId).ToArray();
        Stations.Apply(rows);
        Plan=plan;SelectedLeg=plan.Legs.FirstOrDefault();
        SelectedStart=Stations.First(station=>station.MarketId==plan.Legs[0].From.MarketId);
        AutomaticDestination=Stations.First(station=>station.MarketId==plan.Legs[0].To.MarketId);
        var cgGoals=plan.Request.CommunityGoalSupplyOnly?value.MatchedCommunityGoals.Where(goal=>goal.MarketId==plan.Legs[0].To.MarketId).ToArray():[];
        AutomaticModeIndex=cgGoals.Length>0?1:0;AutomaticReturnTrade=cgGoals.Length==0&&plan.IsLoop;
        if(cgGoals.Length>0)AutomaticCommodities=string.Join(", ",cgGoals.SelectMany(goal=>goal.AcceptedCommodities).Select(goods=>CommodityCatalog.Key(goods))
            .Where(goods=>plan.Request.CommunityGoalCommodityFilter is not{Length:>0}||plan.Request.CommunityGoalCommodityFilter.Contains(goods,StringComparer.Ordinal)).Distinct(StringComparer.Ordinal));
        PlanProfit=$"{plan.NetProfit:N0} Cr";PlanHourly=$"{plan.CreditsPerHour:N0} Cr/h";PlanDuration=$"{plan.Minutes:F1} 分钟";
        PlanInfo=$"{plan.Legs.Count} 段{(plan.IsLoop?"环线":"航线")} · {plan.StationsConsidered} 个候选站点";
        PlanWarnings=string.Join(Environment.NewLine,plan.Warnings);
        NotifyTradePlanningSummary();RefreshTradePlanCommands();
        }
        finally{selectingTradeCandidate=false;}
    }
    private void SetTradeCandidates(IReadOnlyList<RoutePlan> plans,TradePlanningDiagnostics? diagnostics=null,IReadOnlyList<CommunityGoalInfo>? goals=null,bool syntheticFixture=false)
    {
        var now=DateTimeOffset.UtcNow;
        if(TradePlanningModeIndex==1)
        {
            var valid=plans.Where(plan=>plan.Request.CommunityGoalSupplyOnly&&CommunityGoalSupplyScope.MatchesRoute(goals??[],plan,now)
                &&(syntheticFixture||tradeCommunityGoalVerifiedAt is {} at&&now>=at&&now-at<=TimeSpan.FromMinutes(2))).ToArray();
            if(plans.Count>0&&valid.Length==0&&diagnostics is not null)
                diagnostics=diagnostics with{Code="cg-directory-changed",Message="CG 活动目录已过期或发生变化,请重新规划;未沿用普通路线或此前活动结果。"};
            plans=valid;
        }
        TradePlanningDiagnosticForProbe=diagnostics;
        if(diagnostics is null){TradeCommunityGoalReadForProbe=null;tradeCommunityGoalVerifiedAt=null;TradeCommunityGoalStatus="CG 标记需核实有效活动、目标站和接收商品;额外奖励不计入预计收益。";}
        SelectedTradeCandidate=null;TradeCandidates.Clear();
        foreach(var plan in plans.Take(3))TradeCandidates.Add(new(plan,TradeCandidates.Count+1,CommunityGoalCatalogue.MatchRoute(goals??[],plan,DateTimeOffset.UtcNow),syntheticFixture,tradeCommunityGoalVerifiedAt));
        SelectedTradeCandidate=TradeCandidates.FirstOrDefault();
        if(TradeCandidates.Count==0){Plan=null;SelectedLeg=null;PlanInfo="尚无可确认的盈利路线。";TradeEmptyTitle="尚无可确认的盈利路线";
            TradeEmptyDescription=diagnostics?.Message??"请先核对所选范围内的行情与飞船参数,再重新规划。";PlanInfo=TradeEmptyDescription;}
        TradePlanningStatus=TradeCandidates.Count>0?$"{TradeCandidates.Count} 个方案 · 按预计净利润 / 小时排序"+(diagnostics is not null?$" · {diagnostics.FreshStations} 个新鲜行情站点":"")
            :diagnostics?.Message??"尚无可确认的盈利路线。";
        if(TradeCandidates.Count>0&&diagnostics is not null)
        {if(diagnostics.InputTruncated)TradePlanningStatus+=" · 站点输入受限";if(diagnostics.SearchPruned)TradePlanningStatus+=" · 有界路线搜索";}
        OnPropertyChanged(nameof(TradeEmptyVisibility));OnPropertyChanged(nameof(TradePlanVisibility));
    }
    private void SetTradePlanningIssue(string title,string message)
    {SetTradeCandidates([]);TradeEmptyTitle=title;TradeEmptyDescription=message;TradePlanningStatus=PlanInfo=message;}
    private void InvalidateTradeCandidates()
    {
        if(selectingTradeCandidate||RunActive)return;
        Interlocked.Increment(ref tradePlanningInputVersion);
        if(TradeCandidates.Count==0)return;
        SetTradeCandidates([]);TradePlanningStatus="参数已更改,请重新规划路线。";
        TradeEmptyTitle="重新规划路线";TradeEmptyDescription="投入、范围或约束已更改,点击规划路线重新比较。";
    }
    private void RefreshTradePlanningPlayer(PlayerState player,bool force=false)
    {
        RefreshTradePlanningIdentity(player);
        RefreshTradeShipPad();
        Cash=player.Credits is {} credits?(double)credits:0;
        if(force||!tradeInputsInitialized)Reserve=(double)player.Rebuy;else if(Reserve<(double)player.Rebuy)Reserve=(double)player.Rebuy;
        Hold=player.CargoCapacity;JumpRange=player.MaxJumpRange??0;
        if(force){investmentFollowsBalance=true;SetTradeInvestment(Math.Max(0,Cash-EffectiveTradeReserve));}
        if((force||!TradeAdvancedEditing&&Plan is null&&!RunActive)&&player.MarketId is {} market)
            SelectedStart=Stations.FirstOrDefault(station=>station.MarketId==market);
        tradeInputsInitialized=true;NotifyTradePlanningSummary();RefreshTradePlanCommands();
    }
    private void NotifyTradePlanningSummary()
    {
        foreach(var candidate in TradeCandidates)candidate.RefreshCommunityGoalValidity(DateTimeOffset.UtcNow);
        if(TradePlanningModeIndex==1&&!IsBusy&&!RunActive&&TradeCandidates.Any(candidate=>!candidate.IsCommunityGoal))
            SetTradePlanningIssue("CG 目录需要刷新","活动有效期或目录核验时间已到,请重新规划 CG 供货路线;未保留普通推荐。");
        foreach(var name in new[]{nameof(TradeShipName),nameof(TradeCargoSummary),nameof(TradeJumpSummary),nameof(TradeOriginSummary),nameof(TradeFundsSummary),nameof(TradeInvestmentMode),nameof(TradeSelectedRouteSummary),nameof(TradePlanVisibility),nameof(TradeHasExistingCargo),nameof(TradeExecutionActionLabel),nameof(TradeExecutionPolicy)})OnPropertyChanged(name);
    }
    private async Task<IReadOnlyList<CommunityGoalInfo>> ReadTradeCommunityGoalsForPlanningAsync(bool demo,CancellationToken token)
    {
        tradeCommunityGoalVerifiedAt=null;
        TradeCommunityGoalReadForProbe=new{at=DateTimeOffset.UtcNow,attempted=false,readOnly=true,joinSent=false,gameInputsSent=0,selectionChangesSent=0};
        var player=CurrentPlayer;
        if(demo||!player.InGame||!player.Docked||player.InDanger||player.MarketId is not >0||player.CurrentShipId is null||player.LoadGameAt is null
            ||string.IsNullOrWhiteSpace(player.CommanderId)||runtime.FlightDriver is not NativeFlightControlDriver driver)
        {TradeCommunityGoalStatus=TradePlanningModeIndex==1?"请在停靠且原生接口就绪后核对 CG 目录;当前不能推荐 CG 供货路线,未回退普通跑商。":"CG 活动目录尚未核验;已知市场利润仍可比较,额外奖励未计入。";return [];}
        using var deadline=CancellationTokenSource.CreateLinkedTokenSource(token);deadline.CancelAfter(TimeSpan.FromSeconds(3));
        bool SameContext()=>CurrentPlayer.InGame&&CurrentPlayer.Docked&&!CurrentPlayer.InDanger&&CurrentPlayer.CommanderId==player.CommanderId&&CurrentPlayer.CurrentShipId==player.CurrentShipId
            &&CurrentPlayer.LoadGameAt==player.LoadGameAt&&CurrentPlayer.MarketId==player.MarketId&&runtime.FlightDriver is NativeFlightControlDriver current&&current.ProcessId==driver.ProcessId;
        try
        {
            var read=await new NativeCommunityGoalClient(new NativeCommerceChannel(driver.ProcessId),player.MarketId.Value).ReadAsync(false,deadline.Token);
            token.ThrowIfCancellationRequested();
            if(!SameContext())
            {TradeCommunityGoalStatus="会话或停靠地点已改变,未沿用此前CG目录。";TradeCommunityGoalReadForProbe=new{at=DateTimeOffset.UtcNow,attempted=true,readOnly=true,contextMatched=false,joinSent=false,gameInputsSent=0,selectionChangesSent=0};return [];}
            TradeCommunityGoalReadForProbe=new{at=DateTimeOffset.UtcNow,attempted=true,readOnly=true,contextMatched=true,processId=driver.ProcessId,
                read.Generation,read.Proof,goalCount=read.Goals.Count,mutationsSent=0,joinSent=false,gameInputsSent=0,selectionChangesSent=0};
            TradeCommunityGoalStatus="CG 标记依据本次原游戏目录匹配;额外活动奖励未计入预计收益。";
            tradeCommunityGoalVerifiedAt=DateTimeOffset.UtcNow;
            runtime.Log("CG 路线",$"只读目录核验:generation {read.Generation},{read.Goals.Count} 个活动记录;0报名、0交易、0游戏输入。");
            return read.Goals;
        }
        catch(OperationCanceledException)when(token.IsCancellationRequested){throw;}
        catch(Exception error)
        {
            TradeCommunityGoalStatus=TradePlanningModeIndex==1?"CG 活动目录暂不可用,当前不能推荐 CG 供货路线;未回退普通跑商。":"CG 活动目录暂不可用,未标记活动路线;额外奖励未计入。";
            TradeCommunityGoalReadForProbe=new{at=DateTimeOffset.UtcNow,attempted=true,readOnly=true,verified=false,issue=error.GetType().Name,joinSent=false,gameInputsSent=0,selectionChangesSent=0};
            runtime.Log("CG 路线",error.ToString());return [];
        }
    }
    private void RefreshTradeExecutionTimer(bool active)
    {
        OnPropertyChanged(nameof(TradeSelectionEnabled));
        if(active)
        {
            tradeExecutionStartedAt??=DateTimeOffset.UtcNow;
            if(tradeExecutionTimer is null)
            {
                tradeExecutionTimer=dispatcher.CreateTimer();tradeExecutionTimer.Interval=TimeSpan.FromSeconds(1);
                tradeExecutionTimer.Tick+=(_,_)=>RefreshTradeElapsed();
            }
            tradeExecutionTimer.Start();
        }
        else{RefreshTradeElapsed();tradeExecutionTimer?.Stop();}
    }
    private void RefreshTradeElapsed()
    {
        if(tradeExecutionStartedAt is not {} started)return;
        var elapsed=DateTimeOffset.UtcNow-started;if(elapsed<TimeSpan.Zero)elapsed=TimeSpan.Zero;
        TradeElapsed=$"{(int)elapsed.TotalHours:00}:{elapsed.Minutes:00}:{elapsed.Seconds:00}";
    }
    private void BeginPlannedTradeMetrics()
    {
        tradeExecutionStartedAt=DateTimeOffset.UtcNow;plannedRoundCounted=false;
        CompletedTradeRounds=0;TradeConfirmedProfit="0 Cr";TradeElapsed="00:00:00";
    }
    private static int AutomaticRoundsBaseline(string root,AutomaticTradeRunSession session,int completedRounds)
    {
        var path=Path.Combine(root,"run-metrics.json");
        if(File.Exists(path)&&JsonSerializer.Deserialize<TradeRunMetrics>(File.ReadAllText(path),ToolboxSettings.JsonOptions) is {} existing&&existing.SessionId==session.Id)
            return existing.BaselineRounds;
        var value=new TradeRunMetrics(session.Id,completedRounds);var temporary=path+".tmp";
        File.WriteAllText(temporary,JsonSerializer.Serialize(value,ToolboxSettings.JsonOptions));File.Move(temporary,path,true);return value.BaselineRounds;
    }
}