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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text.Json;
using System.Text.RegularExpressions;
using Jint;
using XFEEDToolBox.Core.Engineering;
using XFEEDToolBox.Core.Models;

namespace XFEEDToolBox.Core.Outfitting;

/// <summary>EDSY's pinned headless codec, executed offline without a browser or CLR access.</summary>
public static partial class EdsyLinkCodec
{
    public const string ReferenceCommit = "1fafc5d9f2d07e2e92443657c83d0c65b897bb00";
    private static readonly object Gate = new();
    private static Engine? engine;
    private static Engine Engine => engine ??= CreateEngine();
    private static Engine CreateEngine()
    {
        var result = new Engine(o => o.LimitMemory(256_000_000).TimeoutInterval(TimeSpan.FromSeconds(20)).MaxStatements(20_000_000));
        result.Execute("var window={location:{protocol:'https:',host:'edsy.org',pathname:'/'}};var document={title:'XFEEDToolBox offline'};");
        foreach (var name in new[] { "eddb.js", "edsy.js", "bridge.js" })
        {
            using var stream = typeof(EdsyLinkCodec).Assembly.GetManifestResourceStream("XFEEDToolBox.Core.Outfitting.Edsy." + name)!;
            using var reader = new StreamReader(stream);
            result.Execute(reader.ReadToEnd());
        }
        return result;
    }

