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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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 };
    }

}