using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
private readonly SemaphoreSlim tradeRegionGate=new(1,1);
private Coordinates? tradeRegionCenterPosition;
private string resolvedTradeRegionName="";
[ObservableProperty] public partial string TradeRegionCenterText {get;set;}="";
[ObservableProperty] public partial string TradeRegionStatus {get;set;}="留空跟随当前位置;缓存与规划使用同一范围。";
[ObservableProperty] public partial bool TradeRegionBusy {get;set;}
public bool TradeRegionEditable=>TradeSelectionEnabled&&!TradeRegionBusy&&!SessionBusy;
public string MarketCacheScopeText=>runtime.Settings.MarketCacheCenterPosition is {IsValid:true}
?$"新增行情以 {runtime.Settings.MarketCacheCenterName} 为中心,半径 {runtime.Settings.MarketCacheRadiusLy:N0} ly;保留当前站。不自动下载或校验全量数据。"
:$"新增行情跟随当前位置,半径 {runtime.Settings.MarketCacheRadiusLy:N0} ly;保留当前站。没有定位时等待,不自动下载或校验全量数据。";
private void InitializeTradeRegionScope()
{
var settings=runtime.Settings;
Radius=settings.MarketCacheRadiusLy;
tradeRegionCenterPosition=settings.MarketCacheCenterPosition;
resolvedTradeRegionName=settings.MarketCacheCenterPosition is {IsValid:true}?settings.MarketCacheCenterName:"";
TradeRegionCenterText=resolvedTradeRegionName;
TradeRegionStatus=resolvedTradeRegionName.Length>0?$"{resolvedTradeRegionName} · 半径 {Radius:N0} ly":"跟随当前位置 · 半径 "+Radius.ToString("N0")+" ly";
PropertyChanged+=(_,args)=>
{
if(args.PropertyName is nameof(RunActive) or nameof(IsBusy) or nameof(SessionBusy))OnPropertyChanged(nameof(TradeRegionEditable));
};
}
partial void OnTradeRegionCenterTextChanged(string value)
{
if(!value.Trim().Equals(resolvedTradeRegionName,StringComparison.OrdinalIgnoreCase))tradeRegionCenterPosition=null;
InvalidateTradeCandidates();
}
partial void OnRadiusChanged(double value)=>InvalidateTradeCandidates();
partial void OnTradeRegionBusyChanged(bool value)
{OnPropertyChanged(nameof(TradeRegionEditable));CalculateCommand.NotifyCanExecuteChanged();RefreshMarketCacheSettingsAvailability();}
private static void ValidateTradeRegionRadius(double radius)
{if(!double.IsFinite(radius)||radius<=0)throw new ArgumentException("搜索半径须为有效的正数,单位 ly。");}
private async Task SaveTradeRegionScopeAsync(string name,Coordinates? position,CancellationToken token)
{
token.ThrowIfCancellationRequested();ValidateTradeRegionRadius(Radius);
var settings=runtime.Settings;
if(settings.MarketCacheRadiusLy!=Radius||settings.MarketCacheCenterName!=name||settings.MarketCacheCenterPosition!=position)
{
await runtime.SaveSettingsAsync(settings with{MarketCacheRadiusLy=Radius,MarketCacheCenterName=name,MarketCacheCenterPosition=position,
Planner=settings.Planner with{SearchRadiusLy=Radius,SearchCenterName=name,SearchCenterPosition=position}});
OnPropertyChanged(nameof(MarketCacheScopeText));
}
}
internal async Task CommitTradeRegionCenterAsync(string name,CancellationToken token=default)
{
await tradeRegionGate.WaitAsync(token);
try
{
if(RunActive||SessionBusy)throw new InvalidOperationException("请先停止跑商或完成会话操作,再调整规划范围。");
TradeRegionBusy=true;ValidateTradeRegionRadius(Radius);name=name.Trim();TradeRegionCenterText=name;
Coordinates? position=null;
if(name.Length>0)
{
TradeRegionStatus="正在核对中心星系坐标…";
var found=await ResolveGalacticSelectionAsync(name,token);
if(!found.Position.IsValid)throw new InvalidDataException("尚未取得该星系的真实坐标,请重新选择或提交名称。");
if(!TradeRegionCenterText.Trim().Equals(name,StringComparison.OrdinalIgnoreCase))throw new OperationCanceledException(token);
name=found.Name;position=found.Position;
}
await SaveTradeRegionScopeAsync(name,position,token);
resolvedTradeRegionName=name;tradeRegionCenterPosition=position;TradeRegionCenterText=name;
TradeRegionStatus=name.Length==0?$"跟随当前位置 · 半径 {Radius:N0} ly":$"{name} · 半径 {Radius:N0} ly";
}
finally{TradeRegionBusy=false;tradeRegionGate.Release();}
}
internal async Task SaveTradeRegionRadiusAsync(CancellationToken token=default)
{
if(!double.IsFinite(Radius))Radius=runtime.Settings.MarketCacheRadiusLy;
await tradeRegionGate.WaitAsync(token);
try
{
if(RunActive||SessionBusy)throw new InvalidOperationException("请先停止跑商或完成会话操作,再调整规划范围。");
ValidateTradeRegionRadius(Radius);
await SaveTradeRegionScopeAsync(runtime.Settings.MarketCacheCenterName,runtime.Settings.MarketCacheCenterPosition,token);
TradeRegionStatus=runtime.Settings.MarketCacheCenterPosition is not null?$"{runtime.Settings.MarketCacheCenterName} · 半径 {Radius:N0} ly":$"跟随当前位置 · 半径 {Radius:N0} ly";
}
finally{tradeRegionGate.Release();}
}
[RelayCommand]
private async Task FollowCurrentTradeRegionAsync()
{
try{await CommitTradeRegionCenterAsync("",lifetime.Token);}
catch(OperationCanceledException){}
catch(Exception error){ReportTradeRegionError(error);}
}
internal void ReportTradeRegionError(Exception error)
{TradeRegionStatus=error is ArgumentException or InvalidDataException or InvalidOperationException?error.Message:"中心或范围尚未保存,请重试。";runtime.Log("跑商范围",error.ToString());}
private Coordinates? EffectiveTradeRegionCenter=>TradeRegionCenterText.Trim().Length==0?CurrentPlayer.Position:tradeRegionCenterPosition;
private async Task PrepareTradeRegionForPlanningAsync(CancellationToken token)
{
await CommitTradeRegionCenterAsync(TradeRegionCenterText,token);
var player=CurrentPlayer;
if(player.Position is not {IsValid:true})throw new InvalidOperationException("等待当前星系定位后再规划;也可先选择中心并手动补全周边行情。");
if(EffectiveTradeRegionCenter is not {IsValid:true} center)throw new InvalidOperationException("中心坐标尚未就绪,请选择准确星系后重试。");
var rows=DemoMode?DemoData.CreateBook().Stations.Where(station=>station.Position is {IsValid:true} position&&position.DistanceTo(center)<=Radius).ToArray()
:await Task.Run(()=>runtime.Markets.GetStationsNear(center,Radius).ToArray(),token);
if(rows.Length==0)throw new InvalidOperationException("此范围尚无缓存站点。请等待周边行情,或在设置中手动补全该中心星系。");
var start=rows.FirstOrDefault(station=>station.MarketId==SelectedStart?.MarketId)
??rows.FirstOrDefault(station=>station.MarketId==player.MarketId)
??rows.OrderBy(station=>station.Position!.DistanceTo(player.Position)).ThenBy(station=>station.MarketId).First();
var oldDestination=AutomaticDestination?.MarketId;
Stations.Apply(rows.Concat(AutomaticDestination is null?[]:new[]{AutomaticDestination.Record}).DistinctBy(station=>station.MarketId));
SelectedStart=Stations.First(station=>station.MarketId==start.MarketId);
AutomaticDestination=Stations.FirstOrDefault(station=>station.MarketId==oldDestination);
}
internal void RegisterTradeRegionSystemForProbe(GalacticSystem system,string isolatedDirectory)
{
if(!OfflinePreview||system.Source!="isolated-fixture"||!system.Position.IsValid
||!Path.GetFullPath(runtime.DataDirectory).Equals(Path.GetFullPath(isolatedDirectory),StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Synthetic region coordinates are limited to an explicit isolated interface fixture.");
RememberGalaxy(system);
}
}
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Data;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
private readonly SemaphoreSlim tradeRegionGate=new(1,1);
private Coordinates? tradeRegionCenterPosition;
private string resolvedTradeRegionName="";
[ObservableProperty] public partial string TradeRegionCenterText {get;set;}="";
[ObservableProperty] public partial string TradeRegionStatus {get;set;}="留空跟随当前位置;缓存与规划使用同一范围。";
[ObservableProperty] public partial bool TradeRegionBusy {get;set;}
public bool TradeRegionEditable=>TradeSelectionEnabled&&!TradeRegionBusy&&!SessionBusy;
public string MarketCacheScopeText=>runtime.Settings.MarketCacheCenterPosition is {IsValid:true}
?$"新增行情以 {runtime.Settings.MarketCacheCenterName} 为中心,半径 {runtime.Settings.MarketCacheRadiusLy:N0} ly;保留当前站。不自动下载或校验全量数据。"
:$"新增行情跟随当前位置,半径 {runtime.Settings.MarketCacheRadiusLy:N0} ly;保留当前站。没有定位时等待,不自动下载或校验全量数据。";
private void InitializeTradeRegionScope()
{
var settings=runtime.Settings;
Radius=settings.MarketCacheRadiusLy;
tradeRegionCenterPosition=settings.MarketCacheCenterPosition;
resolvedTradeRegionName=settings.MarketCacheCenterPosition is {IsValid:true}?settings.MarketCacheCenterName:"";
TradeRegionCenterText=resolvedTradeRegionName;
TradeRegionStatus=resolvedTradeRegionName.Length>0?$"{resolvedTradeRegionName} · 半径 {Radius:N0} ly":"跟随当前位置 · 半径 "+Radius.ToString("N0")+" ly";
PropertyChanged+=(_,args)=>
{
if(args.PropertyName is nameof(RunActive) or nameof(IsBusy) or nameof(SessionBusy))OnPropertyChanged(nameof(TradeRegionEditable));
};
}
partial void OnTradeRegionCenterTextChanged(string value)
{
if(!value.Trim().Equals(resolvedTradeRegionName,StringComparison.OrdinalIgnoreCase))tradeRegionCenterPosition=null;
InvalidateTradeCandidates();
}
partial void OnRadiusChanged(double value)=>InvalidateTradeCandidates();
partial void OnTradeRegionBusyChanged(bool value)
{OnPropertyChanged(nameof(TradeRegionEditable));CalculateCommand.NotifyCanExecuteChanged();RefreshMarketCacheSettingsAvailability();}
private static void ValidateTradeRegionRadius(double radius)
{if(!double.IsFinite(radius)||radius<=0)throw new ArgumentException("搜索半径须为有效的正数,单位 ly。");}
private async Task SaveTradeRegionScopeAsync(string name,Coordinates? position,CancellationToken token)
{
token.ThrowIfCancellationRequested();ValidateTradeRegionRadius(Radius);
var settings=runtime.Settings;
if(settings.MarketCacheRadiusLy!=Radius||settings.MarketCacheCenterName!=name||settings.MarketCacheCenterPosition!=position)
{
await runtime.SaveSettingsAsync(settings with{MarketCacheRadiusLy=Radius,MarketCacheCenterName=name,MarketCacheCenterPosition=position,
Planner=settings.Planner with{SearchRadiusLy=Radius,SearchCenterName=name,SearchCenterPosition=position}});
OnPropertyChanged(nameof(MarketCacheScopeText));
}
}
internal async Task CommitTradeRegionCenterAsync(string name,CancellationToken token=default)
{
await tradeRegionGate.WaitAsync(token);
try
{
if(RunActive||SessionBusy)throw new InvalidOperationException("请先停止跑商或完成会话操作,再调整规划范围。");
TradeRegionBusy=true;ValidateTradeRegionRadius(Radius);name=name.Trim();TradeRegionCenterText=name;
Coordinates? position=null;
if(name.Length>0)
{
TradeRegionStatus="正在核对中心星系坐标…";
var found=await ResolveGalacticSelectionAsync(name,token);
if(!found.Position.IsValid)throw new InvalidDataException("尚未取得该星系的真实坐标,请重新选择或提交名称。");
if(!TradeRegionCenterText.Trim().Equals(name,StringComparison.OrdinalIgnoreCase))throw new OperationCanceledException(token);
name=found.Name;position=found.Position;
}
await SaveTradeRegionScopeAsync(name,position,token);
resolvedTradeRegionName=name;tradeRegionCenterPosition=position;TradeRegionCenterText=name;
TradeRegionStatus=name.Length==0?$"跟随当前位置 · 半径 {Radius:N0} ly":$"{name} · 半径 {Radius:N0} ly";
}
finally{TradeRegionBusy=false;tradeRegionGate.Release();}
}
internal async Task SaveTradeRegionRadiusAsync(CancellationToken token=default)
{
if(!double.IsFinite(Radius))Radius=runtime.Settings.MarketCacheRadiusLy;
await tradeRegionGate.WaitAsync(token);
try
{
if(RunActive||SessionBusy)throw new InvalidOperationException("请先停止跑商或完成会话操作,再调整规划范围。");
ValidateTradeRegionRadius(Radius);
await SaveTradeRegionScopeAsync(runtime.Settings.MarketCacheCenterName,runtime.Settings.MarketCacheCenterPosition,token);
TradeRegionStatus=runtime.Settings.MarketCacheCenterPosition is not null?$"{runtime.Settings.MarketCacheCenterName} · 半径 {Radius:N0} ly":$"跟随当前位置 · 半径 {Radius:N0} ly";
}
finally{tradeRegionGate.Release();}
}
[RelayCommand]
private async Task FollowCurrentTradeRegionAsync()
{
try{await CommitTradeRegionCenterAsync("",lifetime.Token);}
catch(OperationCanceledException){}
catch(Exception error){ReportTradeRegionError(error);}
}
internal void ReportTradeRegionError(Exception error)
{TradeRegionStatus=error is ArgumentException or InvalidDataException or InvalidOperationException?error.Message:"中心或范围尚未保存,请重试。";runtime.Log("跑商范围",error.ToString());}
private Coordinates? EffectiveTradeRegionCenter=>TradeRegionCenterText.Trim().Length==0?CurrentPlayer.Position:tradeRegionCenterPosition;
private async Task PrepareTradeRegionForPlanningAsync(CancellationToken token)
{
await CommitTradeRegionCenterAsync(TradeRegionCenterText,token);
var player=CurrentPlayer;
if(player.Position is not {IsValid:true})throw new InvalidOperationException("等待当前星系定位后再规划;也可先选择中心并手动补全周边行情。");
if(EffectiveTradeRegionCenter is not {IsValid:true} center)throw new InvalidOperationException("中心坐标尚未就绪,请选择准确星系后重试。");
var rows=DemoMode?DemoData.CreateBook().Stations.Where(station=>station.Position is {IsValid:true} position&&position.DistanceTo(center)<=Radius).ToArray()
:await Task.Run(()=>runtime.Markets.GetStationsNear(center,Radius).ToArray(),token);
if(rows.Length==0)throw new InvalidOperationException("此范围尚无缓存站点。请等待周边行情,或在设置中手动补全该中心星系。");
var start=rows.FirstOrDefault(station=>station.MarketId==SelectedStart?.MarketId)
??rows.FirstOrDefault(station=>station.MarketId==player.MarketId)
??rows.OrderBy(station=>station.Position!.DistanceTo(player.Position)).ThenBy(station=>station.MarketId).First();
var oldDestination=AutomaticDestination?.MarketId;
Stations.Apply(rows.Concat(AutomaticDestination is null?[]:new[]{AutomaticDestination.Record}).DistinctBy(station=>station.MarketId));
SelectedStart=Stations.First(station=>station.MarketId==start.MarketId);
AutomaticDestination=Stations.FirstOrDefault(station=>station.MarketId==oldDestination);
}
internal void RegisterTradeRegionSystemForProbe(GalacticSystem system,string isolatedDirectory)
{
if(!OfflinePreview||system.Source!="isolated-fixture"||!system.Position.IsValid
||!Path.GetFullPath(runtime.DataDirectory).Equals(Path.GetFullPath(isolatedDirectory),StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Synthetic region coordinates are limited to an explicit isolated interface fixture.");
RememberGalaxy(system);
}
}