    public static string ExtractHash(string value)
    {
        value = value.Trim();
        if (value.Length > 48_000) throw new FormatException("EDSY 链接过长。");
        if (Uri.TryCreate(value, UriKind.Absolute, out var url))
        {
            ValidateHost(url);
            value = url.Fragment;
        }
        if (value.StartsWith('#')) value = value[1..];
        if (value.StartsWith("/L=", StringComparison.OrdinalIgnoreCase)) value = value[3..];
        else if (value.StartsWith("L=", StringComparison.OrdinalIgnoreCase)) value = value[2..];
        value = Uri.UnescapeDataString(value).Split('&')[0];
        if (!HashPattern().IsMatch(value)) throw new FormatException("请输入 EDSY 装配分享链接(#/L=…)或改船码。");
        const string alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_-";
        var version = alphabet.IndexOf(value[0]);
        if (version < 0 || version > 20) throw new FormatException("此 EDSY 编码版本尚未支持,请在 EDSY 中另存为当前版本链接。");
        var groups=value.Split(',');
        if(groups.Length<(version>=10?6:version>=9?5:4)||groups.Length>8||groups[0].Length<7)
            throw new FormatException("EDSY 改船码不完整,请复制完整链接。");
        return value;
    }
    private static void ValidateHost(Uri url)
    {
        if (url.Scheme is not ("http" or "https") || url.Host.ToLowerInvariant() is not ("edsy.org" or "www.edsy.org") || !url.IsDefaultPort || url.UserInfo.Length > 0)
            throw new FormatException("仅支持 edsy.org 的装配链接。");
    }
    public static async Task<ShipRefitDocument> ImportAsync(string value, CancellationToken token = default)
    {
        if (Uri.TryCreate(value.Trim(), UriKind.Absolute, out var url))
        {
            ValidateHost(url);
            if (url.AbsolutePath.StartsWith("/s/", StringComparison.Ordinal) || url.AbsolutePath.StartsWith("/dev/s/", StringComparison.Ordinal))
            {
                using var handler = new HttpClientHandler { AllowAutoRedirect = false };
                using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) };
                using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, token);
                var location = response.Headers.Location;
                if ((int)response.StatusCode is < 300 or >= 400 || location is null) throw new FormatException("EDSY 短链接无法展开,请使用原站的完整装配链接。");
                value = (location.IsAbsoluteUri ? location : new Uri(url, location)).AbsoluteUri;
            }
        }
        return await Task.Run(() => Import(value), token);
    }
    public static ShipRefitDocument Import(string value)
    {
        var hash = ExtractHash(value);
        using var json = Invoke("xfeImport", hash);
        var root = json.RootElement;
        ThrowErrors(root);
        var catalog = OutfittingCatalog.Default;
        var slots = root.GetProperty("slots").EnumerateArray().Select(s =>
        {
            var name = s.GetProperty("slot").GetString()!;
            var moduleJson = s.GetProperty("module");
            FittedModule? module = moduleJson.ValueKind == JsonValueKind.Null ? null : ReadModule(moduleJson);
            return new RefitSlot { Slot = name, Category = s.GetProperty("category").GetString()!, Capacity = s.GetProperty("capacity").GetInt32(),
                MiningOnly = s.GetProperty("miningOnly").GetBoolean(), Module = module, Original = module,
                Origin = "EDSY 导入", Family = module is null ? ShipRefit.CoreFamily(name) : EngineeringCatalog.ModuleType(module.Item) is { Length: >0 } family ? family : catalog.Find(module.Item)?.Name ?? "" };
        }).ToArray();
        return new(-1, root.GetProperty("ship").GetString()!, root.GetProperty("name").GetString()!, null, slots)
        { EdsyHash = root.GetProperty("hash").GetString()!, Ident = root.GetProperty("ident").GetString()! };
    }
    public static string Export(ShipRefitDocument document)
    {
        if(new[]{"Armour","PowerPlant","MainEngines","FrameShiftDrive","LifeSupport","PowerDistributor","Radar","FuelTank"}.Any(name=>document.Slots.FirstOrDefault(s=>s.Slot==name)?.Module is null))
            throw new FormatException("EDSY 链接需要完整的核心模块配置,请先安装缺少的核心模块;不会用原厂模块替代空槽。");
        var loadout = new { Ship = document.ShipType, ShipName = document.Name, ShipIdent = document.Ident,
            Modules = document.Slots.Select(s => JournalModule(s)).ToArray() };
        var changed = document.Slots.Where(s => s.Module != s.Original || s.Target is not null).Select(s => s.Slot).ToArray();
        using var result = Invoke("xfeExport", JsonSerializer.Serialize(new { loadout, baselineHash = document.EdsyHash, changed }));
        ThrowErrors(result.RootElement);
        return "https://edsy.org/#/L=" + result.RootElement.GetProperty("hash").GetString();
    }
    private static object JournalModule(RefitSlot slot)
    {
        var module = slot.Module;
        var engineering = module?.Engineering;
        var type = EngineeringCatalog.ModuleType(module?.Item ?? "");
        var recipes = EngineeringCatalog.Default.Recipes;
        var blueprint = slot.Target?.Blueprint ?? engineering?.Blueprint ?? "";
        var recipe = recipes.FirstOrDefault(r => r.Type == type && r.Grade > 0 && (EngineeringCatalog.Key(r.Name) == EngineeringCatalog.Key(blueprint) || EngineeringCatalog.Key(r.JournalName) == EngineeringCatalog.Key(blueprint)));
        var effect=slot.Target is {} t?t.Experimental:EngineeringExperimental.Current(engineering);
        var effectRecipe = recipes.FirstOrDefault(r => r.Type == type && r.Grade == 0 && (EngineeringCatalog.Key(r.Name) == EngineeringCatalog.Key(effect) || EngineeringCatalog.Key(r.JournalName) == EngineeringCatalog.Key(effect)));
        return new { Slot = slot.Slot, Item = module?.Item ?? "", On = module?.Enabled ?? true, Priority = module?.Priority ?? 0,
            Engineering = engineering is null && slot.Target is null ? null : new {
                BlueprintName = recipe?.JournalName is { Length: >0 } journal ? journal : blueprint,
                Level = slot.Target?.Grade ?? engineering?.Level ?? 0, Quality = slot.Target?.Quality ?? engineering?.Quality ?? 0,
                ExperimentalEffect = effectRecipe?.JournalName is { Length: >0 } journalEffect ? journalEffect : effect,
                Modifiers = slot.Target is null ? (engineering?.Modifiers ?? []).Where(m => m.Value is not null && m.ValueText.Length == 0).Select(m => new {m.Label,m.Value,m.OriginalValue}).ToArray() : [] } };
    }
    private static FittedModule ReadModule(JsonElement m)
    {
        ModuleEngineering? engineering = null;
        if (m.TryGetProperty("Engineering", out var e))
        {
            engineering = new() { Blueprint = Text(e,"BlueprintName"), Level = e.TryGetProperty("Level",out var level)?level.GetInt32():0,
                Quality = e.TryGetProperty("Quality",out var q)?q.GetDouble():null, ExperimentalEffectId = Text(e,"ExperimentalEffect"),
                Modifiers = e.TryGetProperty("Modifiers",out var mods)?mods.EnumerateArray().Select(x => new EngineeringModifier(Text(x,"Label"),x.GetProperty("Value").GetDouble(),x.GetProperty("OriginalValue").GetDouble(),false)).ToArray():[] };
        }
        return new() {Slot=Text(m,"Slot"),Item=Text(m,"Item"),Name=OutfittingCatalog.Default.Find(Text(m,"Item"))?.Name??Text(m,"Item"),Enabled=m.GetProperty("On").GetBoolean(),Priority=m.GetProperty("Priority").GetInt32(),Engineering=engineering,Value=m.TryGetProperty("Value",out var v)?v.GetInt64():null};
    }
    private static string Text(JsonElement e,string name) => e.TryGetProperty(name,out var value)?value.GetString()??"":"";
    private static JsonDocument Invoke(string name,string input)
    {
        lock(Gate)
        {
            try { return JsonDocument.Parse(Engine.Invoke(name,input).AsString()); }
            catch(Exception e) when(e is not FormatException) { engine=null; throw new FormatException("EDSY 装配编码失败:"+e.Message,e); }
        }
    }
    private static void ThrowErrors(JsonElement root)
    {
        var errors=root.GetProperty("errors").EnumerateArray().Select(e=>e.GetString()).ToArray();
        if(errors.Length>0) throw new FormatException("EDSY 无法完整转换此配置:"+string.Join(";",errors));
    }
    [GeneratedRegex("^[0-9A-Za-z_-]{4,}(?:,[0-9A-Za-z_-]*)+$",RegexOptions.CultureInvariant)]
    private static partial Regex HashPattern();
}