using System.Text.Json;
using System.Runtime.CompilerServices;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Galaxy;
public sealed partial class NativeGalaxyCatalog
{
private sealed record PointPage(int ProcessId,long Generation,ulong Tick,int? View,bool D50Known,bool NavKnown,
int OverrideOffset,int OverrideTotal,int NavOffset,int NavTotal,GalaxyOriginalPointOverride[] Overrides,long[] Members);
private sealed record PointContinuation(long Generation,JsonElement FirstPage);
private readonly ConditionalWeakTable<GalaxyCatalogueProfile,PointContinuation> pointContinuations=new();
private static PointPage ReadPointPage(JsonElement p)
{
if(p.ValueKind!=JsonValueKind.Object||p.GetProperty("scope").GetString()!="published-renderer-d50+local-navroute")
throw new InvalidDataException("原星点覆写来源未知。");
int Number(string name,int max)
{
if(!p.GetProperty(name).TryGetInt32(out var value)||value<0||value>max)throw new InvalidDataException("原星点覆写分页无效。");
return value;
}
long Address(JsonElement id)=>id.ValueKind==JsonValueKind.String&&long.TryParse(id.GetString(),out var value)&&value>0&&value<1L<<55
?value:throw new InvalidDataException("原星点覆写地址无效。");
int? view=null;var mode=p.GetProperty("navRouteViewMode");
if(mode.ValueKind!=JsonValueKind.Null)
{if(!mode.TryGetInt32(out var value))throw new InvalidDataException("原星点导航视图无效。");view=value;}
var process=Number("processId",int.MaxValue);var generation=p.GetProperty("sourceGeneration").GetInt64();var tick=p.GetProperty("capturedAtTick").GetUInt64();
if(process<=0||generation<=0)throw new InvalidDataException("原星点覆写代际无效。");
var d50=p.GetProperty("d50Complete").GetBoolean();var nav=p.GetProperty("navRouteComplete").GetBoolean();
if(nav&&view is null)throw new InvalidDataException("原导航成员上下文缺失。");
var rows=new List<GalaxyOriginalPointOverride>();
foreach(var row in p.GetProperty("overrides").EnumerateArray())
{
var id=Address(row.GetProperty("systemAddress"));var minimum=row.GetProperty("minimumExtra").GetSingle();
var position=row.GetProperty("position");if(position.ValueKind!=JsonValueKind.Array||position.GetArrayLength()!=3)throw new InvalidDataException("原星点覆写坐标无效。");
var point=new Coordinates(position[0].GetDouble(),position[1].GetDouble(),position[2].GetDouble());
var name=row.GetProperty("name").ValueKind==JsonValueKind.Null?null:row.GetProperty("name").GetString();
var star=row.GetProperty("starClass").GetString()??"";GalaxyOriginalStellarAppearance? appearance=null;
if(row.TryGetProperty("originalAppearance",out var a)&&a.ValueKind!=JsonValueKind.Null)
{
var type=a.GetProperty("typeRgba").GetUInt32();var raw=a.GetProperty("rawScale").GetUInt32();var classification=a.GetProperty("classification").GetUInt16();
uint? realistic=a.GetProperty("realisticRgba").ValueKind==JsonValueKind.Null?null:a.GetProperty("realisticRgba").GetUInt32();
float? blend=a.GetProperty("colourBlend").ValueKind==JsonValueKind.Null?null:a.GetProperty("colourBlend").GetSingle();
var size=a.GetProperty("size").GetSingle();var small=a.GetProperty("smallSize").GetSingle();
bool? active=a.GetProperty("smallSizeActive").ValueKind==JsonValueKind.Null?null:a.GetProperty("smallSizeActive").GetBoolean();
if(!float.IsFinite(size)||size is <0 or >4||!float.IsFinite(small)||small is <0 or >4||
blend is{} b&&(!float.IsFinite(b)||b is <0 or >1))throw new InvalidDataException("原星点覆写外观无效。");
appearance=new(type,realistic,blend,raw,classification,size,small,active);
}
if(!float.IsFinite(minimum)||minimum<0||!point.IsValid||Math.Max(Math.Abs(point.X),Math.Max(Math.Abs(point.Y),Math.Abs(point.Z)))>1000000||name is{Length:0 or >159})
throw new InvalidDataException("原星点覆写内容无效。");
rows.Add(new(id,minimum,row.GetProperty("farFade").GetBoolean(),row.GetProperty("pending").GetBoolean(),point,name,star,appearance));
if(rows.Count>64)throw new InvalidDataException("原星点覆写分页超出限制。");
}
var members=p.GetProperty("navRouteMembers").EnumerateArray().Select(Address).ToArray();
if(members.Length>512)throw new InvalidDataException("原导航成员分页超出限制。");
var offset=Number("overrideOffset",4096);var total=Number("overrideTotal",4096);
var navOffset=Number("navRouteOffset",16384);var navTotal=Number("navRouteTotal",16384);
if(offset+rows.Count>total||navOffset+members.Length>navTotal||!d50&&(total!=0||rows.Count!=0)||!nav&&(navTotal!=0||members.Length!=0))
throw new InvalidDataException("原星点覆写完整性不匹配。");
return new(process,generation,tick,view,d50,nav,offset,total,navOffset,navTotal,rows.ToArray(),members);
}
/// <summary>Strictly join pages of one immutable copied snapshot. Missing pages keep absence unknown.</summary>
public static GalaxyOriginalPointOverrides ReadPointOverridesPages(IReadOnlyList<JsonElement> pages)
{
if(pages.Count is <1 or >96)throw new InvalidDataException("原星点覆写分页数量无效。");
var first=ReadPointPage(pages[0]);List<GalaxyOriginalPointOverride> rows=[];List<long> members=[];
foreach(var json in pages)
{
var p=ReadPointPage(json);
if(p.ProcessId!=first.ProcessId||p.Generation!=first.Generation||p.Tick!=first.Tick||p.View!=first.View||
p.D50Known!=first.D50Known||p.NavKnown!=first.NavKnown||p.OverrideTotal!=first.OverrideTotal||p.NavTotal!=first.NavTotal||
p.OverrideOffset!=rows.Count||p.NavOffset!=members.Count)
throw new InvalidDataException("原星点覆写分页来源已变化。");
rows.AddRange(p.Overrides);members.AddRange(p.Members);
}
if(rows.Select(r=>r.SystemAddress).Distinct().Count()!=rows.Count||members.Distinct().Count()!=members.Count)
throw new InvalidDataException("原星点覆写分页包含重复地址。");
return new(first.ProcessId,first.Generation,first.Tick,first.View,first.D50Known&&rows.Count==first.OverrideTotal,
first.NavKnown&&members.Count==first.NavTotal,rows,members);
}
private GalaxyCatalogueProfile ReadFirstProfilePage(JsonElement result,long generation)
{
var profileJson=result.GetProperty("profile");var profile=ReadCatalogueProfile(profileJson);
if(!profileJson.TryGetProperty("pointOverrides",out var points)||points.ValueKind==JsonValueKind.Null)return profile;
var first=ReadPointPage(points);
if(first.ProcessId!=ProcessId||first.Generation!=generation)throw new InvalidDataException("原星点覆写进程或代际已变化。");
if(first.Overrides.Length<first.OverrideTotal||first.Members.Length<first.NavTotal)
pointContinuations.Add(profile,new(generation,points.Clone()));
return profile;
}
private async Task<GalaxyCatalogueProfile> ReadCompleteProfileAsync(JsonElement result,long generation,CancellationToken token)
{
var profile=ReadFirstProfilePage(result,generation);
var points=await CompletePointOverridesAsync(profile,token);
return profile with{PointOverrides=points??profile.PointOverrides};
}
/// <summary>Complete only this copied profile's auxiliary pages. A superseding profile returns null and is never cancelled by this reader.</summary>
public async Task<GalaxyOriginalPointOverrides?> CompletePointOverridesAsync(GalaxyCatalogueProfile profile,CancellationToken token=default)
{
ArgumentNullException.ThrowIfNull(profile);
if(profile.PointOverrides is{} firstSnapshot&&(firstSnapshot.ProcessId!=ProcessId||
firstSnapshot.SourceGeneration!=Volatile.Read(ref latestCatalogueProfileGeneration)))return null;
if(!pointContinuations.TryGetValue(profile,out var pending))return profile.PointOverrides;
var generation=pending.Generation;
if(Volatile.Read(ref latestCatalogueProfileGeneration)!=generation)return null;
List<JsonElement> pages=[pending.FirstPage];var first=ReadPointPage(pending.FirstPage);
// Independent IPC serialization: an optional slow page must not hold
// the basic-profile or catalogue-cell client's per-call gate.
var auxiliaryPipe=new NavigationPipeClient(ProcessId);
using var deadline=CancellationTokenSource.CreateLinkedTokenSource(token);deadline.CancelAfter(TimeSpan.FromSeconds(8));
var offset=first.Overrides.Length;var navOffset=first.Members.Length;
while(offset<first.OverrideTotal||navOffset<first.NavTotal)
{
deadline.Token.ThrowIfCancellationRequested();
if(Volatile.Read(ref latestCatalogueProfileGeneration)!=generation)return null;
JsonElement result;
try{result=await auxiliaryPipe.CallAsync(new{op="galaxy.catalogue.profile.result",generation,overrideOffset=offset,navRouteOffset=navOffset},deadline.Token);}
catch(InvalidOperationException)when(Volatile.Read(ref latestCatalogueProfileGeneration)!=generation){return null;}
if(Volatile.Read(ref latestCatalogueProfileGeneration)!=generation)return null;
if(result.GetProperty("generation").GetInt64()!=generation||!result.GetProperty("done").GetBoolean()||
result.TryGetProperty("cancelled",out var cancelled)&&cancelled.ValueKind==JsonValueKind.True||
result.TryGetProperty("error",out var error)&&!string.IsNullOrEmpty(error.GetString()))
throw new InvalidDataException("原星点覆写快照已变化。");
var points=result.GetProperty("profile").GetProperty("pointOverrides");var next=ReadPointPage(points);
if(next.OverrideOffset!=offset||next.NavOffset!=navOffset||next.Overrides.Length+next.Members.Length==0)
throw new InvalidDataException("原星点覆写分页没有进展。");
offset+=next.Overrides.Length;navOffset+=next.Members.Length;pages.Add(points.Clone());
if(pages.Count>96)throw new InvalidDataException("原星点覆写分页超出限制。");
}
return Volatile.Read(ref latestCatalogueProfileGeneration)==generation?ReadPointOverridesPages(pages):null;
}
}
using System.Text.Json;
using System.Runtime.CompilerServices;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;
namespace XFEEDToolBox.Core.Galaxy;
public sealed partial class NativeGalaxyCatalog
{
private sealed record PointPage(int ProcessId,long Generation,ulong Tick,int? View,bool D50Known,bool NavKnown,
int OverrideOffset,int OverrideTotal,int NavOffset,int NavTotal,GalaxyOriginalPointOverride[] Overrides,long[] Members);
private sealed record PointContinuation(long Generation,JsonElement FirstPage);
private readonly ConditionalWeakTable<GalaxyCatalogueProfile,PointContinuation> pointContinuations=new();
private static PointPage ReadPointPage(JsonElement p)
{
if(p.ValueKind!=JsonValueKind.Object||p.GetProperty("scope").GetString()!="published-renderer-d50+local-navroute")
throw new InvalidDataException("原星点覆写来源未知。");
int Number(string name,int max)
{
if(!p.GetProperty(name).TryGetInt32(out var value)||value<0||value>max)throw new InvalidDataException("原星点覆写分页无效。");
return value;
}
long Address(JsonElement id)=>id.ValueKind==JsonValueKind.String&&long.TryParse(id.GetString(),out var value)&&value>0&&value<1L<<55
?value:throw new InvalidDataException("原星点覆写地址无效。");
int? view=null;var mode=p.GetProperty("navRouteViewMode");
if(mode.ValueKind!=JsonValueKind.Null)
{if(!mode.TryGetInt32(out var value))throw new InvalidDataException("原星点导航视图无效。");view=value;}
var process=Number("processId",int.MaxValue);var generation=p.GetProperty("sourceGeneration").GetInt64();var tick=p.GetProperty("capturedAtTick").GetUInt64();
if(process<=0||generation<=0)throw new InvalidDataException("原星点覆写代际无效。");
var d50=p.GetProperty("d50Complete").GetBoolean();var nav=p.GetProperty("navRouteComplete").GetBoolean();
if(nav&&view is null)throw new InvalidDataException("原导航成员上下文缺失。");
var rows=new List<GalaxyOriginalPointOverride>();
foreach(var row in p.GetProperty("overrides").EnumerateArray())
{
var id=Address(row.GetProperty("systemAddress"));var minimum=row.GetProperty("minimumExtra").GetSingle();
var position=row.GetProperty("position");if(position.ValueKind!=JsonValueKind.Array||position.GetArrayLength()!=3)throw new InvalidDataException("原星点覆写坐标无效。");
var point=new Coordinates(position[0].GetDouble(),position[1].GetDouble(),position[2].GetDouble());
var name=row.GetProperty("name").ValueKind==JsonValueKind.Null?null:row.GetProperty("name").GetString();
var star=row.GetProperty("starClass").GetString()??"";GalaxyOriginalStellarAppearance? appearance=null;
if(row.TryGetProperty("originalAppearance",out var a)&&a.ValueKind!=JsonValueKind.Null)
{
var type=a.GetProperty("typeRgba").GetUInt32();var raw=a.GetProperty("rawScale").GetUInt32();var classification=a.GetProperty("classification").GetUInt16();
uint? realistic=a.GetProperty("realisticRgba").ValueKind==JsonValueKind.Null?null:a.GetProperty("realisticRgba").GetUInt32();
float? blend=a.GetProperty("colourBlend").ValueKind==JsonValueKind.Null?null:a.GetProperty("colourBlend").GetSingle();
var size=a.GetProperty("size").GetSingle();var small=a.GetProperty("smallSize").GetSingle();
bool? active=a.GetProperty("smallSizeActive").ValueKind==JsonValueKind.Null?null:a.GetProperty("smallSizeActive").GetBoolean();
if(!float.IsFinite(size)||size is <0 or >4||!float.IsFinite(small)||small is <0 or >4||
blend is{} b&&(!float.IsFinite(b)||b is <0 or >1))throw new InvalidDataException("原星点覆写外观无效。");
appearance=new(type,realistic,blend,raw,classification,size,small,active);
}
if(!float.IsFinite(minimum)||minimum<0||!point.IsValid||Math.Max(Math.Abs(point.X),Math.Max(Math.Abs(point.Y),Math.Abs(point.Z)))>1000000||name is{Length:0 or >159})
throw new InvalidDataException("原星点覆写内容无效。");
rows.Add(new(id,minimum,row.GetProperty("farFade").GetBoolean(),row.GetProperty("pending").GetBoolean(),point,name,star,appearance));
if(rows.Count>64)throw new InvalidDataException("原星点覆写分页超出限制。");
}
var members=p.GetProperty("navRouteMembers").EnumerateArray().Select(Address).ToArray();
if(members.Length>512)throw new InvalidDataException("原导航成员分页超出限制。");
var offset=Number("overrideOffset",4096);var total=Number("overrideTotal",4096);
var navOffset=Number("navRouteOffset",16384);var navTotal=Number("navRouteTotal",16384);
if(offset+rows.Count>total||navOffset+members.Length>navTotal||!d50&&(total!=0||rows.Count!=0)||!nav&&(navTotal!=0||members.Length!=0))
throw new InvalidDataException("原星点覆写完整性不匹配。");
return new(process,generation,tick,view,d50,nav,offset,total,navOffset,navTotal,rows.ToArray(),members);
}
/// <summary>Strictly join pages of one immutable copied snapshot. Missing pages keep absence unknown.</summary>
public static GalaxyOriginalPointOverrides ReadPointOverridesPages(IReadOnlyList<JsonElement> pages)
{
if(pages.Count is <1 or >96)throw new InvalidDataException("原星点覆写分页数量无效。");
var first=ReadPointPage(pages[0]);List<GalaxyOriginalPointOverride> rows=[];List<long> members=[];
foreach(var json in pages)
{
var p=ReadPointPage(json);
if(p.ProcessId!=first.ProcessId||p.Generation!=first.Generation||p.Tick!=first.Tick||p.View!=first.View||
p.D50Known!=first.D50Known||p.NavKnown!=first.NavKnown||p.OverrideTotal!=first.OverrideTotal||p.NavTotal!=first.NavTotal||
p.OverrideOffset!=rows.Count||p.NavOffset!=members.Count)
throw new InvalidDataException("原星点覆写分页来源已变化。");
rows.AddRange(p.Overrides);members.AddRange(p.Members);
}
if(rows.Select(r=>r.SystemAddress).Distinct().Count()!=rows.Count||members.Distinct().Count()!=members.Count)
throw new InvalidDataException("原星点覆写分页包含重复地址。");
return new(first.ProcessId,first.Generation,first.Tick,first.View,first.D50Known&&rows.Count==first.OverrideTotal,
first.NavKnown&&members.Count==first.NavTotal,rows,members);
}
private GalaxyCatalogueProfile ReadFirstProfilePage(JsonElement result,long generation)
{
var profileJson=result.GetProperty("profile");var profile=ReadCatalogueProfile(profileJson);
if(!profileJson.TryGetProperty("pointOverrides",out var points)||points.ValueKind==JsonValueKind.Null)return profile;
var first=ReadPointPage(points);
if(first.ProcessId!=ProcessId||first.Generation!=generation)throw new InvalidDataException("原星点覆写进程或代际已变化。");
if(first.Overrides.Length<first.OverrideTotal||first.Members.Length<first.NavTotal)
pointContinuations.Add(profile,new(generation,points.Clone()));
return profile;
}
private async Task<GalaxyCatalogueProfile> ReadCompleteProfileAsync(JsonElement result,long generation,CancellationToken token)
{
var profile=ReadFirstProfilePage(result,generation);
var points=await CompletePointOverridesAsync(profile,token);
return profile with{PointOverrides=points??profile.PointOverrides};
}
/// <summary>Complete only this copied profile's auxiliary pages. A superseding profile returns null and is never cancelled by this reader.</summary>
public async Task<GalaxyOriginalPointOverrides?> CompletePointOverridesAsync(GalaxyCatalogueProfile profile,CancellationToken token=default)
{
ArgumentNullException.ThrowIfNull(profile);
if(profile.PointOverrides is{} firstSnapshot&&(firstSnapshot.ProcessId!=ProcessId||
firstSnapshot.SourceGeneration!=Volatile.Read(ref latestCatalogueProfileGeneration)))return null;
if(!pointContinuations.TryGetValue(profile,out var pending))return profile.PointOverrides;
var generation=pending.Generation;
if(Volatile.Read(ref latestCatalogueProfileGeneration)!=generation)return null;
List<JsonElement> pages=[pending.FirstPage];var first=ReadPointPage(pending.FirstPage);
// Independent IPC serialization: an optional slow page must not hold
// the basic-profile or catalogue-cell client's per-call gate.
var auxiliaryPipe=new NavigationPipeClient(ProcessId);
using var deadline=CancellationTokenSource.CreateLinkedTokenSource(token);deadline.CancelAfter(TimeSpan.FromSeconds(8));
var offset=first.Overrides.Length;var navOffset=first.Members.Length;
while(offset<first.OverrideTotal||navOffset<first.NavTotal)
{
deadline.Token.ThrowIfCancellationRequested();
if(Volatile.Read(ref latestCatalogueProfileGeneration)!=generation)return null;
JsonElement result;
try{result=await auxiliaryPipe.CallAsync(new{op="galaxy.catalogue.profile.result",generation,overrideOffset=offset,navRouteOffset=navOffset},deadline.Token);}
catch(InvalidOperationException)when(Volatile.Read(ref latestCatalogueProfileGeneration)!=generation){return null;}
if(Volatile.Read(ref latestCatalogueProfileGeneration)!=generation)return null;
if(result.GetProperty("generation").GetInt64()!=generation||!result.GetProperty("done").GetBoolean()||
result.TryGetProperty("cancelled",out var cancelled)&&cancelled.ValueKind==JsonValueKind.True||
result.TryGetProperty("error",out var error)&&!string.IsNullOrEmpty(error.GetString()))
throw new InvalidDataException("原星点覆写快照已变化。");
var points=result.GetProperty("profile").GetProperty("pointOverrides");var next=ReadPointPage(points);
if(next.OverrideOffset!=offset||next.NavOffset!=navOffset||next.Overrides.Length+next.Members.Length==0)
throw new InvalidDataException("原星点覆写分页没有进展。");
offset+=next.Overrides.Length;navOffset+=next.Members.Length;pages.Add(points.Clone());
if(pages.Count>96)throw new InvalidDataException("原星点覆写分页超出限制。");
}
return Volatile.Read(ref latestCatalogueProfileGeneration)==generation?ReadPointOverridesPages(pages):null;
}
}