using System.Diagnostics; using System.Security.Cryptography; using System.Xml.Linq; using System.Text.Json; using XFEEDToolBox.Core.Data; using XFEEDToolBox.Core.Models; namespace XFEEDToolBox.Core.Native; public sealed record GameEnvironment { public int? ProcessId { get; init; } public string Executable { get; init; } = ""; public string ImageSha256 { get; init; } = ""; public string Build { get; init; } = ""; public string Ship { get; init; } = ""; public bool AdvancedDocking { get; init; } public bool SupercruiseAssist { get; init; } public int? Width { get; init; } public int? Height { get; init; } public string Preset { get; init; } = ""; public string BindingFile { get; init; } = ""; public string JournalDirectory { get; init; } = ""; public string ProfileMode { get; init; } = "standard"; public IReadOnlyDictionary Bindings { get; init; } = new Dictionary(); public IReadOnlyList Diagnostics { get; init; } = []; public string Summary => $"{Ship} · 高级起降 {(AdvancedDocking ? "已装配" : "未确认")} · 超巡辅助 {(SupercruiseAssist ? "已装配" : "未确认")}\n构建 {Build.Trim()} · PID {ProcessId?.ToString() ?? "未运行"}\n配置 {Preset} · 档案 {ProfileMode}\n{Width} × {Height}(仅诊断,执行不依赖分辨率)"; } public static class GameEnvironmentReader { public static async Task ReadAsync(PlayerState player, string? optionsDirectory = null, CancellationToken token = default) { var notes = new List(); var options = optionsDirectory ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Frontier Developments", "Elite Dangerous", "Options"); int? pid = null; string exe = "", hash = ""; var journal = new ToolboxSettings().JournalDirectory; var mode = "standard"; var processes = Process.GetProcessesByName("EliteDangerous64"); try { var candidates = new List<(Process Process, string Path, string? Runtime)>(); foreach (var process in processes) { try { var path = process.MainModule?.FileName ?? ""; var runtimePath = process.Modules.Cast().FirstOrDefault(m => m.ModuleName.Equals("XFEEliteDangerous.OfflineRuntime.dll", StringComparison.OrdinalIgnoreCase))?.FileName; candidates.Add((process, path, runtimePath)); } catch (Exception e) when (e is System.ComponentModel.Win32Exception or InvalidOperationException) { notes.Add($"PID {process.Id} 的路径或模块清单不可读取,未选择该进程。"); } } var normal = candidates.Where(x => x.Runtime is null).ToArray(); var selected = normal.Length == 1 ? normal[0] : candidates.Count == 1 ? candidates[0] : default; if (selected.Process != null) { pid = selected.Process.Id; exe = selected.Path; if (candidates.Count > 1) notes.Add("检测到多个实例,环境信息选择唯一未加载隔离运行库的实例;Journal 目录按接入设置读取。"); if (File.Exists(exe)) { await using var file = File.OpenRead(exe); hash = Convert.ToHexStringLower(await SHA256.HashDataAsync(file, token)); } if (selected.Runtime is { } runtimePath) { var receipt = Path.Combine(Path.GetDirectoryName(runtimePath)!, "launch.json"); if (File.Exists(receipt) && new FileInfo(receipt).Length < 16384) { using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(receipt, token)); var r = doc.RootElement; var profile = Path.GetFullPath(r.Text("profile")); var expectedRoot = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "XFEEliteDangerous", "GameProfiles")) + Path.DirectorySeparatorChar; if (r.Long("ProcessId") == pid && r.Text("imageSha256") == hash && Path.GetFullPath(r.Text("ownedNativeRuntime")) == runtimePath && profile.StartsWith(expectedRoot, StringComparison.OrdinalIgnoreCase)) { mode = "isolated-offline"; journal = Path.Combine(profile, "SavedGames", "Frontier Developments", "Elite Dangerous"); options = optionsDirectory ?? Path.Combine(profile, "LocalAppData", "Frontier Developments", "Elite Dangerous", "Options"); notes.Add("该实例使用隔离档案;仅显示检测结果,不自动改写数据接入目录。"); } } } } else notes.Add(processes.Length == 0 ? "尚未发现游戏进程。" : "检测到多个无法唯一识别的游戏进程,仅继续读取已配置的 Journal。"); } catch (Exception e) when (e is System.ComponentModel.Win32Exception or InvalidOperationException or IOException or JsonException or ArgumentException) { notes.Add("无法读取游戏进程或映像:" + e.GetType().Name); } finally { foreach (var process in processes) process.Dispose(); } int? width = null, height = null; try { var display = Path.Combine(options, "Graphics", "DisplaySettings.xml"); if (File.Exists(display)) { var root = XDocument.Load(display).Root; width = (int?)root?.Element("ScreenWidth"); height = (int?)root?.Element("ScreenHeight"); } } catch (Exception e) when (e is IOException or System.Xml.XmlException or FormatException) { notes.Add("画面配置读取失败。"); } var bindings = new Dictionary(); string preset = "", bindingFile = ""; try { var folder = Path.Combine(options, "Bindings"); if (Directory.Exists(folder)) { var start = Directory.EnumerateFiles(folder, "StartPreset*.start").OrderDescending().FirstOrDefault(); var presets = start == null ? [] : File.ReadAllLines(start).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); // 4.x separates General, Ship, SRV and On-foot presets. preset = presets.ElementAtOrDefault(1)?.Trim() ?? presets.FirstOrDefault()?.Trim() ?? ""; bindingFile = Directory.EnumerateFiles(folder, "*.binds").Where(f => Path.GetFileName(f).StartsWith(preset + ".", StringComparison.OrdinalIgnoreCase)).OrderDescending().FirstOrDefault() ?? ""; if (bindingFile.Length > 0) { var root = XDocument.Load(bindingFile).Root; foreach (var action in root?.Elements() ?? []) { var values = new[] { action.Element("Primary"), action.Element("Secondary") }.Where(x => x != null && (string?)x.Attribute("Key") != "").Select(x => $"{(string?)x!.Attribute("Device")}:{(string?)x.Attribute("Key")}"); var text = string.Join(" / ", values); if (text.Length > 0) bindings[action.Name.LocalName] = text; } } } } catch (Exception e) when (e is IOException or System.Xml.XmlException or UnauthorizedAccessException) { notes.Add("键位配置读取失败。"); } return new() { ProcessId = pid, Executable = exe, ImageSha256 = hash, Build = player.GameBuild, Ship = player.Ship, AdvancedDocking = player.AdvancedDocking, SupercruiseAssist = player.SupercruiseAssist, Width = width, Height = height, Preset = preset, BindingFile = bindingFile, JournalDirectory = journal, ProfileMode = mode, Bindings = bindings, Diagnostics = notes }; } }