using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace XFEToolBox.Tools.SpaceEngineers; public sealed record LoaderInstallation(string LauncherPath, string BridgePath); public sealed class XfeLoaderBuilder { private const string ManifestName = "xfe-loader-manifest.json"; private static readonly string[] RequiredGameFiles = { "SpaceEngineers.exe", "SpaceEngineers.exe.config", "VRage.dll", "VRage.Library.dll" }; private static readonly JsonSerializerOptions ManifestJson = new(JsonSerializerDefaults.Web) { WriteIndented = true }; private sealed record BuildManifest(int FormatVersion, string GameDirectory, Dictionary GameFiles, Dictionary LoaderFiles); public async Task BuildAsync(string bin64, string outputRoot, Action? progress = null, CancellationToken cancellationToken = default) { bin64 = Path.TrimEndingDirectorySeparator(Path.GetFullPath(bin64)); outputRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(outputRoot)); string gameRoot = Directory.GetParent(bin64)?.FullName ?? bin64; if (IsWithin(outputRoot, gameRoot)) throw new InvalidOperationException("XFE 加载器必须生成在工具数据目录,不能写入游戏安装目录。"); foreach (string name in RequiredGameFiles) if (!File.Exists(Path.Combine(bin64, name))) throw new FileNotFoundException("游戏目录缺少 " + name, Path.Combine(bin64, name)); Dictionary gameHashes = HashGameFiles(bin64); string framework = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Microsoft.NET", "Framework64", "v4.0.30319"); string compiler = Path.Combine(framework, "csc.exe"); if (!File.Exists(compiler)) throw new FileNotFoundException("未找到 Windows 64 位 .NET Framework 编译器。请安装 .NET Framework 4.8。", compiler); Directory.CreateDirectory(outputRoot); string staging = Path.Combine(outputRoot, ".xfe-build-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(staging); try { cancellationToken.ThrowIfCancellationRequested(); progress?.Invoke("正在使用本机游戏程序集构建 XFE 自研加载器…"); string bootstrapSource = Path.Combine(staging, "XfeBootstrap.cs"); string bridgeSource = Path.Combine(staging, "XfePluginBridge.cs"); string launcher = Path.Combine(staging, "XFE.SpaceEngineers.exe"); string bridge = Path.Combine(staging, "XFE.SpaceEngineers.Bridge.dll"); await File.WriteAllTextAsync(bootstrapSource, XfeLoaderSources.Bootstrap, cancellationToken); await File.WriteAllTextAsync(bridgeSource, XfeLoaderSources.Bridge, cancellationToken); // Current VRage exposes IPlugin through the game's netstandard facade; older builds may not use it. string[] bridgeReferences = new[] { Path.Combine(bin64, "VRage.dll"), Path.Combine(framework, "System.Runtime.Serialization.dll"), Path.Combine(bin64, "netstandard.dll") }.Where(File.Exists).ToArray(); await CompileAsync(compiler, framework, bridgeSource, bridge, "library", bridgeReferences, cancellationToken); await CompileAsync(compiler, framework, bootstrapSource, launcher, "winexe", Array.Empty(), cancellationToken); // CLR binding redirects and runtime settings must match the locally installed game. File.Copy(Path.Combine(bin64, "SpaceEngineers.exe.config"), launcher + ".config", overwrite: false); if (!SameHashes(gameHashes, HashGameFiles(bin64))) throw new IOException("构建期间游戏文件发生变化,请等待游戏更新完成后重新构建加载器。"); var loaderHashes = new[] { launcher, bridge, launcher + ".config" }.ToDictionary(file => Path.GetFileName(file)!, HashFile, StringComparer.Ordinal); var manifest = new BuildManifest(1, bin64, gameHashes, loaderHashes); await File.WriteAllTextAsync(Path.Combine(staging, ManifestName), JsonSerializer.Serialize(manifest, ManifestJson), cancellationToken); cancellationToken.ThrowIfCancellationRequested(); string destination = Path.Combine(outputRoot, "xfe-loader-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss") + "-" + Guid.NewGuid().ToString("N")[..8]); Directory.Move(staging, destination); progress?.Invoke("XFE 加载器已构建,游戏文件未修改。"); return new LoaderInstallation(Path.Combine(destination, Path.GetFileName(launcher)), Path.Combine(destination, Path.GetFileName(bridge))); } finally { if (Directory.Exists(staging) && string.Equals(Path.GetDirectoryName(Path.GetFullPath(staging)), outputRoot, StringComparison.OrdinalIgnoreCase)) Directory.Delete(staging, recursive: true); } } public static void ValidateInstallation(string bin64, string launcherPath, string bridgePath) { const string rebuild = "XFE 加载器与当前游戏文件不匹配或构建文件已改变,请重新构建加载器。"; try { bin64 = Path.TrimEndingDirectorySeparator(Path.GetFullPath(bin64)); launcherPath = Path.GetFullPath(launcherPath); bridgePath = Path.GetFullPath(bridgePath); string directory = Path.GetDirectoryName(launcherPath)!; if (!directory.Equals(Path.GetDirectoryName(bridgePath), StringComparison.OrdinalIgnoreCase) || Path.GetFileName(launcherPath) != "XFE.SpaceEngineers.exe" || Path.GetFileName(bridgePath) != "XFE.SpaceEngineers.Bridge.dll") throw new InvalidDataException(rebuild); string manifestPath = Path.Combine(directory, ManifestName); if (!File.Exists(manifestPath) || new FileInfo(manifestPath).Length > 64 * 1024) throw new InvalidDataException(rebuild); BuildManifest? manifest = JsonSerializer.Deserialize(File.ReadAllText(manifestPath), ManifestJson); if (manifest?.FormatVersion != 1 || manifest.GameFiles == null || manifest.LoaderFiles == null || !bin64.Equals(manifest.GameDirectory, StringComparison.OrdinalIgnoreCase) || !SameHashes(manifest.GameFiles, HashGameFiles(bin64))) throw new InvalidDataException(rebuild); var files = new[] { launcherPath, bridgePath, launcherPath + ".config" }; if (manifest.LoaderFiles.Count != files.Length || files.Any(file => !File.Exists(file) || !manifest.LoaderFiles.TryGetValue(Path.GetFileName(file), out string? hash) || hash != HashFile(file))) throw new InvalidDataException(rebuild); } catch (Exception error) when (error is IOException or InvalidDataException or UnauthorizedAccessException or JsonException or ArgumentException) { throw new InvalidOperationException(rebuild, error); } } private static Dictionary HashGameFiles(string bin64) { IEnumerable names = RequiredGameFiles; if (File.Exists(Path.Combine(bin64, "netstandard.dll"))) names = names.Append("netstandard.dll"); return names.ToDictionary(name => name, name => HashFile(Path.Combine(bin64, name)), StringComparer.Ordinal); } private static string HashFile(string path) { using var stream = File.OpenRead(path); return Convert.ToHexString(SHA256.HashData(stream)); } private static bool SameHashes(Dictionary first, Dictionary second) => first.Count == second.Count && first.All(item => second.TryGetValue(item.Key, out string? value) && value == item.Value); private static bool IsWithin(string path, string root) => path.Equals(root, StringComparison.OrdinalIgnoreCase) || path.StartsWith(Path.TrimEndingDirectorySeparator(root) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase); private static async Task CompileAsync(string compiler, string framework, string source, string output, string target, string[] references, CancellationToken token) { var info = new ProcessStartInfo(compiler) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = Path.GetDirectoryName(source)! }; foreach (string argument in new[] { "/nologo", "/langversion:5", "/platform:x64", "/optimize+", "/target:" + target, "/out:" + output, "/reference:" + Path.Combine(framework, "System.dll"), "/reference:" + Path.Combine(framework, "System.Core.dll") }) info.ArgumentList.Add(argument); foreach (string reference in references) info.ArgumentList.Add("/reference:" + reference); info.ArgumentList.Add(source); using var process = Process.Start(info) ?? throw new IOException("无法启动 .NET Framework 编译器。"); Task standard = process.StandardOutput.ReadToEndAsync(token); Task errors = process.StandardError.ReadToEndAsync(token); try { await process.WaitForExitAsync(token); } catch { if (!process.HasExited) process.Kill(entireProcessTree: true); throw; } string[] outputText = await Task.WhenAll(standard, errors); if (process.ExitCode != 0 || !File.Exists(output)) throw new InvalidOperationException("XFE 加载器编译失败:" + Environment.NewLine + string.Join(Environment.NewLine, outputText)); } }