using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Services;
using System.Text.Json;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
// Capture while the owner is alive. Background metadata continuations may
// still observe cancellation after Dispose has released the source itself.
private readonly CancellationToken galaxyLifetimeToken;
[ObservableProperty] public partial GalaxyMapFilter GalaxyFilter { get; set; }=GalaxyMapFilter.Default;
[ObservableProperty] public partial string GalaxyFilterInformationStatus { get; set; }="恒星类型来自游戏目录;其他数据仅使用实际观测记录。";
[ObservableProperty] public partial string GalaxyOriginalSettingsStatus { get; set; }="可读取游戏已保存的星图筛选和显示开关。";
[ObservableProperty] public partial string GalaxyOriginalSettingsDetails { get; set; }="";
[ObservableProperty] public partial IReadOnlyDictionary<string,string> GalaxyOriginalOptions { get; set; }=new Dictionary<string,string>();
public string GalaxyOriginalSettingsPath { get; private set; }="";
internal Task ImportOriginalForProbe()=>ImportOriginalGalaxySettingsAsync();
[RelayCommand] private async Task ImportOriginalGalaxySettingsAsync()
{
try
{
var directory=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"Frontier Developments","Elite Dangerous","Options","Player");
var start=Path.Combine(directory,"StartPreset.start");var preset=File.Exists(start)&&new FileInfo(start).Length<=128?(await File.ReadAllTextAsync(start,galaxyLifetimeToken)).Trim():"Custom";
if(preset.Length is <1 or >64||preset.Any(c=>!char.IsAsciiLetterOrDigit(c)&&c is not('_' or '-')))throw new InvalidDataException("游戏预设名称无效。");
var file=Directory.EnumerateFiles(directory,preset+".*.misc").OrderByDescending(File.GetLastWriteTimeUtc).FirstOrDefault()??throw new FileNotFoundException("没有找到游戏保存的玩家星图配置。");
var imported=await Task.Run(()=>GalaxyOriginalSettings.Read(file,GalaxyFilter),galaxyLifetimeToken);
GalaxyOriginalSettingsPath=file;GalaxyOriginalOptions=imported.OriginalOptions;GalaxyOriginalSettingsDetails=string.Join("\n",imported.Limitations);GalaxyOriginalSettingsStatus=Path.GetFileName(file)+" · "+imported.Summary;GalaxyFilter=imported.Filter;
}
catch(OperationCanceledException){ }
catch(Exception error)when(error is IOException or UnauthorizedAccessException or System.Xml.XmlException or InvalidDataException)
{GalaxyOriginalSettingsStatus="读取原游戏星图配置失败:"+error.Message;}
}
public double GalaxyShipJumpRange=>CurrentPlayer.MaxJumpRange is >0 and <1000?CurrentPlayer.MaxJumpRange.Value:0;
private DateTimeOffset galaxyMetadataReadAt;
private readonly HashSet<long> galaxyMetadataEvents=[];
private readonly Dictionary<string,GalaxySystemData> galaxyObservedFacts=new(StringComparer.OrdinalIgnoreCase);
private readonly GalaxyJournalMarkers galaxyJournalMarkers=new();
private readonly Dictionary<string,IReadOnlyList<string>> galaxyPersonalMarkers=new(StringComparer.OrdinalIgnoreCase);
private OriginalVisitedStarsSnapshot? galaxyOriginalVisited;
private IReadOnlyDictionary<long,OriginalTradeDataSnapshot> galaxyOriginalTrades=new Dictionary<long,OriginalTradeDataSnapshot>();
private IReadOnlyDictionary<long,IReadOnlyList<string>> galaxyAccountMarkers=new Dictionary<long,IReadOnlyList<string>>();
private IReadOnlyDictionary<long,IReadOnlyList<GalaxyBookmarkStyle>> galaxyBookmarkStyles=new Dictionary<long,IReadOnlyList<GalaxyBookmarkStyle>>();
private IReadOnlyDictionary<long,IReadOnlyList<string>> galaxySocialMarkers=new Dictionary<long,IReadOnlyList<string>>();
private string galaxySocialMarkerStatus="原游戏好友/队伍位置接口尚未连接;时效未确认的位置不作为实时标记。";
private string galaxyAccountMarkerStatus="原游戏书签接口尚未连接;缺少记录保持未知。";
private IReadOnlyDictionary<long,bool> galaxyColonyOwnership=new Dictionary<long,bool>();
private string galaxyOwnedColonisationStatus="原游戏殖民归属接口尚未连接;缺少记录保持未知。";
private DateTimeOffset galaxyNativeAccountReadAt;
private long galaxyNativeFactsVersion;
private string galaxyMetadataAccount="";
private Task? galaxyMetadataReadTask;
[ObservableProperty] public partial IReadOnlyList<GalaxyFilterChoice> GalaxyQueryChoices { get; set; }=[];
[ObservableProperty] public partial IReadOnlyList<GalacticSystem> GalaxyQuerySystems { get; set; }=[];
[ObservableProperty] public partial bool GalaxyQueryBusy { get; set; }
[ObservableProperty] public partial GalaxyQueryBounds? GalaxyActiveQueryBounds { get; set; }
public event Action<GalaxyQueryBounds,long>? GalaxyQueryStarted;
public event Action<GalaxyQueryResult,long>? GalaxyQueryProgress;
private GalaxyQueryService? galaxyQueries;
private CancellationTokenSource? galaxyFilterInformationStop;
private long galaxyFilterQueryGeneration;
private string galaxyQueuedQuery="";
private string galaxyRunningQuery="",galaxyRunningPredicate="";
private GalaxyQueryBounds? galaxyRunningBounds;
private GalaxyViewport? galaxyDesiredViewport;
private bool galaxyQueryPublished;
private readonly Dictionary<string,GalacticSystem> galaxyQueryEvidence=new(StringComparer.OrdinalIgnoreCase);
partial void OnGalaxyFilterChanged(GalaxyMapFilter oldValue,GalaxyMapFilter newValue)
{if(oldValue.Category!=newValue.Category)GalaxyQueryChoices=[];LocateGalaxyPowerHeadquartersCommand.NotifyCanExecuteChanged();PublishGalaxy();QueueGalaxyFilterInformation();}
private static bool GalaxyUsesAccountFacts(GalaxyMapFilter filter)=>filter.RequireOriginalCivilizationFacts||filter.RequireOriginalServiceFacts||filter.RequireOriginalCarrierFacts||filter.RequireOriginalStateFacts||filter.RequireOriginalColonisationOwnershipFacts||filter.RequireOriginalBasicSystemFacts||filter.RequiresOriginalCommodity||filter.SizeCategory==GalaxyFilterCategory.Commodity||filter.RequestsOriginalColonisationSize||filter.UsesPowerOuter||filter.UsesPowerActivityCounter||filter.Category is GalaxyFilterCategory.Markers or GalaxyFilterCategory.ThargoidWar or GalaxyFilterCategory.Colonisation or GalaxyFilterCategory.Visited or GalaxyFilterCategory.Exploration or GalaxyFilterCategory.TradeData or GalaxyFilterCategory.PowerActivityEligibility;
private void QueueGalaxyFilterInformation()
{
QueueGalaxyHistoricalTradeFlows();
QueueGalaxyCurrentTradeFlows();
var filter=GalaxyFilter;
if(!filter.IsQueryActive)
{galaxyFilterInformationStop?.Cancel();galaxyQueuedQuery="";galaxyDesiredViewport=null;GalaxyQueryBusy=false;GalaxyQuerySystems=[];QueueGalaxyOriginalFilterFacts();return;}
var focus=galaxyViewport?.Focus??GalaxyCentre??CurrentPlayer.Position;if(focus is not {IsValid:true})return;
var radius=Math.Max(5,GalaxyRadius);
var viewport=galaxyViewport??new(focus,new(1,0,0),new(0,1,0),new(0,0,1),radius,radius,radius);
QueueGalaxyOriginalFilterFacts(viewport);
var bounds=GalaxyQueryBounds.FromViewport(viewport);var quantum=Math.Max(.01,bounds.Radius/128);
var accountVersion=GalaxyUsesAccountFacts(filter)?galaxyNativeFactsVersion:0;
var key=filter.QueryKey+FormattableString.Invariant($"|{Math.Round(bounds.Minimum.X/quantum)}:{Math.Round(bounds.Minimum.Y/quantum)}:{Math.Round(bounds.Minimum.Z/quantum)}:{Math.Round(bounds.Maximum.X/quantum)}:{Math.Round(bounds.Maximum.Y/quantum)}:{Math.Round(bounds.Maximum.Z/quantum)}:{Math.Round(Math.Log2(bounds.Radius)*64)}:{Math.Round((viewport.Camera?.Yaw??0)*180/Math.PI)}:{Math.Round((viewport.Camera?.Pitch??0)*180/Math.PI)}:{filter.IncludeUnknown}:{filter.SizeCategory}:{filter.PopulationMinimum:R}:{filter.PopulationMaximum:R}:{accountVersion}");
if(galaxyQueuedQuery==key)return;
galaxyDesiredViewport=viewport;GalaxyActiveQueryBounds=bounds;
bool Overlaps(GalaxyQueryBounds a,GalaxyQueryBounds b)=>a.Minimum.X<=b.Maximum.X&&a.Maximum.X>=b.Minimum.X&&a.Minimum.Y<=b.Maximum.Y&&a.Maximum.Y>=b.Minimum.Y&&a.Minimum.Z<=b.Maximum.Z&&a.Maximum.Z>=b.Minimum.Z;
if(GalaxyQueryBusy&&galaxyRunningPredicate==filter.QueryKey&&galaxyRunningBounds is {} running&&Overlaps(bounds,running)&&(GalaxyQueryService.UsesPublicCatalog(filter.Category)||filter.SizeCategory==GalaxyFilterCategory.Population))
{
galaxyQueuedQuery=key;GalaxyQueryStarted?.Invoke(bounds,galaxyFilterQueryGeneration);
// Reuse the first in-flight page while panning. Once actual rows have
// arrived, switch to the latest overlapping view without starvation.
if(galaxyQueryPublished)galaxyFilterInformationStop?.Cancel();
return;
}
if(galaxyRunningPredicate!=filter.QueryKey)galaxyQueryEvidence.Clear();
galaxyQueuedQuery=key;galaxyFilterInformationStop?.Cancel();galaxyFilterInformationStop?.Dispose();
galaxyFilterInformationStop=CancellationTokenSource.CreateLinkedTokenSource(galaxyLifetimeToken);
var generation=++galaxyFilterQueryGeneration;galaxyRunningQuery=key;galaxyRunningPredicate=filter.QueryKey;galaxyRunningBounds=bounds;galaxyQueryPublished=false;GalaxyQueryBusy=true;GalaxyQueryStarted?.Invoke(bounds,generation);
_=QueryGalaxyViewportAsync(filter,viewport,generation,galaxyFilterInformationStop.Token);
}
private async Task QueryGalaxyViewportAsync(GalaxyMapFilter filter,GalaxyViewport viewport,long generation,CancellationToken token)
{
var focus=viewport.Focus;var bounds=GalaxyQueryBounds.FromViewport(viewport);
bool Latest()=>generation==galaxyFilterQueryGeneration&&GalaxyFilter.IsQueryActive&&GalaxyFilter.QueryKey==filter.QueryKey;
try
{
await Task.Delay(120,token);if(!Latest())return;
if(filter.Category==GalaxyFilterCategory.PowerActivityEligibility)
{await ReadGalaxyPowerOptionsAsync(token);if(!Latest())return;if(GalaxyFilter.OriginalPowerOptions.Count==0){galaxyQueries??=new(galaxyHttp);var powers=await galaxyQueries.ChoicesAsync(GalaxyFilterCategory.Power,token);if(!Latest())return;GalaxyQueryChoices=new[]{new GalaxyFilterChoice("__allpowers","全部势力(原活动选项)")}.Concat(powers.Where(p=>p.Key!="none")).ToArray();}}
var metadataFilter=!filter.UsesPowerOuter&&(!GalaxyQueryService.UsesPublicCatalog(filter.Category)||filter.Category==GalaxyFilterCategory.Commodity&&filter.RequiresOriginalCommodity)&&filter.SizeCategory==GalaxyFilterCategory.Population
?new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Population,PopulationMinimum=filter.PopulationMinimum,PopulationMaximum=filter.PopulationMaximum}:filter;
if(filter.UsesPowerOuter||metadataFilter==filter&&filter.Category==GalaxyFilterCategory.Commodity&&filter.RequiresOriginalCommodity||!GalaxyQueryService.UsesPublicCatalog(metadataFilter.Category))
{
await RefreshGalaxyMetadataAsync();token.ThrowIfCancellationRequested();if(!Latest())return;
GalaxyQuerySystems=galaxyCache.Values.Where(s=>viewport.Contains(s.Position,1.000001)&&filter.Matches(s)).ToArray();
if(filter.Category!=GalaxyFilterCategory.PowerActivityEligibility)GalaxyQueryChoices=[];PublishGalaxy();
GalaxyFilterInformationStatus=filter.Category==GalaxyFilterCategory.PowerActivityEligibility?galaxyPowerOptionsStatus+"\n"+(!filter.UseMinimumActivity?"未启用最低近期强度;当前不增加活动阈值过滤。":!filter.ActivityNativeQueryReady?"请选择原势力或全部势力后读取对应近期强度。":galaxySystemFactsStatus.Length>0?galaxySystemFactsStatus:"原近期活动计数尚未就绪;缺缓存保持未知。"):
GalaxyUsesOriginalFilterFacts(filter)&&galaxySystemFactsStatus.Length>0?galaxySystemFactsStatus:$"当前视野已知记录 · {GalaxyQuerySystems.Count:N0} 个匹配;尚未确认的账号属性保持未知。";return;
}
galaxyQueries??=new(galaxyHttp);GalaxyFilterInformationStatus="正在按当前完整视锥查询真实目录…";
if(filter.Category!=GalaxyFilterCategory.PowerActivityEligibility)
{var options=await galaxyQueries.ChoicesAsync(filter.Category,token);token.ThrowIfCancellationRequested();if(!Latest())return;GalaxyQueryChoices=filter.Category==GalaxyFilterCategory.Power?options.Concat(GalaxyFilter.OriginalPowerOptions.Select(p=>new GalaxyFilterChoice(GalaxyMapFilter.Normalize(p.Name),p.Name))).DistinctBy(c=>c.Key).ToArray():options;}
await RefreshGalaxyMetadataAsync();token.ThrowIfCancellationRequested();if(!Latest())return;
var additional=new List<GalacticSystem>();
if(galaxyThargoidContext==ThargoidContext()&&(filter.Category==GalaxyFilterCategory.ThargoidWar||filter.Category==GalaxyFilterCategory.Markers&&(filter.Values is null||filter.Values.Contains("titan"))))
{
var known=galaxyCache.Values.Where(s=>s.SystemAddress is not null).Select(s=>s.SystemAddress!.Value).ToHashSet();
foreach(var address in galaxyThargoidObservedStates.Keys.Where(a=>!known.Contains(a)).ToArray())
{
token.ThrowIfCancellationRequested();if(runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyResolveAddress:true}||NativeGalaxy() is not {} catalog)break;
try{var resolved=await catalog.ResolveAddressAsync(address,token);token.ThrowIfCancellationRequested();if(!Latest())return;RememberGalaxy(resolved);}
catch(Exception error)when(error is KeyNotFoundException or InvalidDataException or IOException){runtime.Log("星图 Thargoid 缓存",error.Message);}
}
additional.AddRange(galaxyCache.Values.Where(s=>viewport.Contains(s.Position,1.000001)&&filter.Matches(s)));
}
if(filter.Category==GalaxyFilterCategory.Services&&(filter.Values is null||filter.Values.Contains("engineer"))||filter.Category==GalaxyFilterCategory.Markers&&(filter.Values is null||filter.Values.Contains("engineer")))
additional.AddRange(EngineerDirectory.All.Where(e=>viewport.Contains(e.Position,1.000001)).Select(e=>new GalacticSystem(e.SystemName,e.Position,Source:"工程师驻地目录"){Data=new(){Services=["engineer"],Markers=["engineer"]}}));
if(filter.Category==GalaxyFilterCategory.Markers)
{
if(filter.Values is null||filter.Values.Any(k=>k is "bookmark" or "squadronbookmark" or "friend" or "wing" or "communitygoal" or "ship" or "mission"))
{
var knownAddresses=galaxyCache.Values.Where(s=>s.SystemAddress is not null).Select(s=>s.SystemAddress!.Value).ToHashSet();
foreach(var address in galaxyAccountMarkers.Keys.Concat(galaxySocialMarkers.Keys).Concat(galaxyCommunityGoalsContext==ThargoidContext()?galaxyCommunityGoalAddresses:[]).Concat(galaxyFleetContext==ThargoidContext()?galaxyFleetAddresses:[]).Concat(galaxyMissionsContext==ThargoidContext()?galaxyMissionAddresses:[]).Distinct().Where(a=>!knownAddresses.Contains(a)).ToArray())
{
token.ThrowIfCancellationRequested();if(runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyResolveAddress:true}||NativeGalaxy() is not {} catalog)break;
try{var resolved=await catalog.ResolveAddressAsync(address,token);token.ThrowIfCancellationRequested();if(!Latest())return;RememberGalaxy(resolved);}
catch(Exception e)when(e is KeyNotFoundException or InvalidDataException or IOException){runtime.Log("星图书签",e.Message);}
}
}
foreach(var name in galaxyPersonalMarkers.Keys.ToArray())
{
token.ThrowIfCancellationRequested();
var candidate=galaxyCache.GetValueOrDefault(name);
if(candidate is null)try{candidate=await FindGalacticSystemAsync(name,token);}catch(Exception e)when(e is HttpRequestException or InvalidDataException or JsonException){runtime.Log("星图标记",name+":"+e.Message);}
if(candidate is not null)RememberGalaxy(candidate);
}
additional.AddRange(galaxyCache.Values.Where(s=>viewport.Contains(s.Position,1.000001)&&filter.Matches(s)));
}
if(filter.Category==GalaxyFilterCategory.Colonisation&&(filter.Values is null||filter.Values.Contains("localplayerclaimed")))
{
var knownAddresses=galaxyCache.Values.Where(s=>s.SystemAddress is not null).Select(s=>s.SystemAddress!.Value).ToHashSet();
foreach(var address in galaxyColonyOwnership.Where(c=>c.Value&&!knownAddresses.Contains(c.Key)).Select(c=>c.Key).ToArray())
{
token.ThrowIfCancellationRequested();if(runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyResolveAddress:true}||NativeGalaxy() is not {} catalog)break;
try{var resolved=await catalog.ResolveAddressAsync(address,token);token.ThrowIfCancellationRequested();if(!Latest())return;RememberGalaxy(resolved);}
catch(Exception error)when(error is KeyNotFoundException or InvalidDataException or IOException){runtime.Log("星图殖民归属",error.Message);}
}
additional.AddRange(galaxyCache.Values.Where(s=>viewport.Contains(s.Position,1.000001)&&filter.Matches(s)));
}
void Commit(GalaxyQueryResult result)
{
if(!Latest()||token.IsCancellationRequested)return;
var accepted=new List<GalacticSystem>();
foreach(var system in result.Systems.Concat(additional).DistinctBy(s=>s.Name,StringComparer.OrdinalIgnoreCase))
{
if(galaxyCache.TryGetValue(system.Name,out var original))
{if(original.Position.DistanceTo(system.Position)>.25)continue;var merged=original with{Data=original.Data.Merge(system.Data),StarClass=original.StarClass.Length>0?original.StarClass:system.StarClass};galaxyCache[original.Name]=merged;accepted.Add(merged);}
else accepted.Add(RememberGalaxy(system));
}
foreach(var s in accepted)galaxyQueryEvidence[s.Name]=s;
var desired=galaxyDesiredViewport??viewport;
var visible=galaxyQueryEvidence.Values.Where(s=>desired.Contains(s.Position,1.000001)&&GalaxyFilter.Matches(s)).ToArray();
GalaxyQuerySystems=visible;galaxyQueryPublished=true;PublishGalaxy();GalaxyQueryProgress?.Invoke(result with{Systems=visible,CoverageComplete=result.CoverageComplete&&!filter.UsesPowerActivityCounter&&galaxyQueuedQuery==galaxyRunningQuery&&metadataFilter.Category==filter.Category},generation);GalaxyFilterInformationStatus=$"当前视锥 · 已返回 {visible.Length:N0} 个匹配 · 待查 {result.PendingQueries:N0} 个分区。";
if(galaxyQueuedQuery!=galaxyRunningQuery)galaxyFilterInformationStop?.Cancel();
}
var result=await galaxyQueries.QueryViewportAsync(metadataFilter,viewport,token,Commit);token.ThrowIfCancellationRequested();if(!Latest())return;Commit(result);
GalaxyFilterInformationStatus=$"当前视野 {GalaxyQuerySystems.Count:N0} 个匹配 · {result.Source}"+(metadataFilter.Category!=filter.Category?" · 人口资料已返回,原筛选属性仍需游戏确认。":filter.UsesPowerActivityCounter?" · 目录已返回,原近期活动条件持续刷新;未知隐藏。":result.CoverageComplete?" · 分区查询已完成。":" · 仍有覆盖待确认。")+(filter.Category==GalaxyFilterCategory.Markers?"\n"+GalaxyMarkerSources.Describe(filter.Values)+(filter.Values is null||filter.Values.Contains("bookmark")||filter.Values.Contains("squadronbookmark")?"\n"+galaxyAccountMarkerStatus:""):"");
if(filter.Category==GalaxyFilterCategory.Colonisation&&(filter.Values is null||filter.Values.Contains("localplayerclaimed")))GalaxyFilterInformationStatus+="\n"+galaxyOwnedColonisationStatus;
if(filter.Category==GalaxyFilterCategory.Commodity)GalaxyFilterInformationStatus+="\n"+galaxyCommodityOptionsStatus;
if(filter.Category==GalaxyFilterCategory.Markers&&(filter.Values is null||filter.Values.Contains("friend")||filter.Values.Contains("wing")))GalaxyFilterInformationStatus+="\n"+galaxySocialMarkerStatus;
if(filter.Category==GalaxyFilterCategory.Markers&&(filter.Values is null||filter.Values.Contains("communitygoal")))GalaxyFilterInformationStatus+="\n"+GalaxyCommunityGoalStatus;
}
catch(OperationCanceledException){ }
catch(Exception error){if(Latest()){galaxyQueuedQuery="";GalaxyFilterInformationStatus="星图查询失败:"+error.Message;}}
finally
{
if(generation==galaxyFilterQueryGeneration)
{
var follow=galaxyDesiredViewport is not null&&GalaxyFilter.IsQueryActive&&galaxyQueuedQuery!=galaxyRunningQuery&&GalaxyFilter.QueryKey==filter.QueryKey;
GalaxyQueryBusy=false;
if(follow){galaxyQueuedQuery="";QueueGalaxyFilterInformation();}
}
}
}
private GalacticSystem RememberGalaxy(GalacticSystem system,int? appearanceProcessId=null)
{
var nativeProcess=runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver driver?(int?)driver.ProcessId:null;
system=ApplyGalaxyAppearanceBinding(system,appearanceProcessId,nativeProcess,galaxyRegionQueryForProbe is not null);
var origin=CurrentPlayer.Navigation.CurrentSystemAddress;
system=system with{Data=system.Data.ForNativeContext(CurrentPlayer.CommanderId,nativeProcess,origin)};
if(string.Equals(galaxyMetadataAccount,CurrentPlayer.CommanderId,StringComparison.OrdinalIgnoreCase)&&galaxyObservedFacts.TryGetValue(system.Name,out var facts))system=system with{Data=system.Data.Merge(facts)};
if(galaxyCache.TryGetValue(system.Name,out var old))
{
var retainOriginal=old.Source=="游戏银河目录"&&system.Source!="游戏银河目录";
system=system with
{Position=retainOriginal?old.Position:system.Position,Source=retainOriginal?old.Source:system.Source,SystemAddress=retainOriginal?old.SystemAddress??system.SystemAddress:system.SystemAddress??old.SystemAddress,
OriginalAppearance=retainOriginal?old.OriginalAppearance:system.OriginalAppearance,
StarClass=retainOriginal&&old.StarClass.Length>0?old.StarClass:system.StarClass.Length>0?system.StarClass:old.StarClass,Data=old.Data.ForNativeContext(CurrentPlayer.CommanderId,nativeProcess,origin).Merge(system.Data)};
}
system=system with{Data=GalaxyPersonalFacts(system)};
if(old is not null&&old==system)return old;
galaxyCache[system.Name]=system;return system;
}
private GalaxySystemData GalaxyPersonalFacts(GalacticSystem system)
{
var data=system.Data;if(!string.Equals(galaxyMetadataAccount,CurrentPlayer.CommanderId,StringComparison.OrdinalIgnoreCase))return data;
var markers=galaxyPersonalMarkers.GetValueOrDefault(system.Name)??[];
var visited=system.SystemAddress is {} address&&galaxyOriginalVisited?.Contains(address)==true?true:(bool?)null;
var trade=system.SystemAddress is {} tradeAddress?galaxyOriginalTrades.GetValueOrDefault(tradeAddress):null;
bool? owned=system.SystemAddress is {} colonyAddress&&galaxyColonyOwnership.TryGetValue(colonyAddress,out var owner)?owner:null;
if(trade is {Routes.Count:>0})markers=markers.Concat(new[]{"traderoute"}).Distinct().ToArray();
if(system.SystemAddress is {} markerAddress&&galaxyAccountMarkers.TryGetValue(markerAddress,out var accountMarkers))markers=markers.Concat(accountMarkers).Distinct().ToArray();
if(system.SystemAddress is {} socialAddress&&galaxySocialMarkers.TryGetValue(socialAddress,out var socialMarkers))markers=markers.Concat(socialMarkers).Distinct().ToArray();
var runtimeMarkerKeys=new List<string>();
if(galaxyCommunityGoalsContext==ThargoidContext()&&system.SystemAddress is {} communityAddress&&galaxyCommunityGoalAddresses.Contains(communityAddress))runtimeMarkerKeys.Add("communitygoal");
if(galaxyFleetContext==ThargoidContext()&&system.SystemAddress is {} fleetAddress&&galaxyFleetAddresses.Contains(fleetAddress))runtimeMarkerKeys.Add("ship");
IReadOnlyList<int> missionMarkerTypes=[];
if(galaxyMissionsContext==ThargoidContext()&&system.SystemAddress is {} missionAddress&&galaxyMissionAddresses.Contains(missionAddress))
{runtimeMarkerKeys.Add("mission");missionMarkerTypes=galaxyMissionMarkerTypes.GetValueOrDefault(missionAddress)??[];}
GalaxyNativeAccountMarkerState? runtimeMarkers=null;
if(runtimeMarkerKeys.Count>0&&runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver markerDriver)
runtimeMarkers=data.NativeAccountMarkers is {} previous&&previous.AccountFid==CurrentPlayer.CommanderId&&previous.ProcessId==markerDriver.ProcessId&&previous.Keys.SequenceEqual(runtimeMarkerKeys)&&previous.MissionMarkerTypes.SequenceEqual(missionMarkerTypes)
?previous:new(CurrentPlayer.CommanderId,markerDriver.ProcessId,runtimeMarkerKeys){MissionMarkerTypes=missionMarkerTypes};
if(data.NativeAccountMarkers!=runtimeMarkers)data=data with{NativeAccountMarkers=runtimeMarkers};
var war=galaxyThargoidContext==ThargoidContext()&&runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver warDriver&&system.SystemAddress is {} warAddress&&galaxyThargoidObservedStates.TryGetValue(warAddress,out var state)?new GalaxyNativeThargoidState(CurrentPlayer.CommanderId,warDriver.ProcessId,state):null;
if(data.NativeThargoid!=war)data=data with{NativeThargoid=war};
var styles=system.SystemAddress is {} styleAddress?galaxyBookmarkStyles.GetValueOrDefault(styleAddress)??[]:[];
if(markers.Count==0&&data.PersonalMarkers is null&&styles.Count==0&&data.BookmarkStyles is null&&visited is null&&trade is null&&owned is null&&data.OwnedColony is null)return data;
var enriched=data.Merge(new(){AccountFid=CurrentPlayer.CommanderId,Visited=visited,PersonalMarkers=markers,BookmarkStyles=styles.Count>0||data.BookmarkStyles is not null?styles:null,TradeDataRecordedAt=trade?.Timestamp});
return enriched.OwnedColony==owned?enriched:enriched with{OwnedColony=owned};
}
private Task RefreshGalaxyMetadataAsync()
{
if(galaxyLifetimeToken.IsCancellationRequested)return Task.FromCanceled(galaxyLifetimeToken);
QueueGalaxyOriginalFilterFacts();QueueGalaxyHistoricalTradeFlows();QueueGalaxyCurrentTradeFlows();
return galaxyMetadataReadTask is {IsCompleted:false} running?running:galaxyMetadataReadTask=RefreshGalaxyMetadataCoreAsync();
}
private async Task RefreshGalaxyMetadataCoreAsync()
{
galaxyLifetimeToken.ThrowIfCancellationRequested();
// Page through the complete indexed local journal once, then stop at already observed rows.
var account=CurrentPlayer.CommanderId;if(string.IsNullOrWhiteSpace(account))return;
if(galaxyMetadataAccount!=account)
{galaxyMetadataAccount=account;galaxyMetadataReadAt=default;galaxyNativeAccountReadAt=default;galaxyNativeFactsVersion++;galaxyMetadataEvents.Clear();galaxyObservedFacts.Clear();galaxyJournalMarkers.Clear();galaxyPersonalMarkers.Clear();galaxyOriginalVisited=null;galaxyOriginalTrades=new Dictionary<long,OriginalTradeDataSnapshot>();galaxyAccountMarkers=new Dictionary<long,IReadOnlyList<string>>();galaxyBookmarkStyles=new Dictionary<long,IReadOnlyList<GalaxyBookmarkStyle>>();galaxySocialMarkers=new Dictionary<long,IReadOnlyList<string>>();galaxyColonyOwnership=new Dictionary<long,bool>();foreach(var s in galaxyCache.Values.ToArray())RememberGalaxy(s);}
if(DateTimeOffset.UtcNow-galaxyMetadataReadAt<TimeSpan.FromSeconds(20))
{
var previousVersion=galaxyNativeFactsVersion;await ReadNativeGalaxyAccountAsync(account);if(CurrentPlayer.CommanderId!=account)return;
if(RefreshGalaxyPersonalMarkers()||previousVersion!=galaxyNativeFactsVersion)foreach(var s in galaxyCache.Values.ToArray())RememberGalaxy(s);
if(previousVersion!=galaxyNativeFactsVersion&&GalaxyUsesAccountFacts(GalaxyFilter))QueueGalaxyFilterInformation();return;
}
galaxyMetadataReadAt=DateTimeOffset.UtcNow;
var rows=await Task.Run(()=>
{
var observations=new List<XFEEDToolBox.Core.Data.GameEventEntry>();
var owners=new Dictionary<string,HashSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach(var ownerEvent in new[]{"LoadGame","Commander","NewCommander"})
{
XFEEDToolBox.Core.Data.GameEventCursor? before=null;
while(true)
{
galaxyLifetimeToken.ThrowIfCancellationRequested();var page=runtime.GameEvents.Query(before:before,limit:500,eventName:ownerEvent);
foreach(var row in page.Entries)try{using var doc=JsonDocument.Parse(row.Json);if(doc.RootElement.TryGetProperty("FID",out var fid)&&fid.ValueKind==JsonValueKind.String&&fid.GetString() is {Length:>0} id){if(!owners.TryGetValue(row.Source,out var set))owners[row.Source]=set=new(StringComparer.OrdinalIgnoreCase);set.Add(id);}}catch(JsonException){ }
if(!page.HasMore||page.Entries.Count==0)break;before=page.Entries[^1].Cursor;
}
}
foreach(var eventName in new[]{"Location","FSDJump","Docked","BuyExplorationData","BuyTradeData","MissionAccepted","MissionRedirected","MissionCompleted","MissionFailed","MissionAbandoned","Missions","CommunityGoal","CommunityGoalDiscard","CommunityGoalReward"})
{
XFEEDToolBox.Core.Data.GameEventCursor? cursor=null;
while(true)
{
galaxyLifetimeToken.ThrowIfCancellationRequested();var page=runtime.GameEvents.Query(before:cursor,limit:500,eventName:eventName);var known=false;
foreach(var row in page.Entries){if(galaxyMetadataEvents.Contains(row.Id)){known=true;continue;}if(owners.TryGetValue(row.Source,out var fids)&&fids.Count==1&&fids.Contains(account))observations.Add(row);}
if(known||!page.HasMore||page.Entries.Count==0)break;cursor=page.Entries[^1].Cursor;
}
}
return observations.OrderBy(e=>e.Time).ThenBy(e=>e.Id).ToArray();
},galaxyLifetimeToken);
galaxyLifetimeToken.ThrowIfCancellationRequested();
if(CurrentPlayer.CommanderId!=account)return;
foreach(var row in rows)
{
galaxyMetadataEvents.Add(row.Id);
try
{
using var json=JsonDocument.Parse(row.Json);var e=json.RootElement;
galaxyJournalMarkers.Apply(e,row.Time);
if(GalaxySystemData.FromAcquisition(e) is {} acquisition&&e.GetProperty("System").GetString() is {} acquiredName)
{
acquisition=acquisition with{AccountFid=account};galaxyObservedFacts[acquiredName]=(galaxyObservedFacts.GetValueOrDefault(acquiredName)??new()).Merge(acquisition);
if(galaxyCache.TryGetValue(acquiredName,out var acquiredSystem))RememberGalaxy(acquiredSystem with{Data=acquiredSystem.Data.Merge(acquisition)});
continue;
}
if(!e.TryGetProperty("StarSystem",out var n)||n.ValueKind!=JsonValueKind.String||n.GetString() is not {Length:>0} name)continue;
GalacticSystem? s=galaxyCache.GetValueOrDefault(name);
if(s is null&&e.TryGetProperty("StarPos",out var p)&&p.ValueKind==JsonValueKind.Array&&p.GetArrayLength()==3)
{var position=new XFEEDToolBox.Core.Models.Coordinates(p[0].GetDouble(),p[1].GetDouble(),p[2].GetDouble());if(position.IsValid)s=new(name,position,Source:"本机 Journal");}
var data=GalaxySystemData.FromJournal(e) with{AccountFid=account};
var previous=galaxyObservedFacts.GetValueOrDefault(name)??s?.Data??new();
// A station's inventory proves that service exists here, not an exhaustive system inventory.
if(data.Services is {} services)data=data with{Services=(previous.Services??[]).Concat(services).Distinct(StringComparer.OrdinalIgnoreCase).ToArray()};
if(e.TryGetProperty("StationType",out var stationType)&&stationType.ValueKind==JsonValueKind.String&&stationType.GetString()=="FleetCarrier")data=data with{FleetCarrier=true};
galaxyObservedFacts[name]=previous.Merge(data);
if(s is null)continue;
RememberGalaxy(s with{Data=s.Data.Merge(data)});
}
catch(Exception error)when(error is JsonException or InvalidOperationException or FormatException){runtime.Log("星图元数据",error.Message);}
}
await ReadGalaxyAccountCachesAsync(account);
if(CurrentPlayer.CommanderId!=account)return;RefreshGalaxyPersonalMarkers();
foreach(var s in galaxyCache.Values.ToArray())RememberGalaxy(s);
OnPropertyChanged(nameof(GalaxyShipJumpRange));
}
private bool RefreshGalaxyPersonalMarkers()
{
var next=new Dictionary<string,IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
void Marker(string name,string key){if(string.IsNullOrWhiteSpace(name))return;next[name]=(next.GetValueOrDefault(name)??[]).Append(key).Distinct().ToArray();}
foreach(var ship in CurrentPlayer.Ships.Where(s=>!s.InTransit))Marker(ship.SystemName,"ship");
foreach(var waypoint in CurrentPlayer.Navigation.Route)Marker(waypoint.Name,"navigation");
foreach(var name in galaxyJournalMarkers.MissionSystems(DateTimeOffset.UtcNow))Marker(name,"mission");
foreach(var name in galaxyJournalMarkers.CommunityGoalSystems(DateTimeOffset.UtcNow))Marker(name,"communitygoal");
if(next.Count==galaxyPersonalMarkers.Count&&next.All(x=>galaxyPersonalMarkers.TryGetValue(x.Key,out var old)&&old.SequenceEqual(x.Value)))return false;
galaxyPersonalMarkers.Clear();foreach(var entry in next)galaxyPersonalMarkers.Add(entry.Key,entry.Value);return true;
}
private async Task ReadGalaxyAccountCachesAsync(string account)
{
var root=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"Frontier Developments","Elite Dangerous");
var snapshots=await Task.Run(()=>
{
OriginalVisitedStarsSnapshot? visited=null;var trades=new Dictionary<long,OriginalTradeDataSnapshot>();
try
{
visited=OriginalVisitedStarsCache.ReadForUser(root,account);
runtime.ResolveDiagnostic("galaxy.visited-cache:"+account,"星图账号缓存","原游戏访问缓存已恢复读取。");
var folder=Path.Combine(root,visited.CommanderId.ToString(System.Globalization.CultureInfo.InvariantCulture));
if(Directory.Exists(folder))foreach(var path in Directory.EnumerateFiles(folder,"*.trd"))
{
galaxyLifetimeToken.ThrowIfCancellationRequested();if(!long.TryParse(Path.GetFileNameWithoutExtension(path),out var address)||address<=0)continue;
var diagnostic="galaxy.trade-cache:"+account+":"+address;
try
{
trades[address]=OriginalTradeDataCache.Read(path,visited.CommanderId,address);
runtime.ResolveDiagnostic(diagnostic,"星图账号缓存","原游戏贸易缓存已恢复读取。");
}
catch(Exception e)when(e is IOException or UnauthorizedAccessException or InvalidDataException or JsonException or KeyNotFoundException or FormatException or ArgumentOutOfRangeException)
{runtime.ReportDiagnostic(diagnostic,"星图账号缓存",e,ActivitySeverity.Warning,"原游戏贸易缓存暂不可用:"+e.Message);}
}
}
catch(Exception e)when(e is IOException or UnauthorizedAccessException or InvalidDataException or ArgumentException)
{
var expected=e is FileNotFoundException or DirectoryNotFoundException or EndOfStreamException or OriginalCacheUpdatingException
||e is IOException io&&(io.HResult&0xffff) is 32 or 33;
runtime.ReportDiagnostic("galaxy.visited-cache:"+account,"星图账号缓存",e,expected?ActivitySeverity.Status:ActivitySeverity.Warning,
expected?"原游戏访问缓存尚未就绪,等待游戏生成或更新。":"原游戏访问缓存读取失败:"+e.Message);
}
return(visited,trades);
},galaxyLifetimeToken);
if(CurrentPlayer.CommanderId!=account)return;galaxyOriginalVisited=snapshots.visited;galaxyOriginalTrades=snapshots.trades;
await ReadNativeGalaxyAccountAsync(account);
}
private void SetGalaxyNativeBookmarks(IReadOnlyDictionary<long,IReadOnlyList<string>> markers,IReadOnlyDictionary<long,IReadOnlyList<GalaxyBookmarkStyle>> styles)
{
if(markers.Count==galaxyAccountMarkers.Count&&styles.Count==galaxyBookmarkStyles.Count&&markers.All(x=>galaxyAccountMarkers.TryGetValue(x.Key,out var old)&&old.SequenceEqual(x.Value))&&styles.All(x=>galaxyBookmarkStyles.TryGetValue(x.Key,out var old)&&old.SequenceEqual(x.Value)))return;
galaxyAccountMarkers=markers;galaxyBookmarkStyles=styles;galaxyNativeFactsVersion++;
}
private void SetGalaxyColonyOwnership(IReadOnlyDictionary<long,bool> colonies)
{if(galaxyColonyOwnership.Count==colonies.Count&&galaxyColonyOwnership.All(c=>colonies.TryGetValue(c.Key,out var own)&&own==c.Value))return;galaxyColonyOwnership=colonies;galaxyNativeFactsVersion++;}
private void SetGalaxyNativeSocial(IReadOnlyDictionary<long,IReadOnlyList<string>> markers)
{if(markers.Count==galaxySocialMarkers.Count&&markers.All(x=>galaxySocialMarkers.TryGetValue(x.Key,out var old)&&old.SequenceEqual(x.Value)))return;galaxySocialMarkers=markers;galaxyNativeFactsVersion++;}
private async Task ReadNativeGalaxyAccountAsync(string account)
{
if(DateTimeOffset.UtcNow-galaxyNativeAccountReadAt<TimeSpan.FromSeconds(2))return;galaxyNativeAccountReadAt=DateTimeOffset.UtcNow;
try
{
if(runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyAccountBookmarks:true} driver)
{
var snapshot=await new NativeGalaxyAccountCatalog(driver.ProcessId).ReadAsync(galaxyLifetimeToken);
if(CurrentPlayer.CommanderId!=account||runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver current||current.ProcessId!=driver.ProcessId)return;
// Private custom bookmark names and UIDs remain inside the native reply;
// only actual system addresses and marker kinds enter the map cache.
var markers=snapshot.Available?snapshot.Bookmarks.GroupBy(b=>b.SystemAddress).ToDictionary(g=>g.Key,g=>(IReadOnlyList<string>)g.Select(b=>b.Squadron?"squadronbookmark":"bookmark").Distinct().Order(StringComparer.Ordinal).ToArray()):new Dictionary<long,IReadOnlyList<string>>();
var styles=snapshot.Available?snapshot.Bookmarks.GroupBy(b=>b.SystemAddress).ToDictionary(g=>g.Key,g=>(IReadOnlyList<GalaxyBookmarkStyle>)g.Select(b=>new GalaxyBookmarkStyle(b.Icon,b.Squadron)).Distinct().OrderBy(s=>s.Squadron).ThenBy(s=>s.Icon).ToArray()):new Dictionary<long,IReadOnlyList<GalaxyBookmarkStyle>>();
SetGalaxyNativeBookmarks(markers,styles);
galaxyAccountMarkerStatus=snapshot.Available?$"原游戏书签管理器 · {snapshot.Bookmarks.Count:N0} 条实际记录;完整加载状态尚未确认。":"原游戏书签服务尚未就绪;缺少记录保持未知。";
}
else{SetGalaxyNativeBookmarks(new Dictionary<long,IReadOnlyList<string>>(),new Dictionary<long,IReadOnlyList<GalaxyBookmarkStyle>>());galaxyAccountMarkerStatus="原游戏书签接口尚未连接;缺少记录保持未知。";}
}
catch(OperationCanceledException)when(!lifetime.IsCancellationRequested)
{galaxyAccountMarkerStatus="原游戏书签读取超时;完整加载状态仍未知。";}
catch(Exception error)when(error is IOException or TimeoutException or InvalidDataException or InvalidOperationException or JsonException or KeyNotFoundException or FormatException)
{galaxyAccountMarkerStatus="原游戏书签读取尚未确认:"+error.Message;}
try
{
if(runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyAccountColonisation:true} driver&&galaxyOriginalVisited is {} verified)
{
var snapshot=await new NativeGalaxyAccountCatalog(driver.ProcessId).ReadColonisationAsync(galaxyLifetimeToken);
if(CurrentPlayer.CommanderId!=account||runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver current||current.ProcessId!=driver.ProcessId)return;
// An actual active claim proves its owner; an absent row proves neither owner.
SetGalaxyColonyOwnership(snapshot.Available?snapshot.Claims.Where(c=>c.Active).GroupBy(c=>c.SystemAddress).Where(g=>g.Select(c=>c.CommanderId).Distinct().Count()==1).ToDictionary(g=>g.Key,g=>g.First().CommanderId==verified.CommanderId):new Dictionary<long,bool>());
galaxyOwnedColonisationStatus=snapshot.Available?$"已匹配当前账号原CommanderId · {galaxyColonyOwnership.Values.Count(v=>v):N0} 个本人活动殖民地;原注册表完整性未知。":"原游戏殖民注册表尚未就绪;归属保持未知。";
}
else{SetGalaxyColonyOwnership(new Dictionary<long,bool>());galaxyOwnedColonisationStatus=galaxyOriginalVisited is null?"未取得当前账号原CommanderId证明;殖民归属保持未知。":"原游戏殖民归属接口尚未连接;缺少记录保持未知。";}
}
catch(OperationCanceledException)when(!lifetime.IsCancellationRequested)
{galaxyOwnedColonisationStatus="原游戏殖民归属读取超时;完整加载状态仍未知。";}
catch(Exception error)when(error is IOException or TimeoutException or InvalidDataException or InvalidOperationException or JsonException or KeyNotFoundException or FormatException)
{galaxyOwnedColonisationStatus="原游戏殖民归属尚未确认:"+error.Message;}
try
{
if(CurrentPlayer.InGame&&runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyAccountSocial:true} driver)
{
var session=CurrentPlayer.LoadGameAt;var snapshot=await new NativeGalaxyAccountCatalog(driver.ProcessId).ReadSocialAsync(galaxyLifetimeToken);
if(CurrentPlayer.CommanderId!=account||CurrentPlayer.LoadGameAt!=session||!CurrentPlayer.InGame||runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver current||current.ProcessId!=driver.ProcessId)return;
var markers=snapshot.Available?snapshot.Locations.Where(l=>l.LocationCurrent==true).GroupBy(l=>l.SystemAddress).ToDictionary(g=>g.Key,g=>(IReadOnlyList<string>)g.Select(l=>l.Kind).Distinct().Order(StringComparer.Ordinal).ToArray()):new Dictionary<long,IReadOnlyList<string>>();
SetGalaxyNativeSocial(markers);
var uncertain=snapshot.Locations.Count(l=>l.LocationCurrent is null);
galaxySocialMarkerStatus=snapshot.Available?$"原游戏社交位置 · {markers.Count:N0} 个确认实时星系"+(uncertain>0?$" · {uncertain:N0} 条时效未确认,不显示为实时位置。":";位置快照不证明全部好友/队伍覆盖。"):"原游戏社交位置尚未就绪;缺少资料保持未知。";
}
else{SetGalaxyNativeSocial(new Dictionary<long,IReadOnlyList<string>>());galaxySocialMarkerStatus="原游戏好友/队伍位置接口尚未连接;时效未确认的位置不作为实时标记。";}
}
catch(OperationCanceledException)when(!lifetime.IsCancellationRequested)
{SetGalaxyNativeSocial(new Dictionary<long,IReadOnlyList<string>>());galaxySocialMarkerStatus="原游戏社交位置读取超时;旧实时标记已清除,时效保持未知。";}
catch(Exception error)when(error is IOException or TimeoutException or InvalidDataException or InvalidOperationException or JsonException or KeyNotFoundException or FormatException)
{SetGalaxyNativeSocial(new Dictionary<long,IReadOnlyList<string>>());galaxySocialMarkerStatus="原游戏社交位置尚未确认:"+error.Message;}
await ReadGalaxyPowerOptionsAsync(galaxyLifetimeToken);
await ReadGalaxyCommodityOptionsAsync(galaxyLifetimeToken);
await ReadGalaxyThargoidAsync(galaxyLifetimeToken);
await ReadGalaxyCommunityGoalsAsync(galaxyLifetimeToken);
await ReadGalaxyFleetAsync(galaxyLifetimeToken);
await ReadGalaxyMissionsAsync(galaxyLifetimeToken);
}
}
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using XFEEDToolBox.Core.Galaxy;
using XFEEDToolBox.Core.Services;
using System.Text.Json;
namespace XFEEDToolBox.ViewModels;
public partial class ToolboxViewModel
{
// Capture while the owner is alive. Background metadata continuations may
// still observe cancellation after Dispose has released the source itself.
private readonly CancellationToken galaxyLifetimeToken;
[ObservableProperty] public partial GalaxyMapFilter GalaxyFilter { get; set; }=GalaxyMapFilter.Default;
[ObservableProperty] public partial string GalaxyFilterInformationStatus { get; set; }="恒星类型来自游戏目录;其他数据仅使用实际观测记录。";
[ObservableProperty] public partial string GalaxyOriginalSettingsStatus { get; set; }="可读取游戏已保存的星图筛选和显示开关。";
[ObservableProperty] public partial string GalaxyOriginalSettingsDetails { get; set; }="";
[ObservableProperty] public partial IReadOnlyDictionary<string,string> GalaxyOriginalOptions { get; set; }=new Dictionary<string,string>();
public string GalaxyOriginalSettingsPath { get; private set; }="";
internal Task ImportOriginalForProbe()=>ImportOriginalGalaxySettingsAsync();
[RelayCommand] private async Task ImportOriginalGalaxySettingsAsync()
{
try
{
var directory=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"Frontier Developments","Elite Dangerous","Options","Player");
var start=Path.Combine(directory,"StartPreset.start");var preset=File.Exists(start)&&new FileInfo(start).Length<=128?(await File.ReadAllTextAsync(start,galaxyLifetimeToken)).Trim():"Custom";
if(preset.Length is <1 or >64||preset.Any(c=>!char.IsAsciiLetterOrDigit(c)&&c is not('_' or '-')))throw new InvalidDataException("游戏预设名称无效。");
var file=Directory.EnumerateFiles(directory,preset+".*.misc").OrderByDescending(File.GetLastWriteTimeUtc).FirstOrDefault()??throw new FileNotFoundException("没有找到游戏保存的玩家星图配置。");
var imported=await Task.Run(()=>GalaxyOriginalSettings.Read(file,GalaxyFilter),galaxyLifetimeToken);
GalaxyOriginalSettingsPath=file;GalaxyOriginalOptions=imported.OriginalOptions;GalaxyOriginalSettingsDetails=string.Join("\n",imported.Limitations);GalaxyOriginalSettingsStatus=Path.GetFileName(file)+" · "+imported.Summary;GalaxyFilter=imported.Filter;
}
catch(OperationCanceledException){ }
catch(Exception error)when(error is IOException or UnauthorizedAccessException or System.Xml.XmlException or InvalidDataException)
{GalaxyOriginalSettingsStatus="读取原游戏星图配置失败:"+error.Message;}
}
public double GalaxyShipJumpRange=>CurrentPlayer.MaxJumpRange is >0 and <1000?CurrentPlayer.MaxJumpRange.Value:0;
private DateTimeOffset galaxyMetadataReadAt;
private readonly HashSet<long> galaxyMetadataEvents=[];
private readonly Dictionary<string,GalaxySystemData> galaxyObservedFacts=new(StringComparer.OrdinalIgnoreCase);
private readonly GalaxyJournalMarkers galaxyJournalMarkers=new();
private readonly Dictionary<string,IReadOnlyList<string>> galaxyPersonalMarkers=new(StringComparer.OrdinalIgnoreCase);
private OriginalVisitedStarsSnapshot? galaxyOriginalVisited;
private IReadOnlyDictionary<long,OriginalTradeDataSnapshot> galaxyOriginalTrades=new Dictionary<long,OriginalTradeDataSnapshot>();
private IReadOnlyDictionary<long,IReadOnlyList<string>> galaxyAccountMarkers=new Dictionary<long,IReadOnlyList<string>>();
private IReadOnlyDictionary<long,IReadOnlyList<GalaxyBookmarkStyle>> galaxyBookmarkStyles=new Dictionary<long,IReadOnlyList<GalaxyBookmarkStyle>>();
private IReadOnlyDictionary<long,IReadOnlyList<string>> galaxySocialMarkers=new Dictionary<long,IReadOnlyList<string>>();
private string galaxySocialMarkerStatus="原游戏好友/队伍位置接口尚未连接;时效未确认的位置不作为实时标记。";
private string galaxyAccountMarkerStatus="原游戏书签接口尚未连接;缺少记录保持未知。";
private IReadOnlyDictionary<long,bool> galaxyColonyOwnership=new Dictionary<long,bool>();
private string galaxyOwnedColonisationStatus="原游戏殖民归属接口尚未连接;缺少记录保持未知。";
private DateTimeOffset galaxyNativeAccountReadAt;
private long galaxyNativeFactsVersion;
private string galaxyMetadataAccount="";
private Task? galaxyMetadataReadTask;
[ObservableProperty] public partial IReadOnlyList<GalaxyFilterChoice> GalaxyQueryChoices { get; set; }=[];
[ObservableProperty] public partial IReadOnlyList<GalacticSystem> GalaxyQuerySystems { get; set; }=[];
[ObservableProperty] public partial bool GalaxyQueryBusy { get; set; }
[ObservableProperty] public partial GalaxyQueryBounds? GalaxyActiveQueryBounds { get; set; }
public event Action<GalaxyQueryBounds,long>? GalaxyQueryStarted;
public event Action<GalaxyQueryResult,long>? GalaxyQueryProgress;
private GalaxyQueryService? galaxyQueries;
private CancellationTokenSource? galaxyFilterInformationStop;
private long galaxyFilterQueryGeneration;
private string galaxyQueuedQuery="";
private string galaxyRunningQuery="",galaxyRunningPredicate="";
private GalaxyQueryBounds? galaxyRunningBounds;
private GalaxyViewport? galaxyDesiredViewport;
private bool galaxyQueryPublished;
private readonly Dictionary<string,GalacticSystem> galaxyQueryEvidence=new(StringComparer.OrdinalIgnoreCase);
partial void OnGalaxyFilterChanged(GalaxyMapFilter oldValue,GalaxyMapFilter newValue)
{if(oldValue.Category!=newValue.Category)GalaxyQueryChoices=[];LocateGalaxyPowerHeadquartersCommand.NotifyCanExecuteChanged();PublishGalaxy();QueueGalaxyFilterInformation();}
private static bool GalaxyUsesAccountFacts(GalaxyMapFilter filter)=>filter.RequireOriginalCivilizationFacts||filter.RequireOriginalServiceFacts||filter.RequireOriginalCarrierFacts||filter.RequireOriginalStateFacts||filter.RequireOriginalColonisationOwnershipFacts||filter.RequireOriginalBasicSystemFacts||filter.RequiresOriginalCommodity||filter.SizeCategory==GalaxyFilterCategory.Commodity||filter.RequestsOriginalColonisationSize||filter.UsesPowerOuter||filter.UsesPowerActivityCounter||filter.Category is GalaxyFilterCategory.Markers or GalaxyFilterCategory.ThargoidWar or GalaxyFilterCategory.Colonisation or GalaxyFilterCategory.Visited or GalaxyFilterCategory.Exploration or GalaxyFilterCategory.TradeData or GalaxyFilterCategory.PowerActivityEligibility;
private void QueueGalaxyFilterInformation()
{
QueueGalaxyHistoricalTradeFlows();
QueueGalaxyCurrentTradeFlows();
var filter=GalaxyFilter;
if(!filter.IsQueryActive)
{galaxyFilterInformationStop?.Cancel();galaxyQueuedQuery="";galaxyDesiredViewport=null;GalaxyQueryBusy=false;GalaxyQuerySystems=[];QueueGalaxyOriginalFilterFacts();return;}
var focus=galaxyViewport?.Focus??GalaxyCentre??CurrentPlayer.Position;if(focus is not {IsValid:true})return;
var radius=Math.Max(5,GalaxyRadius);
var viewport=galaxyViewport??new(focus,new(1,0,0),new(0,1,0),new(0,0,1),radius,radius,radius);
QueueGalaxyOriginalFilterFacts(viewport);
var bounds=GalaxyQueryBounds.FromViewport(viewport);var quantum=Math.Max(.01,bounds.Radius/128);
var accountVersion=GalaxyUsesAccountFacts(filter)?galaxyNativeFactsVersion:0;
var key=filter.QueryKey+FormattableString.Invariant($"|{Math.Round(bounds.Minimum.X/quantum)}:{Math.Round(bounds.Minimum.Y/quantum)}:{Math.Round(bounds.Minimum.Z/quantum)}:{Math.Round(bounds.Maximum.X/quantum)}:{Math.Round(bounds.Maximum.Y/quantum)}:{Math.Round(bounds.Maximum.Z/quantum)}:{Math.Round(Math.Log2(bounds.Radius)*64)}:{Math.Round((viewport.Camera?.Yaw??0)*180/Math.PI)}:{Math.Round((viewport.Camera?.Pitch??0)*180/Math.PI)}:{filter.IncludeUnknown}:{filter.SizeCategory}:{filter.PopulationMinimum:R}:{filter.PopulationMaximum:R}:{accountVersion}");
if(galaxyQueuedQuery==key)return;
galaxyDesiredViewport=viewport;GalaxyActiveQueryBounds=bounds;
bool Overlaps(GalaxyQueryBounds a,GalaxyQueryBounds b)=>a.Minimum.X<=b.Maximum.X&&a.Maximum.X>=b.Minimum.X&&a.Minimum.Y<=b.Maximum.Y&&a.Maximum.Y>=b.Minimum.Y&&a.Minimum.Z<=b.Maximum.Z&&a.Maximum.Z>=b.Minimum.Z;
if(GalaxyQueryBusy&&galaxyRunningPredicate==filter.QueryKey&&galaxyRunningBounds is {} running&&Overlaps(bounds,running)&&(GalaxyQueryService.UsesPublicCatalog(filter.Category)||filter.SizeCategory==GalaxyFilterCategory.Population))
{
galaxyQueuedQuery=key;GalaxyQueryStarted?.Invoke(bounds,galaxyFilterQueryGeneration);
// Reuse the first in-flight page while panning. Once actual rows have
// arrived, switch to the latest overlapping view without starvation.
if(galaxyQueryPublished)galaxyFilterInformationStop?.Cancel();
return;
}
if(galaxyRunningPredicate!=filter.QueryKey)galaxyQueryEvidence.Clear();
galaxyQueuedQuery=key;galaxyFilterInformationStop?.Cancel();galaxyFilterInformationStop?.Dispose();
galaxyFilterInformationStop=CancellationTokenSource.CreateLinkedTokenSource(galaxyLifetimeToken);
var generation=++galaxyFilterQueryGeneration;galaxyRunningQuery=key;galaxyRunningPredicate=filter.QueryKey;galaxyRunningBounds=bounds;galaxyQueryPublished=false;GalaxyQueryBusy=true;GalaxyQueryStarted?.Invoke(bounds,generation);
_=QueryGalaxyViewportAsync(filter,viewport,generation,galaxyFilterInformationStop.Token);
}
private async Task QueryGalaxyViewportAsync(GalaxyMapFilter filter,GalaxyViewport viewport,long generation,CancellationToken token)
{
var focus=viewport.Focus;var bounds=GalaxyQueryBounds.FromViewport(viewport);
bool Latest()=>generation==galaxyFilterQueryGeneration&&GalaxyFilter.IsQueryActive&&GalaxyFilter.QueryKey==filter.QueryKey;
try
{
await Task.Delay(120,token);if(!Latest())return;
if(filter.Category==GalaxyFilterCategory.PowerActivityEligibility)
{await ReadGalaxyPowerOptionsAsync(token);if(!Latest())return;if(GalaxyFilter.OriginalPowerOptions.Count==0){galaxyQueries??=new(galaxyHttp);var powers=await galaxyQueries.ChoicesAsync(GalaxyFilterCategory.Power,token);if(!Latest())return;GalaxyQueryChoices=new[]{new GalaxyFilterChoice("__allpowers","全部势力(原活动选项)")}.Concat(powers.Where(p=>p.Key!="none")).ToArray();}}
var metadataFilter=!filter.UsesPowerOuter&&(!GalaxyQueryService.UsesPublicCatalog(filter.Category)||filter.Category==GalaxyFilterCategory.Commodity&&filter.RequiresOriginalCommodity)&&filter.SizeCategory==GalaxyFilterCategory.Population
?new GalaxyMapFilter{Mode=GalaxyMapMode.Map,Category=GalaxyFilterCategory.Population,PopulationMinimum=filter.PopulationMinimum,PopulationMaximum=filter.PopulationMaximum}:filter;
if(filter.UsesPowerOuter||metadataFilter==filter&&filter.Category==GalaxyFilterCategory.Commodity&&filter.RequiresOriginalCommodity||!GalaxyQueryService.UsesPublicCatalog(metadataFilter.Category))
{
await RefreshGalaxyMetadataAsync();token.ThrowIfCancellationRequested();if(!Latest())return;
GalaxyQuerySystems=galaxyCache.Values.Where(s=>viewport.Contains(s.Position,1.000001)&&filter.Matches(s)).ToArray();
if(filter.Category!=GalaxyFilterCategory.PowerActivityEligibility)GalaxyQueryChoices=[];PublishGalaxy();
GalaxyFilterInformationStatus=filter.Category==GalaxyFilterCategory.PowerActivityEligibility?galaxyPowerOptionsStatus+"\n"+(!filter.UseMinimumActivity?"未启用最低近期强度;当前不增加活动阈值过滤。":!filter.ActivityNativeQueryReady?"请选择原势力或全部势力后读取对应近期强度。":galaxySystemFactsStatus.Length>0?galaxySystemFactsStatus:"原近期活动计数尚未就绪;缺缓存保持未知。"):
GalaxyUsesOriginalFilterFacts(filter)&&galaxySystemFactsStatus.Length>0?galaxySystemFactsStatus:$"当前视野已知记录 · {GalaxyQuerySystems.Count:N0} 个匹配;尚未确认的账号属性保持未知。";return;
}
galaxyQueries??=new(galaxyHttp);GalaxyFilterInformationStatus="正在按当前完整视锥查询真实目录…";
if(filter.Category!=GalaxyFilterCategory.PowerActivityEligibility)
{var options=await galaxyQueries.ChoicesAsync(filter.Category,token);token.ThrowIfCancellationRequested();if(!Latest())return;GalaxyQueryChoices=filter.Category==GalaxyFilterCategory.Power?options.Concat(GalaxyFilter.OriginalPowerOptions.Select(p=>new GalaxyFilterChoice(GalaxyMapFilter.Normalize(p.Name),p.Name))).DistinctBy(c=>c.Key).ToArray():options;}
await RefreshGalaxyMetadataAsync();token.ThrowIfCancellationRequested();if(!Latest())return;
var additional=new List<GalacticSystem>();
if(galaxyThargoidContext==ThargoidContext()&&(filter.Category==GalaxyFilterCategory.ThargoidWar||filter.Category==GalaxyFilterCategory.Markers&&(filter.Values is null||filter.Values.Contains("titan"))))
{
var known=galaxyCache.Values.Where(s=>s.SystemAddress is not null).Select(s=>s.SystemAddress!.Value).ToHashSet();
foreach(var address in galaxyThargoidObservedStates.Keys.Where(a=>!known.Contains(a)).ToArray())
{
token.ThrowIfCancellationRequested();if(runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyResolveAddress:true}||NativeGalaxy() is not {} catalog)break;
try{var resolved=await catalog.ResolveAddressAsync(address,token);token.ThrowIfCancellationRequested();if(!Latest())return;RememberGalaxy(resolved);}
catch(Exception error)when(error is KeyNotFoundException or InvalidDataException or IOException){runtime.Log("星图 Thargoid 缓存",error.Message);}
}
additional.AddRange(galaxyCache.Values.Where(s=>viewport.Contains(s.Position,1.000001)&&filter.Matches(s)));
}
if(filter.Category==GalaxyFilterCategory.Services&&(filter.Values is null||filter.Values.Contains("engineer"))||filter.Category==GalaxyFilterCategory.Markers&&(filter.Values is null||filter.Values.Contains("engineer")))
additional.AddRange(EngineerDirectory.All.Where(e=>viewport.Contains(e.Position,1.000001)).Select(e=>new GalacticSystem(e.SystemName,e.Position,Source:"工程师驻地目录"){Data=new(){Services=["engineer"],Markers=["engineer"]}}));
if(filter.Category==GalaxyFilterCategory.Markers)
{
if(filter.Values is null||filter.Values.Any(k=>k is "bookmark" or "squadronbookmark" or "friend" or "wing" or "communitygoal" or "ship" or "mission"))
{
var knownAddresses=galaxyCache.Values.Where(s=>s.SystemAddress is not null).Select(s=>s.SystemAddress!.Value).ToHashSet();
foreach(var address in galaxyAccountMarkers.Keys.Concat(galaxySocialMarkers.Keys).Concat(galaxyCommunityGoalsContext==ThargoidContext()?galaxyCommunityGoalAddresses:[]).Concat(galaxyFleetContext==ThargoidContext()?galaxyFleetAddresses:[]).Concat(galaxyMissionsContext==ThargoidContext()?galaxyMissionAddresses:[]).Distinct().Where(a=>!knownAddresses.Contains(a)).ToArray())
{
token.ThrowIfCancellationRequested();if(runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyResolveAddress:true}||NativeGalaxy() is not {} catalog)break;
try{var resolved=await catalog.ResolveAddressAsync(address,token);token.ThrowIfCancellationRequested();if(!Latest())return;RememberGalaxy(resolved);}
catch(Exception e)when(e is KeyNotFoundException or InvalidDataException or IOException){runtime.Log("星图书签",e.Message);}
}
}
foreach(var name in galaxyPersonalMarkers.Keys.ToArray())
{
token.ThrowIfCancellationRequested();
var candidate=galaxyCache.GetValueOrDefault(name);
if(candidate is null)try{candidate=await FindGalacticSystemAsync(name,token);}catch(Exception e)when(e is HttpRequestException or InvalidDataException or JsonException){runtime.Log("星图标记",name+":"+e.Message);}
if(candidate is not null)RememberGalaxy(candidate);
}
additional.AddRange(galaxyCache.Values.Where(s=>viewport.Contains(s.Position,1.000001)&&filter.Matches(s)));
}
if(filter.Category==GalaxyFilterCategory.Colonisation&&(filter.Values is null||filter.Values.Contains("localplayerclaimed")))
{
var knownAddresses=galaxyCache.Values.Where(s=>s.SystemAddress is not null).Select(s=>s.SystemAddress!.Value).ToHashSet();
foreach(var address in galaxyColonyOwnership.Where(c=>c.Value&&!knownAddresses.Contains(c.Key)).Select(c=>c.Key).ToArray())
{
token.ThrowIfCancellationRequested();if(runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyResolveAddress:true}||NativeGalaxy() is not {} catalog)break;
try{var resolved=await catalog.ResolveAddressAsync(address,token);token.ThrowIfCancellationRequested();if(!Latest())return;RememberGalaxy(resolved);}
catch(Exception error)when(error is KeyNotFoundException or InvalidDataException or IOException){runtime.Log("星图殖民归属",error.Message);}
}
additional.AddRange(galaxyCache.Values.Where(s=>viewport.Contains(s.Position,1.000001)&&filter.Matches(s)));
}
void Commit(GalaxyQueryResult result)
{
if(!Latest()||token.IsCancellationRequested)return;
var accepted=new List<GalacticSystem>();
foreach(var system in result.Systems.Concat(additional).DistinctBy(s=>s.Name,StringComparer.OrdinalIgnoreCase))
{
if(galaxyCache.TryGetValue(system.Name,out var original))
{if(original.Position.DistanceTo(system.Position)>.25)continue;var merged=original with{Data=original.Data.Merge(system.Data),StarClass=original.StarClass.Length>0?original.StarClass:system.StarClass};galaxyCache[original.Name]=merged;accepted.Add(merged);}
else accepted.Add(RememberGalaxy(system));
}
foreach(var s in accepted)galaxyQueryEvidence[s.Name]=s;
var desired=galaxyDesiredViewport??viewport;
var visible=galaxyQueryEvidence.Values.Where(s=>desired.Contains(s.Position,1.000001)&&GalaxyFilter.Matches(s)).ToArray();
GalaxyQuerySystems=visible;galaxyQueryPublished=true;PublishGalaxy();GalaxyQueryProgress?.Invoke(result with{Systems=visible,CoverageComplete=result.CoverageComplete&&!filter.UsesPowerActivityCounter&&galaxyQueuedQuery==galaxyRunningQuery&&metadataFilter.Category==filter.Category},generation);GalaxyFilterInformationStatus=$"当前视锥 · 已返回 {visible.Length:N0} 个匹配 · 待查 {result.PendingQueries:N0} 个分区。";
if(galaxyQueuedQuery!=galaxyRunningQuery)galaxyFilterInformationStop?.Cancel();
}
var result=await galaxyQueries.QueryViewportAsync(metadataFilter,viewport,token,Commit);token.ThrowIfCancellationRequested();if(!Latest())return;Commit(result);
GalaxyFilterInformationStatus=$"当前视野 {GalaxyQuerySystems.Count:N0} 个匹配 · {result.Source}"+(metadataFilter.Category!=filter.Category?" · 人口资料已返回,原筛选属性仍需游戏确认。":filter.UsesPowerActivityCounter?" · 目录已返回,原近期活动条件持续刷新;未知隐藏。":result.CoverageComplete?" · 分区查询已完成。":" · 仍有覆盖待确认。")+(filter.Category==GalaxyFilterCategory.Markers?"\n"+GalaxyMarkerSources.Describe(filter.Values)+(filter.Values is null||filter.Values.Contains("bookmark")||filter.Values.Contains("squadronbookmark")?"\n"+galaxyAccountMarkerStatus:""):"");
if(filter.Category==GalaxyFilterCategory.Colonisation&&(filter.Values is null||filter.Values.Contains("localplayerclaimed")))GalaxyFilterInformationStatus+="\n"+galaxyOwnedColonisationStatus;
if(filter.Category==GalaxyFilterCategory.Commodity)GalaxyFilterInformationStatus+="\n"+galaxyCommodityOptionsStatus;
if(filter.Category==GalaxyFilterCategory.Markers&&(filter.Values is null||filter.Values.Contains("friend")||filter.Values.Contains("wing")))GalaxyFilterInformationStatus+="\n"+galaxySocialMarkerStatus;
if(filter.Category==GalaxyFilterCategory.Markers&&(filter.Values is null||filter.Values.Contains("communitygoal")))GalaxyFilterInformationStatus+="\n"+GalaxyCommunityGoalStatus;
}
catch(OperationCanceledException){ }
catch(Exception error){if(Latest()){galaxyQueuedQuery="";GalaxyFilterInformationStatus="星图查询失败:"+error.Message;}}
finally
{
if(generation==galaxyFilterQueryGeneration)
{
var follow=galaxyDesiredViewport is not null&&GalaxyFilter.IsQueryActive&&galaxyQueuedQuery!=galaxyRunningQuery&&GalaxyFilter.QueryKey==filter.QueryKey;
GalaxyQueryBusy=false;
if(follow){galaxyQueuedQuery="";QueueGalaxyFilterInformation();}
}
}
}
private GalacticSystem RememberGalaxy(GalacticSystem system,int? appearanceProcessId=null)
{
var nativeProcess=runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver driver?(int?)driver.ProcessId:null;
system=ApplyGalaxyAppearanceBinding(system,appearanceProcessId,nativeProcess,galaxyRegionQueryForProbe is not null);
var origin=CurrentPlayer.Navigation.CurrentSystemAddress;
system=system with{Data=system.Data.ForNativeContext(CurrentPlayer.CommanderId,nativeProcess,origin)};
if(string.Equals(galaxyMetadataAccount,CurrentPlayer.CommanderId,StringComparison.OrdinalIgnoreCase)&&galaxyObservedFacts.TryGetValue(system.Name,out var facts))system=system with{Data=system.Data.Merge(facts)};
if(galaxyCache.TryGetValue(system.Name,out var old))
{
var retainOriginal=old.Source=="游戏银河目录"&&system.Source!="游戏银河目录";
system=system with
{Position=retainOriginal?old.Position:system.Position,Source=retainOriginal?old.Source:system.Source,SystemAddress=retainOriginal?old.SystemAddress??system.SystemAddress:system.SystemAddress??old.SystemAddress,
OriginalAppearance=retainOriginal?old.OriginalAppearance:system.OriginalAppearance,
StarClass=retainOriginal&&old.StarClass.Length>0?old.StarClass:system.StarClass.Length>0?system.StarClass:old.StarClass,Data=old.Data.ForNativeContext(CurrentPlayer.CommanderId,nativeProcess,origin).Merge(system.Data)};
}
system=system with{Data=GalaxyPersonalFacts(system)};
if(old is not null&&old==system)return old;
galaxyCache[system.Name]=system;return system;
}
private GalaxySystemData GalaxyPersonalFacts(GalacticSystem system)
{
var data=system.Data;if(!string.Equals(galaxyMetadataAccount,CurrentPlayer.CommanderId,StringComparison.OrdinalIgnoreCase))return data;
var markers=galaxyPersonalMarkers.GetValueOrDefault(system.Name)??[];
var visited=system.SystemAddress is {} address&&galaxyOriginalVisited?.Contains(address)==true?true:(bool?)null;
var trade=system.SystemAddress is {} tradeAddress?galaxyOriginalTrades.GetValueOrDefault(tradeAddress):null;
bool? owned=system.SystemAddress is {} colonyAddress&&galaxyColonyOwnership.TryGetValue(colonyAddress,out var owner)?owner:null;
if(trade is {Routes.Count:>0})markers=markers.Concat(new[]{"traderoute"}).Distinct().ToArray();
if(system.SystemAddress is {} markerAddress&&galaxyAccountMarkers.TryGetValue(markerAddress,out var accountMarkers))markers=markers.Concat(accountMarkers).Distinct().ToArray();
if(system.SystemAddress is {} socialAddress&&galaxySocialMarkers.TryGetValue(socialAddress,out var socialMarkers))markers=markers.Concat(socialMarkers).Distinct().ToArray();
var runtimeMarkerKeys=new List<string>();
if(galaxyCommunityGoalsContext==ThargoidContext()&&system.SystemAddress is {} communityAddress&&galaxyCommunityGoalAddresses.Contains(communityAddress))runtimeMarkerKeys.Add("communitygoal");
if(galaxyFleetContext==ThargoidContext()&&system.SystemAddress is {} fleetAddress&&galaxyFleetAddresses.Contains(fleetAddress))runtimeMarkerKeys.Add("ship");
IReadOnlyList<int> missionMarkerTypes=[];
if(galaxyMissionsContext==ThargoidContext()&&system.SystemAddress is {} missionAddress&&galaxyMissionAddresses.Contains(missionAddress))
{runtimeMarkerKeys.Add("mission");missionMarkerTypes=galaxyMissionMarkerTypes.GetValueOrDefault(missionAddress)??[];}
GalaxyNativeAccountMarkerState? runtimeMarkers=null;
if(runtimeMarkerKeys.Count>0&&runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver markerDriver)
runtimeMarkers=data.NativeAccountMarkers is {} previous&&previous.AccountFid==CurrentPlayer.CommanderId&&previous.ProcessId==markerDriver.ProcessId&&previous.Keys.SequenceEqual(runtimeMarkerKeys)&&previous.MissionMarkerTypes.SequenceEqual(missionMarkerTypes)
?previous:new(CurrentPlayer.CommanderId,markerDriver.ProcessId,runtimeMarkerKeys){MissionMarkerTypes=missionMarkerTypes};
if(data.NativeAccountMarkers!=runtimeMarkers)data=data with{NativeAccountMarkers=runtimeMarkers};
var war=galaxyThargoidContext==ThargoidContext()&&runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver warDriver&&system.SystemAddress is {} warAddress&&galaxyThargoidObservedStates.TryGetValue(warAddress,out var state)?new GalaxyNativeThargoidState(CurrentPlayer.CommanderId,warDriver.ProcessId,state):null;
if(data.NativeThargoid!=war)data=data with{NativeThargoid=war};
var styles=system.SystemAddress is {} styleAddress?galaxyBookmarkStyles.GetValueOrDefault(styleAddress)??[]:[];
if(markers.Count==0&&data.PersonalMarkers is null&&styles.Count==0&&data.BookmarkStyles is null&&visited is null&&trade is null&&owned is null&&data.OwnedColony is null)return data;
var enriched=data.Merge(new(){AccountFid=CurrentPlayer.CommanderId,Visited=visited,PersonalMarkers=markers,BookmarkStyles=styles.Count>0||data.BookmarkStyles is not null?styles:null,TradeDataRecordedAt=trade?.Timestamp});
return enriched.OwnedColony==owned?enriched:enriched with{OwnedColony=owned};
}
private Task RefreshGalaxyMetadataAsync()
{
if(galaxyLifetimeToken.IsCancellationRequested)return Task.FromCanceled(galaxyLifetimeToken);
QueueGalaxyOriginalFilterFacts();QueueGalaxyHistoricalTradeFlows();QueueGalaxyCurrentTradeFlows();
return galaxyMetadataReadTask is {IsCompleted:false} running?running:galaxyMetadataReadTask=RefreshGalaxyMetadataCoreAsync();
}
private async Task RefreshGalaxyMetadataCoreAsync()
{
galaxyLifetimeToken.ThrowIfCancellationRequested();
// Page through the complete indexed local journal once, then stop at already observed rows.
var account=CurrentPlayer.CommanderId;if(string.IsNullOrWhiteSpace(account))return;
if(galaxyMetadataAccount!=account)
{galaxyMetadataAccount=account;galaxyMetadataReadAt=default;galaxyNativeAccountReadAt=default;galaxyNativeFactsVersion++;galaxyMetadataEvents.Clear();galaxyObservedFacts.Clear();galaxyJournalMarkers.Clear();galaxyPersonalMarkers.Clear();galaxyOriginalVisited=null;galaxyOriginalTrades=new Dictionary<long,OriginalTradeDataSnapshot>();galaxyAccountMarkers=new Dictionary<long,IReadOnlyList<string>>();galaxyBookmarkStyles=new Dictionary<long,IReadOnlyList<GalaxyBookmarkStyle>>();galaxySocialMarkers=new Dictionary<long,IReadOnlyList<string>>();galaxyColonyOwnership=new Dictionary<long,bool>();foreach(var s in galaxyCache.Values.ToArray())RememberGalaxy(s);}
if(DateTimeOffset.UtcNow-galaxyMetadataReadAt<TimeSpan.FromSeconds(20))
{
var previousVersion=galaxyNativeFactsVersion;await ReadNativeGalaxyAccountAsync(account);if(CurrentPlayer.CommanderId!=account)return;
if(RefreshGalaxyPersonalMarkers()||previousVersion!=galaxyNativeFactsVersion)foreach(var s in galaxyCache.Values.ToArray())RememberGalaxy(s);
if(previousVersion!=galaxyNativeFactsVersion&&GalaxyUsesAccountFacts(GalaxyFilter))QueueGalaxyFilterInformation();return;
}
galaxyMetadataReadAt=DateTimeOffset.UtcNow;
var rows=await Task.Run(()=>
{
var observations=new List<XFEEDToolBox.Core.Data.GameEventEntry>();
var owners=new Dictionary<string,HashSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach(var ownerEvent in new[]{"LoadGame","Commander","NewCommander"})
{
XFEEDToolBox.Core.Data.GameEventCursor? before=null;
while(true)
{
galaxyLifetimeToken.ThrowIfCancellationRequested();var page=runtime.GameEvents.Query(before:before,limit:500,eventName:ownerEvent);
foreach(var row in page.Entries)try{using var doc=JsonDocument.Parse(row.Json);if(doc.RootElement.TryGetProperty("FID",out var fid)&&fid.ValueKind==JsonValueKind.String&&fid.GetString() is {Length:>0} id){if(!owners.TryGetValue(row.Source,out var set))owners[row.Source]=set=new(StringComparer.OrdinalIgnoreCase);set.Add(id);}}catch(JsonException){ }
if(!page.HasMore||page.Entries.Count==0)break;before=page.Entries[^1].Cursor;
}
}
foreach(var eventName in new[]{"Location","FSDJump","Docked","BuyExplorationData","BuyTradeData","MissionAccepted","MissionRedirected","MissionCompleted","MissionFailed","MissionAbandoned","Missions","CommunityGoal","CommunityGoalDiscard","CommunityGoalReward"})
{
XFEEDToolBox.Core.Data.GameEventCursor? cursor=null;
while(true)
{
galaxyLifetimeToken.ThrowIfCancellationRequested();var page=runtime.GameEvents.Query(before:cursor,limit:500,eventName:eventName);var known=false;
foreach(var row in page.Entries){if(galaxyMetadataEvents.Contains(row.Id)){known=true;continue;}if(owners.TryGetValue(row.Source,out var fids)&&fids.Count==1&&fids.Contains(account))observations.Add(row);}
if(known||!page.HasMore||page.Entries.Count==0)break;cursor=page.Entries[^1].Cursor;
}
}
return observations.OrderBy(e=>e.Time).ThenBy(e=>e.Id).ToArray();
},galaxyLifetimeToken);
galaxyLifetimeToken.ThrowIfCancellationRequested();
if(CurrentPlayer.CommanderId!=account)return;
foreach(var row in rows)
{
galaxyMetadataEvents.Add(row.Id);
try
{
using var json=JsonDocument.Parse(row.Json);var e=json.RootElement;
galaxyJournalMarkers.Apply(e,row.Time);
if(GalaxySystemData.FromAcquisition(e) is {} acquisition&&e.GetProperty("System").GetString() is {} acquiredName)
{
acquisition=acquisition with{AccountFid=account};galaxyObservedFacts[acquiredName]=(galaxyObservedFacts.GetValueOrDefault(acquiredName)??new()).Merge(acquisition);
if(galaxyCache.TryGetValue(acquiredName,out var acquiredSystem))RememberGalaxy(acquiredSystem with{Data=acquiredSystem.Data.Merge(acquisition)});
continue;
}
if(!e.TryGetProperty("StarSystem",out var n)||n.ValueKind!=JsonValueKind.String||n.GetString() is not {Length:>0} name)continue;
GalacticSystem? s=galaxyCache.GetValueOrDefault(name);
if(s is null&&e.TryGetProperty("StarPos",out var p)&&p.ValueKind==JsonValueKind.Array&&p.GetArrayLength()==3)
{var position=new XFEEDToolBox.Core.Models.Coordinates(p[0].GetDouble(),p[1].GetDouble(),p[2].GetDouble());if(position.IsValid)s=new(name,position,Source:"本机 Journal");}
var data=GalaxySystemData.FromJournal(e) with{AccountFid=account};
var previous=galaxyObservedFacts.GetValueOrDefault(name)??s?.Data??new();
// A station's inventory proves that service exists here, not an exhaustive system inventory.
if(data.Services is {} services)data=data with{Services=(previous.Services??[]).Concat(services).Distinct(StringComparer.OrdinalIgnoreCase).ToArray()};
if(e.TryGetProperty("StationType",out var stationType)&&stationType.ValueKind==JsonValueKind.String&&stationType.GetString()=="FleetCarrier")data=data with{FleetCarrier=true};
galaxyObservedFacts[name]=previous.Merge(data);
if(s is null)continue;
RememberGalaxy(s with{Data=s.Data.Merge(data)});
}
catch(Exception error)when(error is JsonException or InvalidOperationException or FormatException){runtime.Log("星图元数据",error.Message);}
}
await ReadGalaxyAccountCachesAsync(account);
if(CurrentPlayer.CommanderId!=account)return;RefreshGalaxyPersonalMarkers();
foreach(var s in galaxyCache.Values.ToArray())RememberGalaxy(s);
OnPropertyChanged(nameof(GalaxyShipJumpRange));
}
private bool RefreshGalaxyPersonalMarkers()
{
var next=new Dictionary<string,IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
void Marker(string name,string key){if(string.IsNullOrWhiteSpace(name))return;next[name]=(next.GetValueOrDefault(name)??[]).Append(key).Distinct().ToArray();}
foreach(var ship in CurrentPlayer.Ships.Where(s=>!s.InTransit))Marker(ship.SystemName,"ship");
foreach(var waypoint in CurrentPlayer.Navigation.Route)Marker(waypoint.Name,"navigation");
foreach(var name in galaxyJournalMarkers.MissionSystems(DateTimeOffset.UtcNow))Marker(name,"mission");
foreach(var name in galaxyJournalMarkers.CommunityGoalSystems(DateTimeOffset.UtcNow))Marker(name,"communitygoal");
if(next.Count==galaxyPersonalMarkers.Count&&next.All(x=>galaxyPersonalMarkers.TryGetValue(x.Key,out var old)&&old.SequenceEqual(x.Value)))return false;
galaxyPersonalMarkers.Clear();foreach(var entry in next)galaxyPersonalMarkers.Add(entry.Key,entry.Value);return true;
}
private async Task ReadGalaxyAccountCachesAsync(string account)
{
var root=Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"Frontier Developments","Elite Dangerous");
var snapshots=await Task.Run(()=>
{
OriginalVisitedStarsSnapshot? visited=null;var trades=new Dictionary<long,OriginalTradeDataSnapshot>();
try
{
visited=OriginalVisitedStarsCache.ReadForUser(root,account);
runtime.ResolveDiagnostic("galaxy.visited-cache:"+account,"星图账号缓存","原游戏访问缓存已恢复读取。");
var folder=Path.Combine(root,visited.CommanderId.ToString(System.Globalization.CultureInfo.InvariantCulture));
if(Directory.Exists(folder))foreach(var path in Directory.EnumerateFiles(folder,"*.trd"))
{
galaxyLifetimeToken.ThrowIfCancellationRequested();if(!long.TryParse(Path.GetFileNameWithoutExtension(path),out var address)||address<=0)continue;
var diagnostic="galaxy.trade-cache:"+account+":"+address;
try
{
trades[address]=OriginalTradeDataCache.Read(path,visited.CommanderId,address);
runtime.ResolveDiagnostic(diagnostic,"星图账号缓存","原游戏贸易缓存已恢复读取。");
}
catch(Exception e)when(e is IOException or UnauthorizedAccessException or InvalidDataException or JsonException or KeyNotFoundException or FormatException or ArgumentOutOfRangeException)
{runtime.ReportDiagnostic(diagnostic,"星图账号缓存",e,ActivitySeverity.Warning,"原游戏贸易缓存暂不可用:"+e.Message);}
}
}
catch(Exception e)when(e is IOException or UnauthorizedAccessException or InvalidDataException or ArgumentException)
{
var expected=e is FileNotFoundException or DirectoryNotFoundException or EndOfStreamException or OriginalCacheUpdatingException
||e is IOException io&&(io.HResult&0xffff) is 32 or 33;
runtime.ReportDiagnostic("galaxy.visited-cache:"+account,"星图账号缓存",e,expected?ActivitySeverity.Status:ActivitySeverity.Warning,
expected?"原游戏访问缓存尚未就绪,等待游戏生成或更新。":"原游戏访问缓存读取失败:"+e.Message);
}
return(visited,trades);
},galaxyLifetimeToken);
if(CurrentPlayer.CommanderId!=account)return;galaxyOriginalVisited=snapshots.visited;galaxyOriginalTrades=snapshots.trades;
await ReadNativeGalaxyAccountAsync(account);
}
private void SetGalaxyNativeBookmarks(IReadOnlyDictionary<long,IReadOnlyList<string>> markers,IReadOnlyDictionary<long,IReadOnlyList<GalaxyBookmarkStyle>> styles)
{
if(markers.Count==galaxyAccountMarkers.Count&&styles.Count==galaxyBookmarkStyles.Count&&markers.All(x=>galaxyAccountMarkers.TryGetValue(x.Key,out var old)&&old.SequenceEqual(x.Value))&&styles.All(x=>galaxyBookmarkStyles.TryGetValue(x.Key,out var old)&&old.SequenceEqual(x.Value)))return;
galaxyAccountMarkers=markers;galaxyBookmarkStyles=styles;galaxyNativeFactsVersion++;
}
private void SetGalaxyColonyOwnership(IReadOnlyDictionary<long,bool> colonies)
{if(galaxyColonyOwnership.Count==colonies.Count&&galaxyColonyOwnership.All(c=>colonies.TryGetValue(c.Key,out var own)&&own==c.Value))return;galaxyColonyOwnership=colonies;galaxyNativeFactsVersion++;}
private void SetGalaxyNativeSocial(IReadOnlyDictionary<long,IReadOnlyList<string>> markers)
{if(markers.Count==galaxySocialMarkers.Count&&markers.All(x=>galaxySocialMarkers.TryGetValue(x.Key,out var old)&&old.SequenceEqual(x.Value)))return;galaxySocialMarkers=markers;galaxyNativeFactsVersion++;}
private async Task ReadNativeGalaxyAccountAsync(string account)
{
if(DateTimeOffset.UtcNow-galaxyNativeAccountReadAt<TimeSpan.FromSeconds(2))return;galaxyNativeAccountReadAt=DateTimeOffset.UtcNow;
try
{
if(runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyAccountBookmarks:true} driver)
{
var snapshot=await new NativeGalaxyAccountCatalog(driver.ProcessId).ReadAsync(galaxyLifetimeToken);
if(CurrentPlayer.CommanderId!=account||runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver current||current.ProcessId!=driver.ProcessId)return;
// Private custom bookmark names and UIDs remain inside the native reply;
// only actual system addresses and marker kinds enter the map cache.
var markers=snapshot.Available?snapshot.Bookmarks.GroupBy(b=>b.SystemAddress).ToDictionary(g=>g.Key,g=>(IReadOnlyList<string>)g.Select(b=>b.Squadron?"squadronbookmark":"bookmark").Distinct().Order(StringComparer.Ordinal).ToArray()):new Dictionary<long,IReadOnlyList<string>>();
var styles=snapshot.Available?snapshot.Bookmarks.GroupBy(b=>b.SystemAddress).ToDictionary(g=>g.Key,g=>(IReadOnlyList<GalaxyBookmarkStyle>)g.Select(b=>new GalaxyBookmarkStyle(b.Icon,b.Squadron)).Distinct().OrderBy(s=>s.Squadron).ThenBy(s=>s.Icon).ToArray()):new Dictionary<long,IReadOnlyList<GalaxyBookmarkStyle>>();
SetGalaxyNativeBookmarks(markers,styles);
galaxyAccountMarkerStatus=snapshot.Available?$"原游戏书签管理器 · {snapshot.Bookmarks.Count:N0} 条实际记录;完整加载状态尚未确认。":"原游戏书签服务尚未就绪;缺少记录保持未知。";
}
else{SetGalaxyNativeBookmarks(new Dictionary<long,IReadOnlyList<string>>(),new Dictionary<long,IReadOnlyList<GalaxyBookmarkStyle>>());galaxyAccountMarkerStatus="原游戏书签接口尚未连接;缺少记录保持未知。";}
}
catch(OperationCanceledException)when(!lifetime.IsCancellationRequested)
{galaxyAccountMarkerStatus="原游戏书签读取超时;完整加载状态仍未知。";}
catch(Exception error)when(error is IOException or TimeoutException or InvalidDataException or InvalidOperationException or JsonException or KeyNotFoundException or FormatException)
{galaxyAccountMarkerStatus="原游戏书签读取尚未确认:"+error.Message;}
try
{
if(runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyAccountColonisation:true} driver&&galaxyOriginalVisited is {} verified)
{
var snapshot=await new NativeGalaxyAccountCatalog(driver.ProcessId).ReadColonisationAsync(galaxyLifetimeToken);
if(CurrentPlayer.CommanderId!=account||runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver current||current.ProcessId!=driver.ProcessId)return;
// An actual active claim proves its owner; an absent row proves neither owner.
SetGalaxyColonyOwnership(snapshot.Available?snapshot.Claims.Where(c=>c.Active).GroupBy(c=>c.SystemAddress).Where(g=>g.Select(c=>c.CommanderId).Distinct().Count()==1).ToDictionary(g=>g.Key,g=>g.First().CommanderId==verified.CommanderId):new Dictionary<long,bool>());
galaxyOwnedColonisationStatus=snapshot.Available?$"已匹配当前账号原CommanderId · {galaxyColonyOwnership.Values.Count(v=>v):N0} 个本人活动殖民地;原注册表完整性未知。":"原游戏殖民注册表尚未就绪;归属保持未知。";
}
else{SetGalaxyColonyOwnership(new Dictionary<long,bool>());galaxyOwnedColonisationStatus=galaxyOriginalVisited is null?"未取得当前账号原CommanderId证明;殖民归属保持未知。":"原游戏殖民归属接口尚未连接;缺少记录保持未知。";}
}
catch(OperationCanceledException)when(!lifetime.IsCancellationRequested)
{galaxyOwnedColonisationStatus="原游戏殖民归属读取超时;完整加载状态仍未知。";}
catch(Exception error)when(error is IOException or TimeoutException or InvalidDataException or InvalidOperationException or JsonException or KeyNotFoundException or FormatException)
{galaxyOwnedColonisationStatus="原游戏殖民归属尚未确认:"+error.Message;}
try
{
if(CurrentPlayer.InGame&&runtime.FlightDriver is XFEEDToolBox.Core.Navigation.NativeFlightControlDriver{SupportsGalaxyAccountSocial:true} driver)
{
var session=CurrentPlayer.LoadGameAt;var snapshot=await new NativeGalaxyAccountCatalog(driver.ProcessId).ReadSocialAsync(galaxyLifetimeToken);
if(CurrentPlayer.CommanderId!=account||CurrentPlayer.LoadGameAt!=session||!CurrentPlayer.InGame||runtime.FlightDriver is not XFEEDToolBox.Core.Navigation.NativeFlightControlDriver current||current.ProcessId!=driver.ProcessId)return;
var markers=snapshot.Available?snapshot.Locations.Where(l=>l.LocationCurrent==true).GroupBy(l=>l.SystemAddress).ToDictionary(g=>g.Key,g=>(IReadOnlyList<string>)g.Select(l=>l.Kind).Distinct().Order(StringComparer.Ordinal).ToArray()):new Dictionary<long,IReadOnlyList<string>>();
SetGalaxyNativeSocial(markers);
var uncertain=snapshot.Locations.Count(l=>l.LocationCurrent is null);
galaxySocialMarkerStatus=snapshot.Available?$"原游戏社交位置 · {markers.Count:N0} 个确认实时星系"+(uncertain>0?$" · {uncertain:N0} 条时效未确认,不显示为实时位置。":";位置快照不证明全部好友/队伍覆盖。"):"原游戏社交位置尚未就绪;缺少资料保持未知。";
}
else{SetGalaxyNativeSocial(new Dictionary<long,IReadOnlyList<string>>());galaxySocialMarkerStatus="原游戏好友/队伍位置接口尚未连接;时效未确认的位置不作为实时标记。";}
}
catch(OperationCanceledException)when(!lifetime.IsCancellationRequested)
{SetGalaxyNativeSocial(new Dictionary<long,IReadOnlyList<string>>());galaxySocialMarkerStatus="原游戏社交位置读取超时;旧实时标记已清除,时效保持未知。";}
catch(Exception error)when(error is IOException or TimeoutException or InvalidDataException or InvalidOperationException or JsonException or KeyNotFoundException or FormatException)
{SetGalaxyNativeSocial(new Dictionary<long,IReadOnlyList<string>>());galaxySocialMarkerStatus="原游戏社交位置尚未确认:"+error.Message;}
await ReadGalaxyPowerOptionsAsync(galaxyLifetimeToken);
await ReadGalaxyCommodityOptionsAsync(galaxyLifetimeToken);
await ReadGalaxyThargoidAsync(galaxyLifetimeToken);
await ReadGalaxyCommunityGoalsAsync(galaxyLifetimeToken);
await ReadGalaxyFleetAsync(galaxyLifetimeToken);
await ReadGalaxyMissionsAsync(galaxyLifetimeToken);
}
}