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<string, string> Bindings { get; init; } = new Dictionary<string, string>();
public IReadOnlyList<string> 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<GameEnvironment> ReadAsync(PlayerState player, string? optionsDirectory = null, CancellationToken token = default)
{
var notes = new List<string>();
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<ProcessModule>().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, string>(); 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 };
}
}
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<string, string> Bindings { get; init; } = new Dictionary<string, string>();
public IReadOnlyList<string> 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<GameEnvironment> ReadAsync(PlayerState player, string? optionsDirectory = null, CancellationToken token = default)
{
var notes = new List<string>();
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<ProcessModule>().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, string>(); 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 };
}
}