using System.Text.Json; using XFEEDToolBox.Core.Models; namespace XFEEDToolBox.Core.Galaxy; public sealed partial class NativeGalaxyCatalog { private readonly SemaphoreSlim profileQueries=new(1,1); private long latestCatalogueProfileGeneration; /// The native owner reads view mode and chooses/clamps eye or focus before sampling its actual GDM. public Task CatalogueProfileAsync(Coordinates eye,Coordinates focus,CancellationToken token=default) =>QueryCatalogueProfileAsync(eye,focus,true,token); /// Return the basic LOD immediately; incomplete auxiliary tables retain unknown absence until independently completed. public Task CatalogueProfileFirstPageAsync(Coordinates eye,Coordinates focus,CancellationToken token=default) =>QueryCatalogueProfileAsync(eye,focus,false,token); private async Task QueryCatalogueProfileAsync(Coordinates eye,Coordinates focus,bool completePointPages,CancellationToken token) { if(protocolInspected&&!SupportsCatalogueProfile)throw new NotSupportedException("当前游戏插件尚不支持原目录输入快照。"); static bool Valid(Coordinates p)=>p is{IsValid:true}&&Math.Max(Math.Abs(p.X),Math.Max(Math.Abs(p.Y),Math.Abs(p.Z)))<=200000; if(!Valid(eye)||!Valid(focus))throw new ArgumentException("银河目录输入位置无效。"); // This independent managed/native queue must not wait for an old cell // job: the next profile is needed to decide whether that job is useful. await profileQueries.WaitAsync(token);long generation=0; using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(8)); try { token.ThrowIfCancellationRequested(); // Capture acceptance independently of caller cancellation, then retire // its exact generation. Profile cancellation never cancels a cell job. var result=await pipe.CallAsync(new{op="galaxy.catalogue.profile",eye=new[]{eye.X,eye.Y,eye.Z},focus=new[]{focus.X,focus.Y,focus.Z}},deadline.Token); generation=result.GetProperty("generation").GetInt64(); if(generation>0)Volatile.Write(ref latestCatalogueProfileGeneration,generation); while(true) { token.ThrowIfCancellationRequested(); if(generation<=0||result.GetProperty("generation").GetInt64()!=generation)throw new InvalidDataException("游戏目录输入快照回执已变化。"); var error=result.TryGetProperty("error",out var e)?e.GetString():""; if(!string.IsNullOrEmpty(error))throw new IOException(error); if(result.TryGetProperty("cancelled",out var cancelled)&&cancelled.ValueKind==JsonValueKind.True)throw new OperationCanceledException("游戏目录输入快照已取消。",token); if(result.GetProperty("done").GetBoolean()) { if(!completePointPages)return ReadFirstProfilePage(result,generation); using var complete=CancellationTokenSource.CreateLinkedTokenSource(token,deadline.Token); return await ReadCompleteProfileAsync(result,generation,complete.Token); } using var stop=CancellationTokenSource.CreateLinkedTokenSource(token,deadline.Token); await Task.Delay(20,stop.Token); result=await pipe.CallAsync(new{op="galaxy.catalogue.profile.result",generation},stop.Token); } } catch { if(generation>0) { using var cleanup=new CancellationTokenSource(TimeSpan.FromSeconds(2)); try{await pipe.CallAsync(new{op="galaxy.catalogue.profile.cancel",generation},cleanup.Token);}catch{} } if(deadline.IsCancellationRequested&&!token.IsCancellationRequested)throw new TimeoutException("游戏目录输入快照超时,请稍后重试。"); throw; } finally{profileQueries.Release();} } public static GalaxyCatalogueProfile ReadCatalogueProfile(JsonElement result) { if(result.ValueKind!=JsonValueKind.Object)throw new InvalidDataException("游戏目录输入快照不可用。"); Coordinates? Position(string name,bool required=false) { if(!result.TryGetProperty(name,out var v)||v.ValueKind==JsonValueKind.Null) {if(required)throw new InvalidDataException("游戏目录输入缺少真实原点。");return null;} if(v.ValueKind!=JsonValueKind.Array||v.GetArrayLength()!=3)throw new InvalidDataException("游戏目录输入坐标无效。"); var p=new Coordinates(v[0].GetDouble(),v[1].GetDouble(),v[2].GetDouble()); if(!p.IsValid||Math.Max(Math.Abs(p.X),Math.Max(Math.Abs(p.Y),Math.Abs(p.Z)))>1000000)throw new InvalidDataException("游戏目录输入坐标无效。"); return p; } double? Number(string name) { if(!result.TryGetProperty(name,out var v)||v.ValueKind==JsonValueKind.Null)return null; if(v.ValueKind!=JsonValueKind.Number||!v.TryGetDouble(out var n)||!double.IsFinite(n)||n<0)throw new InvalidDataException("游戏目录输入数值无效。"); return n; } bool? Flag(string name) { if(!result.TryGetProperty(name,out var v)||v.ValueKind==JsonValueKind.Null)return null; if(v.ValueKind is not(JsonValueKind.False or JsonValueKind.True))throw new InvalidDataException("游戏目录输入开关无效。"); return v.GetBoolean(); } uint? quality=null;int? mode=null; if(result.TryGetProperty("qualityStarCount",out var n)&&n.ValueKind!=JsonValueKind.Null) {if(n.ValueKind!=JsonValueKind.Number||!n.TryGetUInt32(out var q))throw new InvalidDataException("原图形质量恒星数量无效。");quality=q;} if(result.TryGetProperty("originalViewMode",out var m)&&m.ValueKind!=JsonValueKind.Null) {if(m.ValueKind!=JsonValueKind.Number||!m.TryGetInt32(out var v)||v<0)throw new InvalidDataException("原星图视角模式无效。");mode=v;} var reference=result.TryGetProperty("referenceKind",out var r)&&r.ValueKind==JsonValueKind.String?r.GetString()??"":""; if(reference!="camera-eye")throw new InvalidDataException("原目录参考位置语义未知。"); var centre=Position("queryCentre");var density=Number("surfaceDensity"); if((centre is not null||density is not null)&&mode is null||density is not null&¢re is null) throw new InvalidDataException("原目录输入缺少所采样的视角或位置。"); var blend=Number("colourBlend");if(blend>1)throw new InvalidDataException("原恒星配色混合值无效。"); int? routeView=null;int? routeMode=null; if(result.TryGetProperty("navRouteViewMode",out var rv)&&rv.ValueKind!=JsonValueKind.Null) {if(!rv.TryGetInt32(out var value))throw new InvalidDataException("原导航视图模式无效。");routeView=value;} if(result.TryGetProperty("navRouteMembershipMode",out var rm)&&rm.ValueKind!=JsonValueKind.Null) {if(!rm.TryGetInt32(out var value)||value is <0 or >1)throw new InvalidDataException("原导航成员模式无效。");routeMode=value;} if(routeView is{} rawRoute&&routeMode is{} memberMode&&(rawRoute==1?1:0)!=memberMode||routeView.HasValue!=routeMode.HasValue) throw new InvalidDataException("原导航成员上下文不匹配。"); return new(Position("origin",true)!,centre,density,quality,Number("runtimeRadiusMultiplier"),Number("sparseExpansion"),Flag("smallMode"),mode,Flag("debugEnabled"),reference) {ColourBlend=blend,NavRouteViewMode=routeView,NavRouteMembershipMode=routeMode,DustInputs=ReadCatalogueDustInputs(result),CurrentRouteChoices=ReadCatalogueRouteChoices(result), PointOverrides=result.TryGetProperty("pointOverrides",out var points)&&points.ValueKind!=JsonValueKind.Null?ReadPointOverridesPages([points]):null}; } }