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); } }