using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Galaxy;
public sealed record GalacticSystem(string Name,Coordinates Position,long? SystemAddress=null,string StarClass="",string Source="本机记录")
{
public GalaxySystemData Data { get; init; }=new();
[System.Text.Json.Serialization.JsonIgnore]
public GalaxyOriginalStellarAppearance? OriginalAppearance { get; init; }
public string DisplayName=>Localization.ChineseNames.Translate(Name) is var value&&value!="未收录名称"?value:Name;
public string OriginalName=>DisplayName==Name?"":Name;
public string StarTypeName=>StarClass.Length==0?"恒星类型未记录":StarClass=="N"||StarClass.Contains("Neutron",StringComparison.OrdinalIgnoreCase)?"中子星":StarClass=="H"||StarClass.Contains("Black Hole",StringComparison.OrdinalIgnoreCase)?"黑洞":StarClass.StartsWith('D')||StarClass.Contains("White Dwarf",StringComparison.OrdinalIgnoreCase)?"白矮星": "OBAFGKMLTY".Contains(StarClass[0])&&(StarClass.Length==1||StarClass[1]==' ')?$"{StarClass[0]} 型恒星":"特殊恒星类型";
public string Detail=>Position.IsValid?$"{Position.X:F2}, {Position.Y:F2}, {Position.Z:F2} ly · {Source}":Source;
}
public sealed class GalaxyCatalog(HttpClient http)
{
public async Task<IReadOnlyList<string>> SuggestNamesAsync(string prefix,CancellationToken token=default)
{
prefix=prefix.Trim();if(prefix.Length is <2 or >128)return [];
using var response=await http.GetAsync("https://spansh.co.uk/api/systems/field_values/system_names?q="+Uri.EscapeDataString(prefix),token);response.EnsureSuccessStatusCode();
var bytes=await response.Content.ReadAsByteArrayAsync(token);if(bytes.Length>1024*1024)throw new InvalidDataException("名称补全响应超出限制。");
using var json=JsonDocument.Parse(bytes);
return json.RootElement.GetProperty("values").EnumerateArray().Take(30).Where(v=>v.ValueKind==JsonValueKind.String).Select(v=>v.GetString()!).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
public async Task<IReadOnlyList<GalacticSystem>> RegionAsync(Coordinates centre,int size=200,CancellationToken token=default)
{
if(!centre.IsValid)throw new ArgumentException("星图区域坐标无效。");
var url=FormattableString.Invariant($"https://www.edsm.net/api-v1/cube-systems?x={centre.X:R}&y={centre.Y:R}&z={centre.Z:R}&size={Math.Clamp(size,10,200)}&showCoordinates=1&showId=1&showPrimaryStar=1&showInformation=1");
using var response=await http.GetAsync(url,HttpCompletionOption.ResponseHeadersRead,token);response.EnsureSuccessStatusCode();
await using var body=await response.Content.ReadAsStreamAsync(token);using var limit=new LimitedReadStream(body,32*1024*1024);
using var json=await JsonDocument.ParseAsync(limit,cancellationToken:token);
if(json.RootElement.ValueKind!=JsonValueKind.Array)throw new InvalidDataException("星图区域服务返回了无效数据。");
return await Task.Run(()=>Parse(json.RootElement),token);
}
public async Task<IReadOnlyList<GalacticSystem>> NearbyAsync(string system,int radius,CancellationToken token=default)
{
var url=$"https://www.edsm.net/api-v1/sphere-systems?systemName={Uri.EscapeDataString(system)}&radius={Math.Clamp(radius,5,100)}&showCoordinates=1&showId=1&showPrimaryStar=1&showInformation=1";
using var response=await http.GetAsync(url,HttpCompletionOption.ResponseHeadersRead,token);response.EnsureSuccessStatusCode();
await using var body=await response.Content.ReadAsStreamAsync(token);
using var limit=new LimitedReadStream(body,32*1024*1024);
using var json=await JsonDocument.ParseAsync(limit,cancellationToken:token);
if(json.RootElement.ValueKind!=JsonValueKind.Array)throw new InvalidDataException("星系服务未返回有效星系列表。");
return Parse(json.RootElement);
}
public async Task<GalacticSystem> FindAsync(string name,CancellationToken token=default)
{
using var response=await http.GetAsync($"https://www.edsm.net/api-v1/system?systemName={Uri.EscapeDataString(name)}&showCoordinates=1&showId=1&showPrimaryStar=1&showInformation=1",token);
response.EnsureSuccessStatusCode();var data=await response.Content.ReadAsByteArrayAsync(token);
if(data.Length>1024*1024)throw new InvalidDataException("星系响应超出限制。");
using var json=JsonDocument.Parse(data);return Read(json.RootElement)??throw new InvalidDataException("未查到带有效坐标的星系,请检查完整原名。");
}
public static IReadOnlyList<GalacticSystem> Parse(JsonElement array)=>array.EnumerateArray().Take(25001).Select(Read).OfType<GalacticSystem>().Where(s=>s.Position.IsValid).DistinctBy(s=>s.Name,StringComparer.OrdinalIgnoreCase).Take(25000).ToArray();
private static GalacticSystem? Read(JsonElement e)
{
if(e.ValueKind!=JsonValueKind.Object||!e.TryGetProperty("name",out var n)||n.ValueKind!=JsonValueKind.String||!e.TryGetProperty("coords",out var c))return null;
if(!c.TryGetProperty("x",out var x)||!x.TryGetDouble(out var xx)||!c.TryGetProperty("y",out var y)||!y.TryGetDouble(out var yy)||!c.TryGetProperty("z",out var z)||!z.TryGetDouble(out var zz))return null;
var p=new Coordinates(xx,yy,zz);if(!p.IsValid)return null;
var star=e.TryGetProperty("primaryStar",out var s)&&s.TryGetProperty("type",out var t)?t.GetString()??"":"";
var info=e.TryGetProperty("information",out var information)&&information.ValueKind==JsonValueKind.Object?information:default;
string? Text(string key)=>info.ValueKind==JsonValueKind.Object&&info.TryGetProperty(key,out var v)&&v.ValueKind==JsonValueKind.String?v.GetString():null;
return new(n.GetString()!,p,e.TryGetProperty("id64",out var id)&&id.TryGetInt64(out var address)?address:null,star,"EDSM 公共星系目录")
{Data=new(){Allegiance=Text("allegiance"),Government=Text("government"),Economy=Text("economy"),SecondaryEconomy=Text("secondEconomy"),Security=Text("security"),State=Text("factionState"),
Population=info.ValueKind==JsonValueKind.Object&&info.TryGetProperty("population",out var pop)&&pop.TryGetInt64(out var population)&&population>=0?population:null}};
}
private sealed class LimitedReadStream(Stream source,long remaining) : Stream
{
public override bool CanRead=>true;public override bool CanSeek=>false;public override bool CanWrite=>false;
public override long Length=>throw new NotSupportedException();public override long Position{get=>throw new NotSupportedException();set=>throw new NotSupportedException();}
private int Account(int count){remaining-=count;if(remaining<0)throw new InvalidDataException("星系响应超出大小限制。");return count;}
public override int Read(byte[] buffer,int offset,int count)=>Account(source.Read(buffer,offset,count));
public override async ValueTask<int> ReadAsync(Memory<byte> buffer,CancellationToken token=default)=>Account(await source.ReadAsync(buffer,token));
public override void Flush(){}public override long Seek(long o,SeekOrigin s)=>throw new NotSupportedException();public override void SetLength(long v)=>throw new NotSupportedException();public override void Write(byte[] b,int o,int c)=>throw new NotSupportedException();
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Galaxy;
public sealed record GalacticSystem(string Name,Coordinates Position,long? SystemAddress=null,string StarClass="",string Source="本机记录")
{
public GalaxySystemData Data { get; init; }=new();
[System.Text.Json.Serialization.JsonIgnore]
public GalaxyOriginalStellarAppearance? OriginalAppearance { get; init; }
public string DisplayName=>Localization.ChineseNames.Translate(Name) is var value&&value!="未收录名称"?value:Name;
public string OriginalName=>DisplayName==Name?"":Name;
public string StarTypeName=>StarClass.Length==0?"恒星类型未记录":StarClass=="N"||StarClass.Contains("Neutron",StringComparison.OrdinalIgnoreCase)?"中子星":StarClass=="H"||StarClass.Contains("Black Hole",StringComparison.OrdinalIgnoreCase)?"黑洞":StarClass.StartsWith('D')||StarClass.Contains("White Dwarf",StringComparison.OrdinalIgnoreCase)?"白矮星": "OBAFGKMLTY".Contains(StarClass[0])&&(StarClass.Length==1||StarClass[1]==' ')?$"{StarClass[0]} 型恒星":"特殊恒星类型";
public string Detail=>Position.IsValid?$"{Position.X:F2}, {Position.Y:F2}, {Position.Z:F2} ly · {Source}":Source;
}
public sealed class GalaxyCatalog(HttpClient http)
{
public async Task<IReadOnlyList<string>> SuggestNamesAsync(string prefix,CancellationToken token=default)
{
prefix=prefix.Trim();if(prefix.Length is <2 or >128)return [];
using var response=await http.GetAsync("https://spansh.co.uk/api/systems/field_values/system_names?q="+Uri.EscapeDataString(prefix),token);response.EnsureSuccessStatusCode();
var bytes=await response.Content.ReadAsByteArrayAsync(token);if(bytes.Length>1024*1024)throw new InvalidDataException("名称补全响应超出限制。");
using var json=JsonDocument.Parse(bytes);
return json.RootElement.GetProperty("values").EnumerateArray().Take(30).Where(v=>v.ValueKind==JsonValueKind.String).Select(v=>v.GetString()!).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
public async Task<IReadOnlyList<GalacticSystem>> RegionAsync(Coordinates centre,int size=200,CancellationToken token=default)
{
if(!centre.IsValid)throw new ArgumentException("星图区域坐标无效。");
var url=FormattableString.Invariant($"https://www.edsm.net/api-v1/cube-systems?x={centre.X:R}&y={centre.Y:R}&z={centre.Z:R}&size={Math.Clamp(size,10,200)}&showCoordinates=1&showId=1&showPrimaryStar=1&showInformation=1");
using var response=await http.GetAsync(url,HttpCompletionOption.ResponseHeadersRead,token);response.EnsureSuccessStatusCode();
await using var body=await response.Content.ReadAsStreamAsync(token);using var limit=new LimitedReadStream(body,32*1024*1024);
using var json=await JsonDocument.ParseAsync(limit,cancellationToken:token);
if(json.RootElement.ValueKind!=JsonValueKind.Array)throw new InvalidDataException("星图区域服务返回了无效数据。");
return await Task.Run(()=>Parse(json.RootElement),token);
}
public async Task<IReadOnlyList<GalacticSystem>> NearbyAsync(string system,int radius,CancellationToken token=default)
{
var url=$"https://www.edsm.net/api-v1/sphere-systems?systemName={Uri.EscapeDataString(system)}&radius={Math.Clamp(radius,5,100)}&showCoordinates=1&showId=1&showPrimaryStar=1&showInformation=1";
using var response=await http.GetAsync(url,HttpCompletionOption.ResponseHeadersRead,token);response.EnsureSuccessStatusCode();
await using var body=await response.Content.ReadAsStreamAsync(token);
using var limit=new LimitedReadStream(body,32*1024*1024);
using var json=await JsonDocument.ParseAsync(limit,cancellationToken:token);
if(json.RootElement.ValueKind!=JsonValueKind.Array)throw new InvalidDataException("星系服务未返回有效星系列表。");
return Parse(json.RootElement);
}
public async Task<GalacticSystem> FindAsync(string name,CancellationToken token=default)
{
using var response=await http.GetAsync($"https://www.edsm.net/api-v1/system?systemName={Uri.EscapeDataString(name)}&showCoordinates=1&showId=1&showPrimaryStar=1&showInformation=1",token);
response.EnsureSuccessStatusCode();var data=await response.Content.ReadAsByteArrayAsync(token);
if(data.Length>1024*1024)throw new InvalidDataException("星系响应超出限制。");
using var json=JsonDocument.Parse(data);return Read(json.RootElement)??throw new InvalidDataException("未查到带有效坐标的星系,请检查完整原名。");
}
public static IReadOnlyList<GalacticSystem> Parse(JsonElement array)=>array.EnumerateArray().Take(25001).Select(Read).OfType<GalacticSystem>().Where(s=>s.Position.IsValid).DistinctBy(s=>s.Name,StringComparer.OrdinalIgnoreCase).Take(25000).ToArray();
private static GalacticSystem? Read(JsonElement e)
{
if(e.ValueKind!=JsonValueKind.Object||!e.TryGetProperty("name",out var n)||n.ValueKind!=JsonValueKind.String||!e.TryGetProperty("coords",out var c))return null;
if(!c.TryGetProperty("x",out var x)||!x.TryGetDouble(out var xx)||!c.TryGetProperty("y",out var y)||!y.TryGetDouble(out var yy)||!c.TryGetProperty("z",out var z)||!z.TryGetDouble(out var zz))return null;
var p=new Coordinates(xx,yy,zz);if(!p.IsValid)return null;
var star=e.TryGetProperty("primaryStar",out var s)&&s.TryGetProperty("type",out var t)?t.GetString()??"":"";
var info=e.TryGetProperty("information",out var information)&&information.ValueKind==JsonValueKind.Object?information:default;
string? Text(string key)=>info.ValueKind==JsonValueKind.Object&&info.TryGetProperty(key,out var v)&&v.ValueKind==JsonValueKind.String?v.GetString():null;
return new(n.GetString()!,p,e.TryGetProperty("id64",out var id)&&id.TryGetInt64(out var address)?address:null,star,"EDSM 公共星系目录")
{Data=new(){Allegiance=Text("allegiance"),Government=Text("government"),Economy=Text("economy"),SecondaryEconomy=Text("secondEconomy"),Security=Text("security"),State=Text("factionState"),
Population=info.ValueKind==JsonValueKind.Object&&info.TryGetProperty("population",out var pop)&&pop.TryGetInt64(out var population)&&population>=0?population:null}};
}
private sealed class LimitedReadStream(Stream source,long remaining) : Stream
{
public override bool CanRead=>true;public override bool CanSeek=>false;public override bool CanWrite=>false;
public override long Length=>throw new NotSupportedException();public override long Position{get=>throw new NotSupportedException();set=>throw new NotSupportedException();}
private int Account(int count){remaining-=count;if(remaining<0)throw new InvalidDataException("星系响应超出大小限制。");return count;}
public override int Read(byte[] buffer,int offset,int count)=>Account(source.Read(buffer,offset,count));
public override async ValueTask<int> ReadAsync(Memory<byte> buffer,CancellationToken token=default)=>Account(await source.ReadAsync(buffer,token));
public override void Flush(){}public override long Seek(long o,SeekOrigin s)=>throw new NotSupportedException();public override void SetLength(long v)=>throw new NotSupportedException();public override void Write(byte[] b,int o,int c)=>throw new NotSupportedException();
}
}