using System.Text.Json; using System.Text.RegularExpressions; using Jint; using XFEEDToolBox.Core.Engineering; using XFEEDToolBox.Core.Models; namespace XFEEDToolBox.Core.Outfitting; /// EDSY's pinned headless codec, executed offline without a browser or CLR access. 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 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(); }