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<string,GalacticSystem> galaxyCache=new(StringComparer.OrdinalIgnoreCase);
private Func<Coordinates,double,CancellationToken,Task<GalaxyRegionResult>>? galaxyRegionQueryForProbe;
internal void SetGalaxyRegionQueryForProbe(Func<Coordinates,double,CancellationToken,Task<GalaxyRegionResult>>? 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<GalacticSystem> 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<GalaxyNativeRegionProgress>? 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<GalaxyRegionResult> 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<bool> 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<bool> 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<string>(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<GalacticSystem> 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<GalacticSystem> 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<IReadOnlyList<GalacticSystem>> 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<GalacticSystem> 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<GalacticSystem> GalaxySystems{get;set;}=[];
[ObservableProperty] public partial IReadOnlyList<NavWaypoint> 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<GalacticSystem[]>(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();
}
}
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<string,GalacticSystem> galaxyCache=new(StringComparer.OrdinalIgnoreCase);
private Func<Coordinates,double,CancellationToken,Task<GalaxyRegionResult>>? galaxyRegionQueryForProbe;
internal void SetGalaxyRegionQueryForProbe(Func<Coordinates,double,CancellationToken,Task<GalaxyRegionResult>>? 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<GalacticSystem> 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<GalaxyNativeRegionProgress>? 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<GalaxyRegionResult> 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<bool> 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<bool> 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<string>(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<GalacticSystem> 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<GalacticSystem> 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<IReadOnlyList<GalacticSystem>> 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<GalacticSystem> 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<GalacticSystem> GalaxySystems{get;set;}=[];
[ObservableProperty] public partial IReadOnlyList<NavWaypoint> 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<GalacticSystem[]>(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();
}
}