XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Globalization;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;

namespace XFEEDToolBox.Core.Galaxy;

public sealed record SystemMapCatalogue(GalacticSystem System,CurrentSystemSnapshot Preview,IReadOnlyDictionary<string,Station> Stations)
{
    public const string PreviewDescription="公开目录示意 · 非实时位置 · 抵达后核对游戏坐标";
}

/// <summary>One selected system only. Public catalogue positions are presentation data and never navigation coordinates.</summary>
public sealed class SystemMapCatalogueClient(HttpClient http)
{
    public async Task<SystemMapCatalogue> ReadAsync(GalacticSystem system,CancellationToken token=default)
    {
        var query=Uri.EscapeDataString(system.Name);
        var stationsTask=ReadJsonAsync("stations",query,token);
        var bodiesTask=ReadJsonAsync("bodies",query,token);
        await Task.WhenAll(stationsTask,bodiesTask);
        using var stations=await stationsTask;using var bodies=await bodiesTask;
        return Parse(system,bodies.RootElement,stations.RootElement);
    }
    private async Task<JsonDocument> ReadJsonAsync(string kind,string query,CancellationToken token)
    {
        // EDSM System API: https://www.edsm.net/en/api-system-v1 . No region or database dump is requested.
        using var response=await http.GetAsync($"https://www.edsm.net/api-system-v1/{kind}?systemName={query}",HttpCompletionOption.ResponseHeadersRead,token);
        response.EnsureSuccessStatusCode();
        await using var stream=await response.Content.ReadAsStreamAsync(token);using var buffer=new MemoryStream();
        var block=new byte[16384];int count;
        while((count=await stream.ReadAsync(block,token))!=0)
        {if(buffer.Length+count>16*1024*1024)throw new InvalidDataException("本地星系目录响应超出大小限制。");await buffer.WriteAsync(block.AsMemory(0,count),token);}
        return JsonDocument.Parse(buffer.ToArray());
    }
    public static SystemMapCatalogue Parse(GalacticSystem system,JsonElement bodies,JsonElement stations)
    {
        string Text(JsonElement row,string key)=>row.ValueKind==JsonValueKind.Object&&row.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.String?value.GetString()??"":"";
        long Number(JsonElement row,string key)=>row.ValueKind==JsonValueKind.Object&&row.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.Number&&value.TryGetInt64(out var number)?number:0;
        double? Decimal(JsonElement row,string key)=>row.ValueKind==JsonValueKind.Object&&row.TryGetProperty(key,out var value)&&value.ValueKind==JsonValueKind.Number&&value.TryGetDouble(out var number)&&double.IsFinite(number)&&number>=0?number:null;
        void RequireSystem(JsonElement root)
        {
            if(!Text(root,"name").Equals(system.Name,StringComparison.OrdinalIgnoreCase))throw new InvalidDataException("公开目录返回的星系名称不一致。");
            var address=Number(root,"id64");if(system.SystemAddress is >0&&address>0&&address!=system.SystemAddress)throw new InvalidDataException("公开目录返回的星系身份不一致。");
        }
        RequireSystem(bodies);RequireSystem(stations);
        var systemAddress=system.SystemAddress??Number(bodies,"id64");
        if(systemAddress<=0)systemAddress=Number(stations,"id64");
        if(systemAddress<=0)throw new InvalidDataException("本地星系目录缺少星系身份。");
        var rows=new List<SystemLocation>();var bodyPositions=new Dictionary<string,Vector3d>(StringComparer.OrdinalIgnoreCase);
        // A stable catalogue diagram deliberately supplies no orbit or native world identity.
        // Unknown orbital planes/epochs must never be labelled as a real-time system map.
        var index=0;
        if(bodies.TryGetProperty("bodies",out var bodyArray)&&bodyArray.ValueKind==JsonValueKind.Array)
        foreach(var body in bodyArray.EnumerateArray())
        {
            var name=Text(body,"name");if(name.Length==0)continue;
            var star=Text(body,"type")=="Star";var landable=body.TryGetProperty("isLandable",out var land)&&land.ValueKind==JsonValueKind.True;
            var distance=Decimal(body,"distanceToArrival");var phase=index*2.399963229728653;
            var radius=index==0&&star?0:1+Math.Log10(1+(distance??index*20));
            var position=new Vector3d(Math.Cos(phase)*radius,Math.Sin(phase*.7)*radius*.25,Math.Sin(phase)*radius)*1e9;
            var identity=$"catalogue-body:{systemAddress}:{Number(body,"id")}";
            rows.Add(new(index++,0,name,Text(body,"subType"),star?1:3,star?SystemLocationCategory.Star:landable?SystemLocationCategory.LandablePlanet:SystemLocationCategory.Planet,
                null,position,distance*299792458,false,identity));bodyPositions.TryAdd(name,position);
        }
        var stationMap=new Dictionary<string,Station>(StringComparer.Ordinal);
        if(stations.TryGetProperty("stations",out var stationArray)&&stationArray.ValueKind==JsonValueKind.Array)
        foreach(var row in stationArray.EnumerateArray())
        {
            var name=Text(row,"name");var market=Number(row,"marketId");if(name.Length==0||market<=0)continue;
            var type=Text(row,"type");var compact=string.Concat(type.Where(char.IsLetterOrDigit)).ToLowerInvariant();
            var planetary=compact.Contains("planetary")||compact.Contains("settlement");var carrier=compact.Contains("carrier");
            var pads=row.TryGetProperty("maxLandingPad",out var pad)&&pad.ValueKind==JsonValueKind.String?pad.GetString():null;
            var maxPad=pads?.ToLowerInvariant() switch{"large"=>LandingPad.Large,"medium"=>LandingPad.Medium,"small"=>LandingPad.Small,_=>LandingPad.Unknown};
            // These original station forms have fixed large docks; unknown forms stay unknown.
            if(maxPad==LandingPad.Unknown&&compact is "coriolisstarport" or "orbisstarport" or "ocellusstarport" or "asteroidbase" or "fleetcarrier")maxPad=LandingPad.Large;
            if(maxPad==LandingPad.Unknown&&compact=="outpost")maxPad=LandingPad.Medium;
            var at=DateTimeOffset.MinValue;
            if(row.TryGetProperty("updateTime",out var update))DateTimeOffset.TryParse(Text(update,"information"),CultureInfo.InvariantCulture,DateTimeStyles.AssumeUniversal,out at);
            var station=new Station{MarketId=market,Name=name,SystemName=system.Name,Position=system.Position,Type=type,IsPlanetary=planetary,IsFleetCarrier=carrier,
                DistanceToArrivalLs=Decimal(row,"distanceToArrival"),MaxLandingPad=maxPad,UpdatedAt=at};
            var identity=$"catalogue-station:{systemAddress}:{market}";
            if(stationMap.ContainsKey(identity))throw new InvalidDataException("本地星系目录包含重复市场身份。");
            stationMap.Add(identity,station);
            var phase=index*2.399963229728653;var radius=1+Math.Log10(1+(station.DistanceToArrivalLs??20));
            var position=new Vector3d(Math.Cos(phase)*radius,0,Math.Sin(phase)*radius)*1e9;
            if(row.TryGetProperty("body",out var parent)&&bodyPositions.TryGetValue(Text(parent,"name"),out var parentPoint))
                position=parentPoint+new Vector3d(Math.Cos(phase),.2,Math.Sin(phase))*2e8;
            rows.Add(new(index++,0,name,type,0,carrier?SystemLocationCategory.Carrier:planetary?SystemLocationCategory.Settlement:SystemLocationCategory.Station,
                market,position,station.DistanceToArrivalLs*299792458,false,identity));
        }
        return new(system,new(systemAddress,system.Name,0,0,DateTimeOffset.UtcNow,rows){DisplayOnly=true},stationMap);
    }
}