using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Planning;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Presentation;
namespace XFEEDToolBox.ViewModels;
public sealed class TradeSourceKindOptionViewModel(StationSourceKinds kind,string label):ObservableObject
{
public StationSourceKinds Kind{get;}=kind;
public string Label{get;}=label;
private bool selected;
public bool IsSelected{get=>selected;set{if(SetProperty(ref selected,value))Changed?.Invoke();}}
internal event Action? Changed;
}
public partial class ToolboxViewModel
{
private bool updatingTradeSources;
private bool tradePlanningFixtureActive;
public int TradePlanningModeIndex{get=>AutomaticModeIndex;set=>AutomaticModeIndex=Math.Clamp(value,0,1);}
public string TradePlanningModeDescription=>TradePlanningModeIndex==1?"只比较有效 CG 的接收商品,按预计净利润 / 小时排序。":"比较范围内全部合规市场的预计净利润 / 小时。";
private void TradePlanningModeChanged(int value)
{
OnPropertyChanged(nameof(TradePlanningModeIndex));OnPropertyChanged(nameof(TradePlanningModeDescription));
InvalidateTradeCandidates();
if(initialized&&!selectingTradeCandidate&&!tradePlanningFixtureActive&&!OfflinePreview&&!DemoMode)_=SaveTradePlanningModeAsync(value==1);
}
[ObservableProperty] public partial StationLiveRow? TradeSourceSearchStation{get;set;}
[ObservableProperty] public partial StationLiveRow? TradeDestinationSearchStation{get;set;}
[ObservableProperty] public partial string TradeSourceSearchText{get;set;}="";
[ObservableProperty] public partial string TradeDestinationSearchText{get;set;}="";
[ObservableProperty] public partial string TradeSourceSystemText{get;set;}="";
[ObservableProperty] public partial string TradeDestinationSystemText{get;set;}="";
[ObservableProperty] public partial string TradeCommoditySearchText{get;set;}="";
private long? tradeSourceSearchMarketId,tradeDestinationSearchMarketId;
private bool updatingTradeSearch;
partial void OnTradeSourceSystemTextChanged(string value)=>InvalidateTradeCandidates();
partial void OnTradeDestinationSystemTextChanged(string value)=>InvalidateTradeCandidates();
partial void OnTradeCommoditySearchTextChanged(string value)=>InvalidateTradeCandidates();
partial void OnTradeSourceSearchTextChanged(string value)
{
if(updatingTradeSearch)return;
var matches=TradeSourceSearchStation is {} station&&(value.Trim().Equals(station.Record.Name,StringComparison.OrdinalIgnoreCase)
||value.Trim().Equals(station.DisplayName,StringComparison.OrdinalIgnoreCase));
tradeSourceSearchMarketId=matches?TradeSourceSearchStation!.MarketId:null;
if(!matches)TradeSourceSearchStation=null;
InvalidateTradeCandidates();
}
partial void OnTradeDestinationSearchTextChanged(string value)
{
if(updatingTradeSearch)return;
var matches=TradeDestinationSearchStation is {} station&&(value.Trim().Equals(station.Record.Name,StringComparison.OrdinalIgnoreCase)
||value.Trim().Equals(station.DisplayName,StringComparison.OrdinalIgnoreCase));
tradeDestinationSearchMarketId=matches?TradeDestinationSearchStation!.MarketId:null;
if(!matches)TradeDestinationSearchStation=null;
InvalidateTradeCandidates();
}
partial void OnTradeSourceSearchStationChanged(StationLiveRow? value)
{
tradeSourceSearchMarketId=value?.MarketId;
if(value is not null){updatingTradeSearch=true;try{TradeSourceSearchText=value.Record.Name;SelectedStart=value;}finally{updatingTradeSearch=false;}}
InvalidateTradeCandidates();
}
partial void OnTradeDestinationSearchStationChanged(StationLiveRow? value)
{
tradeDestinationSearchMarketId=value?.MarketId;
if(value is not null){updatingTradeSearch=true;try{TradeDestinationSearchText=value.Record.Name;AutomaticDestination=value;}finally{updatingTradeSearch=false;}}
InvalidateTradeCandidates();
}
private async Task SaveTradePlanningModeAsync(bool cgOnly)
{
try{await runtime.SaveSettingsAsync(runtime.Settings with{Planner=runtime.Settings.Planner with{CommunityGoalSupplyOnly=cgOnly}});}
catch(Exception error){runtime.Log("跑商规划类型",error.Message);}
}
private int tradePlanningOperation;
private long tradePlanningInputVersion;
internal long TradePlanningInputVersionForProbe=>Volatile.Read(ref tradePlanningInputVersion);
private sealed record TradePlanningPlayerKey(string CommanderId,long? ShipId,string ShipType,int CargoCount,long? MarketId,string SystemName,Coordinates? Position,bool InGame);
private TradePlanningPlayerKey? tradePlanningPlayerKey;
public IReadOnlyList<TradeSourceKindOptionViewModel> TradeSourceKinds{get;}=
[
new(StationSourceKinds.Starport,"星港"),new(StationSourceKinds.Outpost,"轨道前哨"),
new(StationSourceKinds.PlanetaryPort,"行星港"),new(StationSourceKinds.Settlement,"地面据点"),
new(StationSourceKinds.FleetCarrier,"舰队航母"),new(StationSourceKinds.AsteroidBase,"小行星基地"),
new(StationSourceKinds.Megaship,"巨舰"),new(StationSourceKinds.Other,"其他已知类型"),
new(StationSourceKinds.Unknown,"未知类型")
];
public StationSourceKinds SelectedTradeSourceKinds=>TradeSourceKinds.Where(option=>option.IsSelected).Aggregate((StationSourceKinds)0,(value,option)=>value|option.Kind);
public string TradeSourceSelectionSummary=>SelectedTradeSourceKinds==0?"未限定货源类型 · 使用默认全部已知类型,未知类型未启用":$"已选 {TradeSourceKinds.Count(option=>option.IsSelected)} 类货源 · "+
(TradeSourceKinds.Any(option=>option.Kind==StationSourceKinds.Unknown&&option.IsSelected)?"已单独允许未知类型":"未知类型未启用");
public LandingPad TradeActualShipPad=>ShipPadPolicy.RequiredPad(CurrentPlayer.Ship);
public bool TradeShipPadKnown=>TradeActualShipPad>LandingPad.Unknown;
public bool TradeSmallPadOptionAvailable=>TradeActualShipPad==LandingPad.Small;
public bool TradeMediumPadOptionAvailable=>TradeActualShipPad is LandingPad.Small or LandingPad.Medium;
public bool TradeLargePadOptionAvailable=>TradeShipPadKnown;
public string TradeShipPadSummary=>TradeShipPadKnown
?$"当前飞船至少需要 {PadLabel(TradeActualShipPad)} 停机坪;每个采购与交付站都会核对。"
:"等待识别当前飞船尺寸;确认前不能生成可执行路线。";
public string TradeEffectivePadSummary=>TradeShipPadKnown
?$"本次要求:{PadLabel((LandingPad)Math.Max((int)TradeActualShipPad,Math.Clamp(PadIndex,0,2)+1))};手动选择只能更严格。"
:"未识别飞船尺寸时,手动停机坪设置不能替代船型核验。";
private static string PadLabel(LandingPad pad)=>pad switch{LandingPad.Small=>"小型 S",LandingPad.Medium=>"中型 M",LandingPad.Large=>"大型 L",_=>"未知"};
[ObservableProperty,NotifyPropertyChangedFor(nameof(TradeProgressVisibility))]public partial bool TradePlanningBusy{get;set;}
[ObservableProperty]public partial string TradeProgressStage{get;set;}="";
[ObservableProperty]public partial string TradeProgressDetail{get;set;}="";
[ObservableProperty,NotifyPropertyChangedFor(nameof(TradeProgressPercent),nameof(TradeProgressCount))]public partial long TradeProgressProcessed{get;set;}
[ObservableProperty,NotifyPropertyChangedFor(nameof(TradeProgressIsIndeterminate),nameof(TradeProgressPercent),nameof(TradeProgressCount))]public partial long? TradeProgressTotal{get;set;}
public Visibility TradeProgressVisibility=>TradePlanningBusy?Visibility.Visible:Visibility.Collapsed;
public bool TradeProgressIsIndeterminate=>TradeProgressTotal is null;
public double TradeProgressPercent=>TradeProgressTotal is >0?Math.Clamp(TradeProgressProcessed*100d/TradeProgressTotal.Value,0,100):0;
public string TradeProgressCount=>TradeProgressTotal is {} total?$"{TradeProgressProcessed:N0} / {total:N0}":$"已处理 {TradeProgressProcessed:N0} · 总量待确认";
internal IReadOnlyList<TradePlanningProgress> TradeProgressForProbe=>tradeProgressEvidence.ToArray();
private readonly List<TradePlanningProgress> tradeProgressEvidence=[];
private void InitializeTradePlanningOptions(PlannerRequest saved)
{
TradePlanningModeIndex=saved.CommunityGoalSupplyOnly?1:0;
TradeSourceSearchText=saved.SourceStationFilter??"";TradeDestinationSearchText=saved.DestinationStationFilter??"";
TradeSourceSystemText=saved.SourceSystemFilter??"";TradeDestinationSystemText=saved.DestinationSystemFilter??"";
TradeCommoditySearchText=string.Join(", ",saved.CommunityGoalCommodityFilter??[]);
tradeSourceSearchMarketId=saved.SourceMarketIdFilter;tradeDestinationSearchMarketId=saved.DestinationMarketIdFilter;
updatingTradeSources=true;
try{foreach(var option in TradeSourceKinds){option.IsSelected=(saved.AllowedSourceKinds&option.Kind)!=0;option.Changed+=TradeSourceKindsChanged;}}
finally{updatingTradeSources=false;}
PropertyChanged+=(_,args)=>
{
if(args.PropertyName is nameof(RiskIndex) or nameof(ObjectiveIndex) or nameof(Hold) or nameof(JumpRange)
or nameof(MaxDistance) or nameof(MaxAgeHours) or nameof(MaxArrival) or nameof(MaxLegs) or nameof(Loop)
or nameof(JumpMinutes) or nameof(DockMinutes) or nameof(CostPerJump) or nameof(CostPerMinute) or nameof(AllowUnknown)
or nameof(AutomaticCommodities) or nameof(AutomaticReturnTrade))InvalidateTradeCandidates();
};
}
private void RefreshTradePlanningIdentity(PlayerState player)
{
var next=new TradePlanningPlayerKey(player.CommanderId,player.CurrentShipId,(player.Ship??"").ToUpperInvariant(),player.CargoCount,
player.MarketId,(player.SystemName??"").ToUpperInvariant(),player.Position,player.InGame);
if(tradePlanningPlayerKey is {} previous&&previous!=next)InvalidateTradeCandidates();
tradePlanningPlayerKey=next;
}
private void TradeSourceKindsChanged()
{
if(updatingTradeSources)return;
OnPropertyChanged(nameof(SelectedTradeSourceKinds));OnPropertyChanged(nameof(TradeSourceSelectionSummary));InvalidateTradeCandidates();CalculateCommand.NotifyCanExecuteChanged();
}
[RelayCommand]private void SelectKnownTradeSources()=>SetTradeSourceKinds(StationSourceKinds.AllKnown);
[RelayCommand]private void ClearTradeSources()=>SetTradeSourceKinds((StationSourceKinds)0);
private void SetTradeSourceKinds(StationSourceKinds kinds)
{
updatingTradeSources=true;try{foreach(var option in TradeSourceKinds)option.IsSelected=(kinds&option.Kind)!=0;}
finally{updatingTradeSources=false;}TradeSourceKindsChanged();
}
private void RefreshTradeShipPad()
{
if(TradeShipPadKnown&&PadIndex<(int)TradeActualShipPad-1)PadIndex=(int)TradeActualShipPad-1;
foreach(var name in new[]{nameof(TradeActualShipPad),nameof(TradeShipPadKnown),nameof(TradeSmallPadOptionAvailable),nameof(TradeMediumPadOptionAvailable),
nameof(TradeLargePadOptionAvailable),nameof(TradeShipPadSummary),nameof(TradeEffectivePadSummary)})OnPropertyChanged(name);
CalculateCommand.NotifyCanExecuteChanged();
}
partial void OnPadIndexChanged(int value)
{
if(TradeShipPadKnown&&value<(int)TradeActualShipPad-1){PadIndex=(int)TradeActualShipPad-1;return;}
OnPropertyChanged(nameof(TradeEffectivePadSummary));InvalidateTradeCandidates();
}
private IProgress<TradePlanningProgress> BeginTradePlanningProgress(CancellationToken token)
{
var operation=Interlocked.Increment(ref tradePlanningOperation);tradeProgressEvidence.Clear();TradePlanningBusy=true;
ApplyTradePlanningProgress(new("核对规划范围",0,null,"正在读取当前飞船与搜索中心。"));
TradePlanningProgress? pending=null;var queued=0;
return new TradeProgressReceiver(value=>
{
Volatile.Write(ref pending,value);
if(Interlocked.Exchange(ref queued,1)!=0)return;
if(!dispatcher.TryEnqueue(()=>
{
Interlocked.Exchange(ref queued,0);
if(operation==Volatile.Read(ref tradePlanningOperation)&&TradePlanningBusy&&!token.IsCancellationRequested&&Volatile.Read(ref pending) is {} latest)
ApplyTradePlanningProgress(latest);
}))Interlocked.Exchange(ref queued,0);
});
}
private void ApplyTradePlanningProgress(TradePlanningProgress value)
{
TradeProgressStage=value.Stage;TradeProgressDetail=value.Detail;TradeProgressProcessed=Math.Max(0,value.Processed);
TradeProgressTotal=value.Total is >=0?value.Total:null;
// Keep a small observation record; do not turn elapsed time into progress.
if(tradeProgressEvidence.Count<128)tradeProgressEvidence.Add(value);
}
private void EndTradePlanningProgress()
{
Interlocked.Increment(ref tradePlanningOperation);TradePlanningBusy=false;TradeProgressStage="";TradeProgressDetail="";
TradeProgressProcessed=0;TradeProgressTotal=null;
}
private sealed class TradeProgressReceiver(Action<TradePlanningProgress> report):IProgress<TradePlanningProgress>
{public void Report(TradePlanningProgress value)=>report(value);}
internal IDisposable EnterTradePlanningFixtureForProbe(TradePlanningResult result,IReadOnlyList<CommunityGoalInfo> goals,TradePlanningProgress observedProgress,string isolatedDirectory)
{
if(!OfflinePreview||!Environment.GetCommandLineArgs().Contains("--offline-preview")
||!Path.GetFullPath(runtime.DataDirectory).Equals(Path.GetFullPath(isolatedDirectory),StringComparison.OrdinalIgnoreCase)
||RunActive||IsBusy||Plan is not null||TradeCandidates.Count>0)
throw new InvalidOperationException("Synthetic planning visuals require an idle, empty, explicit isolated offline profile.");
var rows=Stations.Select(row=>row.Record).ToArray();var start=SelectedStart?.MarketId;var destination=AutomaticDestination?.MarketId;
var oldSources=SelectedTradeSourceKinds;var oldManual=manualTradeConfigured;var oldMode=AutomaticModeIndex;
var oldStatus=TradePlanningStatus;var oldTitle=TradeEmptyTitle;var oldDescription=TradeEmptyDescription;var oldCg=TradeCommunityGoalStatus;
var oldCgRead=TradeCommunityGoalReadForProbe;var oldDiagnostic=TradePlanningDiagnosticForProbe;
var oldInfo=PlanInfo;var oldProfit=PlanProfit;var oldHourly=PlanHourly;var oldDuration=PlanDuration;var oldWarnings=PlanWarnings;
IsBusy=true;SetTradeSourceKinds(StationSourceKinds.Starport|StationSourceKinds.FleetCarrier);
SetTradeCandidates(result.Candidates,result.Diagnostics,goals,syntheticFixture:true);
TradePlanningStatus="离线合成界面夹具 · 非真实资金、行情或活动";
TradeCommunityGoalStatus="合成数据仅核验布局;没有修改玩家数据、访问游戏或提交交易。";
TradePlanningBusy=true;tradePlanningFixtureActive=true;ReplayTradePlanningProgressForProbe(observedProgress);
return new TradePlanningFixtureScope(()=>
{
tradePlanningFixtureActive=false;EndTradePlanningProgress();SetTradeCandidates([]);SetTradeSourceKinds(oldSources);Stations.Apply(rows);
SelectedStart=Stations.FirstOrDefault(row=>row.MarketId==start);AutomaticDestination=Stations.FirstOrDefault(row=>row.MarketId==destination);
AutomaticModeIndex=oldMode;manualTradeConfigured=oldManual;Plan=null;SelectedLeg=null;
PlanInfo=oldInfo;PlanProfit=oldProfit;PlanHourly=oldHourly;PlanDuration=oldDuration;PlanWarnings=oldWarnings;
TradePlanningStatus=oldStatus;TradeEmptyTitle=oldTitle;TradeEmptyDescription=oldDescription;TradeCommunityGoalStatus=oldCg;
TradeCommunityGoalReadForProbe=oldCgRead;TradePlanningDiagnosticForProbe=oldDiagnostic;IsBusy=false;
});
}
internal void ReplayTradePlanningProgressForProbe(TradePlanningProgress observed)
{
if(!tradePlanningFixtureActive||!OfflinePreview)throw new InvalidOperationException("No isolated planning visual fixture is active.");
ApplyTradePlanningProgress(observed with{Stage="离线阶段快照 · "+observed.Stage,Detail=observed.Detail+"(仅合成数据界面核验,不代表交易进度。)"});
}
private sealed class TradePlanningFixtureScope(Action release):IDisposable
{private Action? restore=release;public void Dispose()=>Interlocked.Exchange(ref restore,null)?.Invoke();}
}
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.UI.Xaml;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Planning;
using XFEEDToolBox.Core.Execution;
using XFEEDToolBox.Core.Presentation;
namespace XFEEDToolBox.ViewModels;
public sealed class TradeSourceKindOptionViewModel(StationSourceKinds kind,string label):ObservableObject
{
public StationSourceKinds Kind{get;}=kind;
public string Label{get;}=label;
private bool selected;
public bool IsSelected{get=>selected;set{if(SetProperty(ref selected,value))Changed?.Invoke();}}
internal event Action? Changed;
}
public partial class ToolboxViewModel
{
private bool updatingTradeSources;
private bool tradePlanningFixtureActive;
public int TradePlanningModeIndex{get=>AutomaticModeIndex;set=>AutomaticModeIndex=Math.Clamp(value,0,1);}
public string TradePlanningModeDescription=>TradePlanningModeIndex==1?"只比较有效 CG 的接收商品,按预计净利润 / 小时排序。":"比较范围内全部合规市场的预计净利润 / 小时。";
private void TradePlanningModeChanged(int value)
{
OnPropertyChanged(nameof(TradePlanningModeIndex));OnPropertyChanged(nameof(TradePlanningModeDescription));
InvalidateTradeCandidates();
if(initialized&&!selectingTradeCandidate&&!tradePlanningFixtureActive&&!OfflinePreview&&!DemoMode)_=SaveTradePlanningModeAsync(value==1);
}
[ObservableProperty] public partial StationLiveRow? TradeSourceSearchStation{get;set;}
[ObservableProperty] public partial StationLiveRow? TradeDestinationSearchStation{get;set;}
[ObservableProperty] public partial string TradeSourceSearchText{get;set;}="";
[ObservableProperty] public partial string TradeDestinationSearchText{get;set;}="";
[ObservableProperty] public partial string TradeSourceSystemText{get;set;}="";
[ObservableProperty] public partial string TradeDestinationSystemText{get;set;}="";
[ObservableProperty] public partial string TradeCommoditySearchText{get;set;}="";
private long? tradeSourceSearchMarketId,tradeDestinationSearchMarketId;
private bool updatingTradeSearch;
partial void OnTradeSourceSystemTextChanged(string value)=>InvalidateTradeCandidates();
partial void OnTradeDestinationSystemTextChanged(string value)=>InvalidateTradeCandidates();
partial void OnTradeCommoditySearchTextChanged(string value)=>InvalidateTradeCandidates();
partial void OnTradeSourceSearchTextChanged(string value)
{
if(updatingTradeSearch)return;
var matches=TradeSourceSearchStation is {} station&&(value.Trim().Equals(station.Record.Name,StringComparison.OrdinalIgnoreCase)
||value.Trim().Equals(station.DisplayName,StringComparison.OrdinalIgnoreCase));
tradeSourceSearchMarketId=matches?TradeSourceSearchStation!.MarketId:null;
if(!matches)TradeSourceSearchStation=null;
InvalidateTradeCandidates();
}
partial void OnTradeDestinationSearchTextChanged(string value)
{
if(updatingTradeSearch)return;
var matches=TradeDestinationSearchStation is {} station&&(value.Trim().Equals(station.Record.Name,StringComparison.OrdinalIgnoreCase)
||value.Trim().Equals(station.DisplayName,StringComparison.OrdinalIgnoreCase));
tradeDestinationSearchMarketId=matches?TradeDestinationSearchStation!.MarketId:null;
if(!matches)TradeDestinationSearchStation=null;
InvalidateTradeCandidates();
}
partial void OnTradeSourceSearchStationChanged(StationLiveRow? value)
{
tradeSourceSearchMarketId=value?.MarketId;
if(value is not null){updatingTradeSearch=true;try{TradeSourceSearchText=value.Record.Name;SelectedStart=value;}finally{updatingTradeSearch=false;}}
InvalidateTradeCandidates();
}
partial void OnTradeDestinationSearchStationChanged(StationLiveRow? value)
{
tradeDestinationSearchMarketId=value?.MarketId;
if(value is not null){updatingTradeSearch=true;try{TradeDestinationSearchText=value.Record.Name;AutomaticDestination=value;}finally{updatingTradeSearch=false;}}
InvalidateTradeCandidates();
}
private async Task SaveTradePlanningModeAsync(bool cgOnly)
{
try{await runtime.SaveSettingsAsync(runtime.Settings with{Planner=runtime.Settings.Planner with{CommunityGoalSupplyOnly=cgOnly}});}
catch(Exception error){runtime.Log("跑商规划类型",error.Message);}
}
private int tradePlanningOperation;
private long tradePlanningInputVersion;
internal long TradePlanningInputVersionForProbe=>Volatile.Read(ref tradePlanningInputVersion);
private sealed record TradePlanningPlayerKey(string CommanderId,long? ShipId,string ShipType,int CargoCount,long? MarketId,string SystemName,Coordinates? Position,bool InGame);
private TradePlanningPlayerKey? tradePlanningPlayerKey;
public IReadOnlyList<TradeSourceKindOptionViewModel> TradeSourceKinds{get;}=
[
new(StationSourceKinds.Starport,"星港"),new(StationSourceKinds.Outpost,"轨道前哨"),
new(StationSourceKinds.PlanetaryPort,"行星港"),new(StationSourceKinds.Settlement,"地面据点"),
new(StationSourceKinds.FleetCarrier,"舰队航母"),new(StationSourceKinds.AsteroidBase,"小行星基地"),
new(StationSourceKinds.Megaship,"巨舰"),new(StationSourceKinds.Other,"其他已知类型"),
new(StationSourceKinds.Unknown,"未知类型")
];
public StationSourceKinds SelectedTradeSourceKinds=>TradeSourceKinds.Where(option=>option.IsSelected).Aggregate((StationSourceKinds)0,(value,option)=>value|option.Kind);
public string TradeSourceSelectionSummary=>SelectedTradeSourceKinds==0?"未限定货源类型 · 使用默认全部已知类型,未知类型未启用":$"已选 {TradeSourceKinds.Count(option=>option.IsSelected)} 类货源 · "+
(TradeSourceKinds.Any(option=>option.Kind==StationSourceKinds.Unknown&&option.IsSelected)?"已单独允许未知类型":"未知类型未启用");
public LandingPad TradeActualShipPad=>ShipPadPolicy.RequiredPad(CurrentPlayer.Ship);
public bool TradeShipPadKnown=>TradeActualShipPad>LandingPad.Unknown;
public bool TradeSmallPadOptionAvailable=>TradeActualShipPad==LandingPad.Small;
public bool TradeMediumPadOptionAvailable=>TradeActualShipPad is LandingPad.Small or LandingPad.Medium;
public bool TradeLargePadOptionAvailable=>TradeShipPadKnown;
public string TradeShipPadSummary=>TradeShipPadKnown
?$"当前飞船至少需要 {PadLabel(TradeActualShipPad)} 停机坪;每个采购与交付站都会核对。"
:"等待识别当前飞船尺寸;确认前不能生成可执行路线。";
public string TradeEffectivePadSummary=>TradeShipPadKnown
?$"本次要求:{PadLabel((LandingPad)Math.Max((int)TradeActualShipPad,Math.Clamp(PadIndex,0,2)+1))};手动选择只能更严格。"
:"未识别飞船尺寸时,手动停机坪设置不能替代船型核验。";
private static string PadLabel(LandingPad pad)=>pad switch{LandingPad.Small=>"小型 S",LandingPad.Medium=>"中型 M",LandingPad.Large=>"大型 L",_=>"未知"};
[ObservableProperty,NotifyPropertyChangedFor(nameof(TradeProgressVisibility))]public partial bool TradePlanningBusy{get;set;}
[ObservableProperty]public partial string TradeProgressStage{get;set;}="";
[ObservableProperty]public partial string TradeProgressDetail{get;set;}="";
[ObservableProperty,NotifyPropertyChangedFor(nameof(TradeProgressPercent),nameof(TradeProgressCount))]public partial long TradeProgressProcessed{get;set;}
[ObservableProperty,NotifyPropertyChangedFor(nameof(TradeProgressIsIndeterminate),nameof(TradeProgressPercent),nameof(TradeProgressCount))]public partial long? TradeProgressTotal{get;set;}
public Visibility TradeProgressVisibility=>TradePlanningBusy?Visibility.Visible:Visibility.Collapsed;
public bool TradeProgressIsIndeterminate=>TradeProgressTotal is null;
public double TradeProgressPercent=>TradeProgressTotal is >0?Math.Clamp(TradeProgressProcessed*100d/TradeProgressTotal.Value,0,100):0;
public string TradeProgressCount=>TradeProgressTotal is {} total?$"{TradeProgressProcessed:N0} / {total:N0}":$"已处理 {TradeProgressProcessed:N0} · 总量待确认";
internal IReadOnlyList<TradePlanningProgress> TradeProgressForProbe=>tradeProgressEvidence.ToArray();
private readonly List<TradePlanningProgress> tradeProgressEvidence=[];
private void InitializeTradePlanningOptions(PlannerRequest saved)
{
TradePlanningModeIndex=saved.CommunityGoalSupplyOnly?1:0;
TradeSourceSearchText=saved.SourceStationFilter??"";TradeDestinationSearchText=saved.DestinationStationFilter??"";
TradeSourceSystemText=saved.SourceSystemFilter??"";TradeDestinationSystemText=saved.DestinationSystemFilter??"";
TradeCommoditySearchText=string.Join(", ",saved.CommunityGoalCommodityFilter??[]);
tradeSourceSearchMarketId=saved.SourceMarketIdFilter;tradeDestinationSearchMarketId=saved.DestinationMarketIdFilter;
updatingTradeSources=true;
try{foreach(var option in TradeSourceKinds){option.IsSelected=(saved.AllowedSourceKinds&option.Kind)!=0;option.Changed+=TradeSourceKindsChanged;}}
finally{updatingTradeSources=false;}
PropertyChanged+=(_,args)=>
{
if(args.PropertyName is nameof(RiskIndex) or nameof(ObjectiveIndex) or nameof(Hold) or nameof(JumpRange)
or nameof(MaxDistance) or nameof(MaxAgeHours) or nameof(MaxArrival) or nameof(MaxLegs) or nameof(Loop)
or nameof(JumpMinutes) or nameof(DockMinutes) or nameof(CostPerJump) or nameof(CostPerMinute) or nameof(AllowUnknown)
or nameof(AutomaticCommodities) or nameof(AutomaticReturnTrade))InvalidateTradeCandidates();
};
}
private void RefreshTradePlanningIdentity(PlayerState player)
{
var next=new TradePlanningPlayerKey(player.CommanderId,player.CurrentShipId,(player.Ship??"").ToUpperInvariant(),player.CargoCount,
player.MarketId,(player.SystemName??"").ToUpperInvariant(),player.Position,player.InGame);
if(tradePlanningPlayerKey is {} previous&&previous!=next)InvalidateTradeCandidates();
tradePlanningPlayerKey=next;
}
private void TradeSourceKindsChanged()
{
if(updatingTradeSources)return;
OnPropertyChanged(nameof(SelectedTradeSourceKinds));OnPropertyChanged(nameof(TradeSourceSelectionSummary));InvalidateTradeCandidates();CalculateCommand.NotifyCanExecuteChanged();
}
[RelayCommand]private void SelectKnownTradeSources()=>SetTradeSourceKinds(StationSourceKinds.AllKnown);
[RelayCommand]private void ClearTradeSources()=>SetTradeSourceKinds((StationSourceKinds)0);
private void SetTradeSourceKinds(StationSourceKinds kinds)
{
updatingTradeSources=true;try{foreach(var option in TradeSourceKinds)option.IsSelected=(kinds&option.Kind)!=0;}
finally{updatingTradeSources=false;}TradeSourceKindsChanged();
}
private void RefreshTradeShipPad()
{
if(TradeShipPadKnown&&PadIndex<(int)TradeActualShipPad-1)PadIndex=(int)TradeActualShipPad-1;
foreach(var name in new[]{nameof(TradeActualShipPad),nameof(TradeShipPadKnown),nameof(TradeSmallPadOptionAvailable),nameof(TradeMediumPadOptionAvailable),
nameof(TradeLargePadOptionAvailable),nameof(TradeShipPadSummary),nameof(TradeEffectivePadSummary)})OnPropertyChanged(name);
CalculateCommand.NotifyCanExecuteChanged();
}
partial void OnPadIndexChanged(int value)
{
if(TradeShipPadKnown&&value<(int)TradeActualShipPad-1){PadIndex=(int)TradeActualShipPad-1;return;}
OnPropertyChanged(nameof(TradeEffectivePadSummary));InvalidateTradeCandidates();
}
private IProgress<TradePlanningProgress> BeginTradePlanningProgress(CancellationToken token)
{
var operation=Interlocked.Increment(ref tradePlanningOperation);tradeProgressEvidence.Clear();TradePlanningBusy=true;
ApplyTradePlanningProgress(new("核对规划范围",0,null,"正在读取当前飞船与搜索中心。"));
TradePlanningProgress? pending=null;var queued=0;
return new TradeProgressReceiver(value=>
{
Volatile.Write(ref pending,value);
if(Interlocked.Exchange(ref queued,1)!=0)return;
if(!dispatcher.TryEnqueue(()=>
{
Interlocked.Exchange(ref queued,0);
if(operation==Volatile.Read(ref tradePlanningOperation)&&TradePlanningBusy&&!token.IsCancellationRequested&&Volatile.Read(ref pending) is {} latest)
ApplyTradePlanningProgress(latest);
}))Interlocked.Exchange(ref queued,0);
});
}
private void ApplyTradePlanningProgress(TradePlanningProgress value)
{
TradeProgressStage=value.Stage;TradeProgressDetail=value.Detail;TradeProgressProcessed=Math.Max(0,value.Processed);
TradeProgressTotal=value.Total is >=0?value.Total:null;
// Keep a small observation record; do not turn elapsed time into progress.
if(tradeProgressEvidence.Count<128)tradeProgressEvidence.Add(value);
}
private void EndTradePlanningProgress()
{
Interlocked.Increment(ref tradePlanningOperation);TradePlanningBusy=false;TradeProgressStage="";TradeProgressDetail="";
TradeProgressProcessed=0;TradeProgressTotal=null;
}
private sealed class TradeProgressReceiver(Action<TradePlanningProgress> report):IProgress<TradePlanningProgress>
{public void Report(TradePlanningProgress value)=>report(value);}
internal IDisposable EnterTradePlanningFixtureForProbe(TradePlanningResult result,IReadOnlyList<CommunityGoalInfo> goals,TradePlanningProgress observedProgress,string isolatedDirectory)
{
if(!OfflinePreview||!Environment.GetCommandLineArgs().Contains("--offline-preview")
||!Path.GetFullPath(runtime.DataDirectory).Equals(Path.GetFullPath(isolatedDirectory),StringComparison.OrdinalIgnoreCase)
||RunActive||IsBusy||Plan is not null||TradeCandidates.Count>0)
throw new InvalidOperationException("Synthetic planning visuals require an idle, empty, explicit isolated offline profile.");
var rows=Stations.Select(row=>row.Record).ToArray();var start=SelectedStart?.MarketId;var destination=AutomaticDestination?.MarketId;
var oldSources=SelectedTradeSourceKinds;var oldManual=manualTradeConfigured;var oldMode=AutomaticModeIndex;
var oldStatus=TradePlanningStatus;var oldTitle=TradeEmptyTitle;var oldDescription=TradeEmptyDescription;var oldCg=TradeCommunityGoalStatus;
var oldCgRead=TradeCommunityGoalReadForProbe;var oldDiagnostic=TradePlanningDiagnosticForProbe;
var oldInfo=PlanInfo;var oldProfit=PlanProfit;var oldHourly=PlanHourly;var oldDuration=PlanDuration;var oldWarnings=PlanWarnings;
IsBusy=true;SetTradeSourceKinds(StationSourceKinds.Starport|StationSourceKinds.FleetCarrier);
SetTradeCandidates(result.Candidates,result.Diagnostics,goals,syntheticFixture:true);
TradePlanningStatus="离线合成界面夹具 · 非真实资金、行情或活动";
TradeCommunityGoalStatus="合成数据仅核验布局;没有修改玩家数据、访问游戏或提交交易。";
TradePlanningBusy=true;tradePlanningFixtureActive=true;ReplayTradePlanningProgressForProbe(observedProgress);
return new TradePlanningFixtureScope(()=>
{
tradePlanningFixtureActive=false;EndTradePlanningProgress();SetTradeCandidates([]);SetTradeSourceKinds(oldSources);Stations.Apply(rows);
SelectedStart=Stations.FirstOrDefault(row=>row.MarketId==start);AutomaticDestination=Stations.FirstOrDefault(row=>row.MarketId==destination);
AutomaticModeIndex=oldMode;manualTradeConfigured=oldManual;Plan=null;SelectedLeg=null;
PlanInfo=oldInfo;PlanProfit=oldProfit;PlanHourly=oldHourly;PlanDuration=oldDuration;PlanWarnings=oldWarnings;
TradePlanningStatus=oldStatus;TradeEmptyTitle=oldTitle;TradeEmptyDescription=oldDescription;TradeCommunityGoalStatus=oldCg;
TradeCommunityGoalReadForProbe=oldCgRead;TradePlanningDiagnosticForProbe=oldDiagnostic;IsBusy=false;
});
}
internal void ReplayTradePlanningProgressForProbe(TradePlanningProgress observed)
{
if(!tradePlanningFixtureActive||!OfflinePreview)throw new InvalidOperationException("No isolated planning visual fixture is active.");
ApplyTradePlanningProgress(observed with{Stage="离线阶段快照 · "+observed.Stage,Detail=observed.Detail+"(仅合成数据界面核验,不代表交易进度。)"});
}
private sealed class TradePlanningFixtureScope(Action release):IDisposable
{private Action? restore=release;public void Dispose()=>Interlocked.Exchange(ref restore,null)?.Invoke();}
}