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&¤t.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;
}
}
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&¤t.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;
}
}