using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using XFEEDToolBox.Core.Galaxy; using XFEEDToolBox.Core.Models; using XFEEDToolBox.Core.Navigation; using System.Text.Json; namespace XFEEDToolBox.ViewModels; public partial class ToolboxViewModel { private readonly Dictionary galaxyCache=new(StringComparer.OrdinalIgnoreCase); private Func>? galaxyRegionQueryForProbe; internal void SetGalaxyRegionQueryForProbe(Func>? query) { var args=Environment.GetCommandLineArgs(); if(!args.Contains("--offline-preview")||!args.Contains("--probe-galaxy-rework"))throw new InvalidOperationException("仅允许独立离线星图验收设置记录样本查询。"); Interlocked.Increment(ref galaxyViewVersion); Interlocked.Increment(ref galaxySourceEpoch); ResetGalaxyAppearanceBinding(false); galaxyStreaming.Clear();galaxyCellStreaming.Clear();GalaxyCatalogueSelection=null;galaxyCache.Clear();galaxyViewport=null; galaxyFilterInformationStop?.Cancel();galaxyFilterQueryGeneration++;galaxyQueuedQuery="";galaxyDesiredViewport=null;GalaxyQuerySystems=[];GalaxyQueryBusy=false; galaxyRegionQueryForProbe=query;galaxyStreamSource=int.MinValue; galaxyLastPublish=0;galaxyNativePartialRows=0;GalaxyCatalogueBusy=false;PublishGalaxy(); } internal void SeedGalaxyForProbe(IReadOnlyList systems) { var args=Environment.GetCommandLineArgs(); if(!args.Contains("--offline-preview")||!args.Contains("--probe-galaxy-rework"))throw new InvalidOperationException("仅允许独立离线星图验收载入记录样本。"); Interlocked.Increment(ref galaxyViewVersion);Interlocked.Increment(ref galaxySourceEpoch); ResetGalaxyAppearanceBinding(false); galaxyStreaming.Clear();galaxyCellStreaming.Clear();GalaxyCatalogueSelection=null;galaxyCache.Clear();foreach(var system in systems)galaxyCache[system.Name]=system;galaxyViewport=null;PublishGalaxy(); } private readonly HttpClient galaxyHttp=new(){Timeout=TimeSpan.FromSeconds(25),MaxResponseContentBufferSize=32*1024*1024}; private int galaxyRequest; private NativeGalaxyCatalog? nativeGalaxy; private Action? galaxyNativeProgressHandler; private readonly GalaxyStreamingSession galaxyStreaming=new(new(){MaximumCachedSystems=240000,MaximumCachedTiles=3072}); private readonly GalaxyCellStreamingSession galaxyCellStreaming=new(new(){MaximumCachedSystems=240000,MaximumCachedCells=3072}); private CancellationTokenSource? galaxyCatalogueProfileStop; private CancellationTokenSource? galaxyOverrideResolveStop; private CancellationTokenSource? galaxyPointOverridesStop; [ObservableProperty] public partial GalaxyCatalogueSelection? GalaxyCatalogueSelection {get;set;} private readonly GalaxyMapLod galaxyCacheLod=new(); private int galaxyStreamSource=int.MinValue; private long galaxyViewVersion; private long galaxyLastPublish; private long galaxyNativePartialRows; private long galaxySourceEpoch; private CancellationToken galaxyStreamLifetime; private GalaxyViewport? galaxyViewport; internal void SuspendGalaxyStreaming() { Interlocked.Increment(ref galaxyViewVersion);Interlocked.Increment(ref galaxySourceEpoch); CancelGalaxyCatalogueProfile(); ResetGalaxyAppearanceBinding(); galaxyStreaming.Clear();galaxyCellStreaming.Clear();GalaxyCatalogueSelection=null;GalaxyCatalogueBusy=false; } private void CancelGalaxyCatalogueProfile() { try{Volatile.Read(ref galaxyCatalogueProfileStop)?.Cancel();}catch(ObjectDisposedException){} try{Volatile.Read(ref galaxyOverrideResolveStop)?.Cancel();}catch(ObjectDisposedException){} try{Volatile.Read(ref galaxyPointOverridesStop)?.Cancel();}catch(ObjectDisposedException){} } internal async Task StreamGalaxyRegionAsync(GalaxyViewport viewport,CancellationToken token) { token.ThrowIfCancellationRequested();lifetime.Token.ThrowIfCancellationRequested(); using var requestStop=CancellationTokenSource.CreateLinkedTokenSource(token,lifetime.Token); var requestToken=requestStop.Token;galaxyStreamLifetime=token; if(!viewport.Focus.IsValid)return; var version=Interlocked.Increment(ref galaxyViewVersion);galaxyViewport=viewport; try{Volatile.Read(ref galaxyOverrideResolveStop)?.Cancel();}catch(ObjectDisposedException){} try{Volatile.Read(ref galaxyPointOverridesStop)?.Cancel();}catch(ObjectDisposedException){} // A new view requests a new owner snapshot. Keep immutable cell data, // but do not render with the previous view's route/D50 classification. if(GalaxyCatalogueSelection is{PointOverrides:not null} oldSelection) GalaxyCatalogueSelection=oldSelection with{PointOverrides=null}; PublishGalaxy(); QueueGalaxyFilterInformation(); if(NavigationActive){CancelGalaxyCatalogueProfile();galaxyStreaming.Clear();galaxyCellStreaming.Clear();GalaxyCatalogueBusy=false;GalaxyStatus="导航中 · 当前视野使用缓存星系,停止后继续加载";return;} var probeQuery=galaxyRegionQueryForProbe; var sourceDriver=runtime.FlightDriver; var native=probeQuery is null&&sourceDriver is NativeFlightControlDriver{SupportsGalaxyRegion:true}?NativeGalaxy():null; var source=probeQuery is not null?-1:native?.ProcessId??0; if(galaxyStreamSource!=source){Interlocked.Increment(ref galaxySourceEpoch);ResetGalaxyCurrentRouteChoices();ResetGalaxyAppearanceBinding(source>=0);galaxyStreaming.Clear();galaxyCellStreaming.Clear();GalaxyCatalogueSelection=null;galaxyStreamSource=source;galaxyNativePartialRows=0;PublishGalaxy();} var sourceEpoch=Volatile.Read(ref galaxySourceEpoch); bool CurrentSource()=>version==Volatile.Read(ref galaxyViewVersion)&&sourceEpoch==Volatile.Read(ref galaxySourceEpoch)&& !requestToken.IsCancellationRequested&&!NavigationActive&&ReferenceEquals(probeQuery,galaxyRegionQueryForProbe)&& (probeQuery is not null||ReferenceEquals(sourceDriver,runtime.FlightDriver)&& (native is null?runtime.FlightDriver is not NativeFlightControlDriver{SupportsGalaxyRegion:true}: ReferenceEquals(native,nativeGalaxy)&&runtime.FlightDriver is NativeFlightControlDriver{SupportsGalaxyRegion:true} live&&live.ProcessId==native.ProcessId)); void ClearOriginalScope() { if(version!=Volatile.Read(ref galaxyViewVersion)||sourceEpoch!=Volatile.Read(ref galaxySourceEpoch))return; GalaxyCatalogueSelection=null;galaxyCellStreaming.Clear(); if(viewport.Camera is {} previous)galaxyViewport=previous.WithCatalogueQueryFar(Math.Max(previous.Near,previous.Distance*4)).ToViewport(); PublishGalaxy(); } async Task Query(Coordinates centre,double radius,CancellationToken stop) { if(!CurrentSource())throw new OperationCanceledException(stop); if(probeQuery is not null)return await probeQuery(centre,radius,stop); if(native is not null){await native.InspectProtocolAsync(stop);return await native.RegionPageAsync(centre,radius,stop);} // A cube result can prove coverage only of its actual requested cube. // The public source still covers reported systems, not Stellar Forge. var size=radius*2/Math.Sqrt(3);var systems=await new GalaxyCatalog(galaxyHttp).RegionAsync(centre,(int)Math.Clamp(Math.Ceiling(size),10,200),stop); var truncated=size>200||systems.Count>=25000; return new(systems,0,truncated?0:1,1,truncated,!truncated); } using var profileStop=CancellationTokenSource.CreateLinkedTokenSource(requestToken); var previousProfile=Interlocked.Exchange(ref galaxyCatalogueProfileStop,profileStop); try{previousProfile?.Cancel();}catch(ObjectDisposedException){} GalaxyCatalogueBusy=true; try { var originalInputsUnavailable=false; if(native is not null&&viewport.Camera is {} camera) { await native.InspectProtocolAsync(requestToken); if(!CurrentSource()){ClearOriginalScope();return;} if(native.SupportsCells&&native.SupportsCatalogueProfile) { GalaxyCatalogueProfile? profile=null; try{profile=await native.CatalogueProfileFirstPageAsync(camera.Eye,camera.Focus,profileStop.Token);} catch(Exception error)when(error is IOException or TimeoutException or InvalidOperationException){originalInputsUnavailable=true;} if(!CurrentSource()){ClearOriginalScope();return;} if(profile is not null)BindGalaxyAppearance(native.ProcessId,profile.ColourBlend is{} blend?(float)blend:null,profile.SmallMode); if(profile is not null)MergeGalaxyCurrentRouteChoices(native,sourceEpoch,profile.CurrentRouteChoices); if(profile is not null&&profile.TrySelection(camera,out var selection)&&selection is not null) {await StreamOriginalGalaxyCellsAsync(camera,selection,profile,native,CurrentSource,requestToken);if(!CurrentSource())ClearOriginalScope();return;} originalInputsUnavailable=true; } } if(GalaxyCatalogueSelection is not null){GalaxyCatalogueSelection=null;galaxyCellStreaming.Clear();PublishGalaxy();} if(viewport.Camera is {} fallbackCamera&&fallbackCamera.QueryFar!=Math.Max(fallbackCamera.Near,fallbackCamera.Distance*4)) { viewport=fallbackCamera.WithCatalogueQueryFar(Math.Max(fallbackCamera.Near,fallbackCamera.Distance*4)).ToViewport(); galaxyViewport=viewport;PublishGalaxy();QueueGalaxyFilterInformation(); } await foreach(var update in galaxyStreaming.StreamAsync(viewport,Query,requestToken)) { if(!CurrentSource()){ClearOriginalScope();return;} foreach(var system in update.Systems)RememberGalaxy(system,native?.ProcessId); TrimGalaxyCache(viewport); var now=Environment.TickCount64; if(update.Finished||now-galaxyLastPublish>=80){galaxyLastPublish=now;PublishGalaxy();} var label=probeQuery is not null?"离线原生记录回放":native is null?"已上报目录":"游戏原生目录"; GalaxyStatus=$"{label} · 视野 {GalaxySystems.Count:N0} 个星系 · 已查 {update.QueriedTiles:N0} 块 / 待查 {update.PendingTiles:N0} 块"+ (update.Finished?(update.CoverageComplete?" · 当前数据源视锥覆盖已完成":" · 仍有覆盖未确认"):$" · 正在补充细节(细分 {update.RefinedTiles:N0})")+ (probeQuery is not null?" · 有限记录不证明实机完整覆盖":native is null?" · 游戏星图接口未连接,未探索星系需原生目录":"")+ (originalInputsUnavailable?" · 原星区参数尚未就绪,当前使用有限范围":""); } } catch(OperationCanceledException){if(version==Volatile.Read(ref galaxyViewVersion)&&!CurrentSource())ClearOriginalScope();throw;} catch(Exception error) { if(version==Volatile.Read(ref galaxyViewVersion)){ClearOriginalScope();GalaxyStatus="星区查询中断,已保留实际返回星系;移动或缩放可重试:"+error.Message;} throw; } finally{Interlocked.CompareExchange(ref galaxyCatalogueProfileStop,null,profileStop);if(version==Volatile.Read(ref galaxyViewVersion))GalaxyCatalogueBusy=false;} } private async Task StreamOriginalGalaxyCellsAsync(GalaxyCamera camera,GalaxyCatalogueSelection selection,GalaxyCatalogueProfile profile, NativeGalaxyCatalog native,Func currentSource,CancellationToken token) { if(!currentSource())return; galaxyStreaming.Clear(); GalaxyCatalogueSelection=selection; if(selection.PointOverrides is{} points&&points.ProcessId==native.ProcessId) { foreach(var row in points.Overrides.Values) if(row.System is{} system)RememberGalaxy(system,native.ProcessId); // Resolution uses the existing exact-address native operation; // until it returns an actual name and matching coordinate no row // is manufactured from an address or a generated position. _=ResolvePublishedGalaxyOverridesAsync(selection,native,currentSource,token); } var viewport=selection.ApplyTo(camera).ToViewport();galaxyViewport=viewport; PublishGalaxy();QueueGalaxyFilterInformation(); // Basic LOD and the cell plan never wait for optional table pages. // Completion updates this view's classification/envelope in place. _=CompletePublishedGalaxyOverridesAsync(camera,selection,profile,native,currentSource,token); var plan=new GalaxyCellQueryPlan(camera,selection.Descriptor,selection.Origin); await foreach(var update in galaxyCellStreaming.StreamAsync(plan,native.CellsAsync,token)) { if(!currentSource()||!native.SupportsCells||!native.SupportsCatalogueProfile)return; foreach(var system in update.Systems)RememberGalaxy(system,native.ProcessId); TrimGalaxyCache(galaxyViewport??viewport); var now=Environment.TickCount64; if(update.Finished||now-galaxyLastPublish>=80){galaxyLastPublish=now;PublishGalaxy();} GalaxyStatus=$"游戏原生目录 · 视野 {GalaxySystems.Count:N0} 个星系 · 已覆盖 {update.CoveredCells:N0} / 待查 {update.PendingCells:N0} 个原星区"+ (update.Finished?(update.CoverageComplete?" · 当前原星区覆盖已完成":" · 仍有覆盖未确认"):$" · 正在补充(重试细分 {update.RefinedBatches:N0})")+ (update.OmittedCandidates>0?$" · 调度范围尚有 {update.OmittedCandidates:N0} 个候选未检查":""); } } private async Task CompletePublishedGalaxyOverridesAsync(GalaxyCamera camera,GalaxyCatalogueSelection selection,GalaxyCatalogueProfile profile, NativeGalaxyCatalog native,Func currentSource,CancellationToken token) { using var stop=CancellationTokenSource.CreateLinkedTokenSource(token,lifetime.Token); var previous=Interlocked.Exchange(ref galaxyPointOverridesStop,stop); try{previous?.Cancel();}catch(ObjectDisposedException){} try { await Task.Yield(); if(!currentSource()||!ReferenceEquals(selection,GalaxyCatalogueSelection)||stop.IsCancellationRequested)return; var points=await native.CompletePointOverridesAsync(profile,stop.Token); if(points is null||ReferenceEquals(points,selection.PointOverrides)||points.ProcessId!=native.ProcessId|| !currentSource()||!ReferenceEquals(selection,GalaxyCatalogueSelection)||stop.IsCancellationRequested)return; var updated=selection with{PointOverrides=points};GalaxyCatalogueSelection=updated; galaxyViewport=updated.ApplyTo(camera).ToViewport(); foreach(var row in points.Overrides.Values) if(row.System is{} system)RememberGalaxy(system,native.ProcessId); PublishGalaxy();QueueGalaxyFilterInformation(); _=ResolvePublishedGalaxyOverridesAsync(updated,native,currentSource,token); } catch(OperationCanceledException){} catch(Exception error)when(error is IOException or InvalidDataException or TimeoutException or InvalidOperationException or KeyNotFoundException){} finally{Interlocked.CompareExchange(ref galaxyPointOverridesStop,null,stop);} } private void TrimGalaxyCache(GalaxyViewport viewport) { if(galaxyCache.Count<=180000)return; var keep=new HashSet(StringComparer.OrdinalIgnoreCase); foreach(var point in CurrentPlayer.Navigation.Route)keep.Add(point.Name); keep.Add(CurrentPlayer.SystemName);if(GalaxySelection is {} selected)keep.Add(selected.Name); foreach(var place in GalaxyPlaces)keep.Add(place.SystemName); if(viewport.Camera is {} camera) { var selection=GalaxyCatalogueSelection;var filter=GalaxyFilter; bool Candidate(GalacticSystem system)=>filter.Matches(system)||GalaxyUsesOriginalFilterFacts(filter)&&filter.Values is not{Count:0}&&filter.IsUnknown(system); galaxyCacheLod.Begin(camera.Width,camera.Height,140000); foreach(var system in galaxyCache.Values) { if(selection?.Contains(system)==false||!Candidate(system))continue; var p=camera.ProjectForCatalogue(system.Position);if(p.IsVisible)galaxyCacheLod.Count(p.X,p.Y,GalaxyMapLod.IsPriority(system,selection,true,keep.Contains(system.Name))); } galaxyCacheLod.Seal(); foreach(var system in galaxyCache.Values) { if(selection?.Contains(system)==false||!Candidate(system))continue; var p=camera.ProjectForCatalogue(system.Position);if(p.IsVisible&&galaxyCacheLod.Accept(p.X,p.Y,GalaxyMapLod.IsPriority(system,selection,true,keep.Contains(system.Name))))keep.Add(system.Name); } } else foreach(var system in galaxyCache.Values) {if(keep.Count>=140000)break;if(viewport.Contains(system.Position,1.4))keep.Add(system.Name);} // Keep recent neighbouring tiles when the visible screen budget leaves // room. Never use a distance prefix that can empty sparse screen edges. foreach(var system in galaxyCache.Values.Reverse()) {if(keep.Count>=140000)break;keep.Add(system.Name);} foreach(var key in galaxyCache.Keys.Where(k=>!keep.Contains(k)).ToArray())galaxyCache.Remove(key); } private void NativeGalaxyProgress(NativeGalaxyCatalog sender,GalaxyNativeRegionProgress progress) { var epoch=Volatile.Read(ref galaxySourceEpoch); void Commit() { if(epoch!=Volatile.Read(ref galaxySourceEpoch)||!ReferenceEquals(sender,nativeGalaxy)||galaxyStreamSource!=sender.ProcessId ||galaxyRegionQueryForProbe is not null||NavigationActive||galaxyStreamLifetime.IsCancellationRequested ||runtime.FlightDriver is not NativeFlightControlDriver driver||driver.ProcessId!=sender.ProcessId||!driver.SupportsGalaxyRegion ||galaxyViewport is not {} viewport||lifetime.IsCancellationRequested)return; if(progress.Coverage.Scope=="requested-cells"&&(GalaxyCatalogueSelection is not {} selection||progress.Coverage.Origin!=selection.Origin))return; var accepted=0; foreach(var system in progress.Systems.Where(s=>viewport.Contains(s.Position,1.12)&&GalaxyCatalogueSelection?.Contains(s)!=false)){RememberGalaxy(system,sender.ProcessId);accepted++;} galaxyNativePartialRows+=accepted; if(accepted==0&&!progress.Done)return; TrimGalaxyCache(viewport); var now=Environment.TickCount64; if(now-galaxyLastPublish>=80||progress.Done) { galaxyLastPublish=now;PublishGalaxy(); GalaxyStatus=$"游戏原生目录 · 视野 {GalaxySystems.Count:N0} 个星系 · 当前分区已接收 {progress.DownloadedCount:N0}/{progress.TotalAvailable:N0},继续按视锥补查"+ (progress.Coverage.Error.Length>0?" · 部分单元失败,将细分重试":""); } } if(dispatcher.HasThreadAccess)Commit();else dispatcher.TryEnqueue(Commit); } private NativeGalaxyCatalog? NativeGalaxy() { if(runtime.FlightDriver is not NativeFlightControlDriver{SupportsGalaxyCatalog:true} driver)return null; if(nativeGalaxy?.ProcessId!=driver.ProcessId) { if(nativeGalaxy is not null&&galaxyNativeProgressHandler is not null)nativeGalaxy.RegionProgress-=galaxyNativeProgressHandler; var created=new NativeGalaxyCatalog(driver.ProcessId);nativeGalaxy=created; galaxyNativeProgressHandler=progress=>NativeGalaxyProgress(created,progress);created.RegionProgress+=galaxyNativeProgressHandler; } return nativeGalaxy; } private async Task FindGalacticSystemAsync(string name,CancellationToken token) { if(NativeGalaxy() is {} original) { try{return RememberGalaxy(await original.FindAsync(name,token),original.ProcessId);} catch(Exception error)when(error is KeyNotFoundException or InvalidDataException) { // An unrelated lookup result must never become the destination. // The independent completion service can still resolve an exact name. var exact=(await original.SuggestAsync(name,token)).FirstOrDefault(s=>s.Name.Equals(name,StringComparison.OrdinalIgnoreCase)); if(exact is not null)return RememberGalaxy(exact,original.ProcessId); } } var found=await new GalaxyCatalog(galaxyHttp).FindAsync(name,token); if(!found.Name.Equals(name,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("星系目录返回的名称与搜索目标不一致。"); return found; } internal async Task ResolveGalacticSelectionAsync(string name,CancellationToken token) { name=name.Trim();if(name.Length is <1 or >128)throw new ArgumentException("请输入有效的星系名称。"); var system=galaxyCache.GetValueOrDefault(name)??await FindGalacticSystemAsync(name,token); if(!system.Name.Equals(name,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("星系搜索结果与目标名称不一致。"); token.ThrowIfCancellationRequested();RememberGalaxy(system);PublishGalaxy();return system; } internal async Task> SuggestGalacticSystemsAsync(string query,CancellationToken token) { query=query.Trim();if(query.Length<2)return []; var local=galaxyCache.Values.Where(s=>s.Name.StartsWith(query,StringComparison.OrdinalIgnoreCase)).Take(20).ToArray(); IReadOnlyList found=[]; int? appearanceProcessId=null; if(!NavigationActive&&NativeGalaxy() is {} original) { try{found=await original.SuggestAsync(query,token);appearanceProcessId=original.ProcessId;} catch(Exception e)when(e is IOException or OperationCanceledException&&!token.IsCancellationRequested){ } } if(found.Count==0) { var names=await new GalaxyCatalog(galaxyHttp).SuggestNamesAsync(query,token); found=names.Select(n=>galaxyCache.GetValueOrDefault(n)??new GalacticSystem(n,new(double.NaN,double.NaN,double.NaN),Source:"选择后读取真实坐标")).ToArray(); } token.ThrowIfCancellationRequested(); foreach(var item in found.Where(s=>s.Position.IsValid))RememberGalaxy(item,appearanceProcessId); return found.Concat(local).DistinctBy(s=>s.Name,StringComparer.OrdinalIgnoreCase) .OrderByDescending(s=>s.Name.Equals(query,StringComparison.OrdinalIgnoreCase)).ThenBy(s=>s.Name).Take(20).ToArray(); } [ObservableProperty] public partial IReadOnlyList GalaxySystems{get;set;}=[]; [ObservableProperty] public partial IReadOnlyList GalaxyRoute{get;set;}=[]; [ObservableProperty] public partial Coordinates? GalaxyCentre{get;set;} [ObservableProperty] public partial GalacticSystem? GalaxySelection{get;set;} public bool HasGalaxySelection=>GalaxySelection is not null; [ObservableProperty] public partial string GalaxySearch{get;set;}=""; [ObservableProperty] public partial string GalaxyStatus{get;set;}="真实银河坐标 · 拖动旋转,右键拖动平移,滚轮缩放,点击选择星系。"; [ObservableProperty] public partial string GalaxyCoverage{get;set;}="等待本机位置与星系坐标"; [ObservableProperty] public partial double GalaxyRadius{get;set;}=30; [ObservableProperty] public partial bool GalaxyBusy{get;set;} [ObservableProperty] public partial bool GalaxyCatalogueBusy{get;set;} public bool GalaxyLoading=>GalaxyBusy||GalaxyCatalogueBusy; public string GalaxyLoadingText=>GalaxyBusy?"正在载入星系":"正在补充星区细节"; partial void OnGalaxyBusyChanged(bool value){OnPropertyChanged(nameof(GalaxyLoading));OnPropertyChanged(nameof(GalaxyLoadingText));} partial void OnGalaxyCatalogueBusyChanged(bool value){OnPropertyChanged(nameof(GalaxyLoading));OnPropertyChanged(nameof(GalaxyLoadingText));} partial void OnGalaxySelectionChanged(GalacticSystem? value) { OnPropertyChanged(nameof(HasGalaxySelection)); UseGalaxyDestinationCommand.NotifyCanExecuteChanged(); if(value is not null)GalaxyStatus=value.Name+" · "+value.Detail+(CurrentPlayer.Position is {} p?$" · 距当前 {p.DistanceTo(value.Position):F2} ly":""); } private async Task RefreshGalaxyAsync(PlayerState p,bool markets) { if(markets) { var stations=await Task.Run(()=>runtime.Markets.GetStations("",10000),lifetime.Token); foreach(var s in stations.Where(s=>s.Position is {IsValid:true}))galaxyCache.TryAdd(s.SystemName,new(s.SystemName,s.Position!,Source:"本机市场坐标")); } foreach(var w in p.Navigation.Route)RememberGalaxy(new(w.Name,w.Position,w.SystemAddress,w.StarClass,"游戏航线")); if(p.Position is {IsValid:true} position&&p.SystemName.Length>0) { RememberGalaxy(new(p.SystemName,position,p.Navigation.CurrentSystemAddress,galaxyCache.GetValueOrDefault(p.SystemName)?.StarClass??"",Source:"游戏当前位置")); if(GalaxyCentre is null)GalaxyCentre=position; } await RefreshGalaxyMetadataAsync(); GalaxyRoute=StableSequence(GalaxyRoute,p.Navigation.Route); PublishGalaxy(); } private void PublishGalaxy() { var retained=CurrentPlayer.Navigation.Route.Select(w=>w.Name).ToHashSet(StringComparer.OrdinalIgnoreCase); retained.Add(CurrentPlayer.SystemName);if(GalaxySelection is {} selected)retained.Add(selected.Name); foreach(var place in GalaxyPlaces)retained.Add(place.SystemName); var visible=galaxyCache.Values.Where(s=>GalaxyCatalogueSelection?.Contains(s)!=false&&(galaxyViewport is null||galaxyViewport.Contains(s.Position)||!GalaxyFilter.IsQueryActive&&retained.Contains(s.Name))); if(GalaxyFilter.IsQueryActive)visible=visible.Where(GalaxyFilter.Matches); // The control applies a fair screen-cell GPU budget across this bounded // cache; a preceding distance cut would remove sparse viewport edges. GalaxySystems=StableSequence(GalaxySystems,visible); GalaxyCoverage=$"{GalaxySystems.Count:N0} 个有真实坐标的星系 · {GalaxyRoute.Count} 个游戏航点 · 单位:光年(ly)"; } [RelayCommand] private void CentreGalaxy(){GalaxyCentre=CurrentPlayer.Position is {} p?new(p.X,p.Y,p.Z):GalaxySelection?.Position;} [RelayCommand] private async Task LoadNearbyGalaxyAsync(CancellationToken cancellationToken=default) { using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token; var centre=GalaxySelection?.Name??CurrentPlayer.SystemName;if(centre.Length==0){GalaxyStatus="尚未读取当前位置,请先搜索已知星系。";return;} var request=++galaxyRequest;GalaxyBusy=true; try { var systems=await new GalaxyCatalog(galaxyHttp).NearbyAsync(centre,(int)GalaxyRadius,token); token.ThrowIfCancellationRequested(); if(request!=galaxyRequest)return; foreach(var s in systems)RememberGalaxy(s); PublishGalaxy(); GalaxyStatus=$"已载入 {centre} 周围 {(int)GalaxyRadius} ly 的 {systems.Count:N0} 个已记录星系。公共目录不包含尚未上报的银河星系。"; await File.WriteAllTextAsync(Path.Combine(runtime.DataDirectory,"galaxy-cache.json"),JsonSerializer.Serialize(galaxyCache.Values,ToolboxSettings.JsonOptions),token); } catch(OperationCanceledException){if(request==galaxyRequest)GalaxyStatus="附近星系载入已取消。";} catch(Exception e)when(e is not OperationCanceledException){if(request==galaxyRequest)GalaxyStatus="载入失败:"+e.Message;} finally{if(request==galaxyRequest)GalaxyBusy=false;} } [RelayCommand] private async Task SearchGalaxyAsync(CancellationToken cancellationToken=default) { using var stop=CancellationTokenSource.CreateLinkedTokenSource(lifetime.Token,cancellationToken);var token=stop.Token; if(GalaxySearch.Trim().Length is <1 or >128)return;var request=++galaxyRequest;GalaxyBusy=true; try { var found=galaxyCache.GetValueOrDefault(GalaxySearch.Trim())??await FindGalacticSystemAsync(GalaxySearch.Trim(),token); token.ThrowIfCancellationRequested(); if(request!=galaxyRequest)return;RememberGalaxy(found);GalaxySelection=found;GalaxyCentre=found.Position;PublishGalaxy();GalaxyStatus=found.Name+" · "+found.Detail; } catch(OperationCanceledException){if(request==galaxyRequest)GalaxyStatus="星系搜索已取消。";} catch(Exception e)when(e is not OperationCanceledException){if(request==galaxyRequest)GalaxyStatus="搜索失败:"+e.Message;} finally{if(request==galaxyRequest)GalaxyBusy=false;} } private bool CanUseGalaxyDestination()=>GalaxySelection is not null&&!NavigationActive; [RelayCommand(CanExecute=nameof(CanUseGalaxyDestination))] private async Task UseGalaxyDestinationAsync() { if(!CanUseGalaxyDestination()||GalaxySelection is not {} selected)return;await ChooseNavigationTargetAsync(selected); GalaxyStatus="目的地已设为 "+selected.Name+";长按星图中的恒星,由游戏规划航路。"; } partial void OnNavigationActiveChanged(bool value){StartRemoteJourneyCommand.NotifyCanExecuteChanged();if(value)SuspendGalaxyStreaming();UseGalaxyDestinationCommand.NotifyCanExecuteChanged();PlanNavigationCommand.NotifyCanExecuteChanged();OnPropertyChanged(nameof(NavigationTargetLocked));OnPropertyChanged(nameof(NavigationTargetEditable));RefreshNavigationLegPresentation();UpdateSystemControlAvailability();} private async Task LoadGalaxyCacheAsync() { var path=Path.Combine(runtime.DataDirectory,"galaxy-cache.json"); try { if(File.Exists(path)&&new FileInfo(path).Length<64*1024*1024) foreach(var s in JsonSerializer.Deserialize(await File.ReadAllTextAsync(path,lifetime.Token),ToolboxSettings.JsonOptions)??[]) if(s.Position is {IsValid:true}&&s.Name.Length>0) RememberGalaxy(s); } catch(Exception e)when(e is IOException or JsonException){runtime.Log("星图","星系缓存读取失败:"+e.Message);} PublishGalaxy(); } }