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

XFEWarframe

【WinUI】基于Warframe游戏本体的公开服务器以及安装器

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;

namespace XFEWarframe.Client;

public sealed class ClientInstaller
{
    public void Recover(string game)
    {
        game = ValidateGame(game); EnsureStopped(game);
        using var operation = Lock(game);
        FileTransaction.Recover(game);
    }
    public const string BridgeFile = "XFEWarframe.Bridge.dll";
    public const string ConfigFile = "client.json";
    public const string StateFile = "installation.json";
    public const string MinimumBridgeVersion = "1.4.2";
    public static readonly string[] LegacyProxies = ["dwmapi.dll", "wtsapi32.dll", "version.dll"];
    public static string Managed(string game, string name) => SafePath.Within(game, "XFEWarframe/" + name);

    public static string FindPayload()
    {
        foreach (var basePath in new[] { AppContext.BaseDirectory, Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..")) })
        {
            var path = Path.Combine(basePath, "bridge");
            if (File.Exists(Path.Combine(path, "payload.json"))) return path;
        }
        throw new FileNotFoundException("未找到兼容组件,请使用完整发行包,或指定 --payload 目录。");
    }
    public CompatibilityReport Install(string game, string server, string payload, IProgress<string>? progress = null, CancellationToken cancel = default)
    {
        game = ValidateGame(game);
        server = ClientProtocol.NormalizeServer(server);
        EnsureStopped(game);
        using var operation = Lock(game);
        FileTransaction.Recover(game);
        progress?.Report("检查客户端版本与关键签名…");
        var report = Compatibility.Inspect(game);
        if (!report.Compatible) throw new InvalidDataException(string.Join(Environment.NewLine, report.Problems));
        progress?.Report(report.ExperimentalMultiAbilities ? $"此游戏版本可使用多技能实验绑定(需要兼容组件 {MinimumBridgeVersion} 或更新版本)。" : "此游戏版本不支持多技能实验绑定,保留常规单技能模式。");
        cancel.ThrowIfCancellationRequested();
        var metadata = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(payload, "payload.json")), ClientJson.Default.PayloadManifest) ?? throw new InvalidDataException("兼容组件清单为空。");
        if (metadata.SchemaVersion != 1) throw new InvalidDataException("兼容组件清单版本不支持。");
        RequireCurrentBridge(metadata.Version);
        var bridgePath = Path.Combine(payload, BridgeFile);
        var bridgeBytes = File.ReadAllBytes(bridgePath);
        if (!string.Equals(Convert.ToHexString(SHA256.HashData(bridgeBytes)), metadata.Sha256, StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("兼容组件 SHA-256 校验失败。");
        using (var pe = new BinaryImage(bridgeBytes)) _ = pe.ExportRva("XfeInitialize");
        var previous = ReadState(game);
        var changes = new Dictionary<string, byte[]?>();
        var legacy = previous?.LegacyFiles.ToList() ?? [];
        foreach (var file in LegacyProxies)
        {
            var path = SafePath.Within(game, file);
            if (!File.Exists(path)) continue;
            if (legacy.Any(x => x.FileName == file)) throw new IOException($"发现新出现的 {file},请先恢复旧安装,避免覆盖备份。");
            var backupName = $"backups/legacy/{Guid.NewGuid():N}/{file}";
            var bytes = File.ReadAllBytes(path);
            var hash = Convert.ToHexString(SHA256.HashData(bytes));
            changes.Add("XFEWarframe/" + backupName, bytes);
            changes.Add(file, null);
            legacy.Add(new(file, backupName, hash));
        }
        var configuration = new ClientConfiguration { ServerUrl = server, BuildLabel = report.Version, GameSha256 = report.Sha256, Addresses = report.Addresses, Prologues = report.Prologues };
        changes.Add("XFEWarframe/" + BridgeFile, bridgeBytes);
        changes.Add("XFEWarframe/" + ConfigFile, JsonSerializer.SerializeToUtf8Bytes(configuration, ClientJson.Default.ClientConfiguration));
        changes.Add("XFEWarframe/compatibility.json", JsonSerializer.SerializeToUtf8Bytes(report, ClientJson.Default.CompatibilityReport));
        var state = new InstallationState { Version = metadata.Version, GameSha256 = report.Sha256, LegacyFiles = legacy, InstalledAt = DateTimeOffset.UtcNow };
        foreach (var file in new[] { BridgeFile, ConfigFile, "compatibility.json" })
            state.Files[file] = Convert.ToHexString(SHA256.HashData(changes["XFEWarframe/" + file]!));
        changes.Add("XFEWarframe/" + StateFile, JsonSerializer.SerializeToUtf8Bytes(state, ClientJson.Default.InstallationState));
        progress?.Report("备份旧引导文件并安装兼容组件…");
        if (Compatibility.Hash(report.Executable) != report.Sha256) throw new IOException("检测期间游戏已更新,请重新安装。");
        FileTransaction.Apply(game, changes, cancel);
        progress?.Report("安装完成,游戏 EXE 与缓存文件保持原样。");
        return report;
    }
    public void Uninstall(string game, IProgress<string>? progress = null)
    {
        game = ValidateGame(game); EnsureStopped(game);
        using var operation = Lock(game);
        FileTransaction.Recover(game);
        var state = ReadState(game) ?? throw new InvalidOperationException("此目录尚未安装 XFE 引导组件。");
        var changes = new Dictionary<string, byte[]?>();
        foreach (var entry in state.LegacyFiles)
        {
            if (!LegacyProxies.Contains(entry.FileName)) throw new InvalidDataException("未知旧引导文件。");
            var backup = Managed(game, entry.BackupName);
            if (Compatibility.Hash(backup) != entry.Sha256) throw new InvalidDataException("旧引导备份损坏。");
            if (File.Exists(SafePath.Within(game, entry.FileName))) throw new IOException($"{entry.FileName} 已存在,不覆盖其他程序的文件。");
            changes.Add(entry.FileName, File.ReadAllBytes(backup));
        }
        foreach (var (file, hash) in state.Files)
        {
            if (!new[] { BridgeFile, ConfigFile, "compatibility.json" }.Contains(file)) throw new InvalidDataException("未知安装文件。");
            var path = Managed(game, file);
            if (File.Exists(path) && Compatibility.Hash(path) != hash) throw new IOException($"{file} 已被修改,请先备份或修复再卸载。");
            changes.Add("XFEWarframe/" + file, null);
        }
        changes.Add("XFEWarframe/" + StateFile, null);
        FileTransaction.Apply(game, changes);
        progress?.Report("XFE 引导已卸载,原引导已恢复;备份与诊断日志已保留。");
    }
    public static InstallationState? ReadState(string game)
    {
        var path = Managed(game, StateFile);
        if (!File.Exists(path)) return null;
        var result = JsonSerializer.Deserialize(File.ReadAllText(path), ClientJson.Default.InstallationState);
        if (result?.SchemaVersion != 1) throw new InvalidDataException("安装记录版本不支持。");
        return result;
    }
    public static ClientConfiguration VerifyInstalled(string game)
    {
        if (File.Exists(Managed(game, "transaction.json"))) throw new IOException("安装尚未完成,请先修复。");
        var state = ReadState(game) ?? throw new InvalidOperationException("请先安装兼容组件。");
        RequireCurrentBridge(state.Version);
        if (state.GameSha256 != Compatibility.Hash(Path.Combine(game, "Warframe.x64.exe")))
            throw new InvalidOperationException("游戏版本已更新,请重新检测并安装兼容组件。");
        foreach (var (file, hash) in state.Files)
            if (Compatibility.Hash(Managed(game, file)) != hash) throw new InvalidDataException("安装文件校验失败,请修复:" + file);
        if (LegacyProxies.Any(f => File.Exists(SafePath.Within(game, f)))) throw new IOException("检测到冲突的旧引导 DLL,请修复安装。");
        return JsonSerializer.Deserialize(File.ReadAllText(Managed(game, ConfigFile)), ClientJson.Default.ClientConfiguration)!;
    }
    public static string ValidateGame(string path)
    {
        path = Path.GetFullPath(path); SafePath.CheckNoLinks(path);
        if (!File.Exists(Path.Combine(path, "Warframe.x64.exe"))) throw new FileNotFoundException("目录中没有 Warframe.x64.exe。");
        return path;
    }
    private static void RequireCurrentBridge(string? version)
    {
        if (!Version.TryParse(version, out var installed) || installed < Version.Parse(MinimumBridgeVersion))
            throw new InvalidDataException($"兼容组件版本过旧({version ?? "未知"}),当前启动器需要 {MinimumBridgeVersion} 或更新版本。请在“安装与维护”中修复安装。");
    }
    public static void EnsureStopped(string game)
    {
        foreach (var process in Process.GetProcessesByName("Warframe.x64"))
        {
            using (process)
            {
                try
                {
                    if (string.Equals(Path.GetDirectoryName(process.MainModule?.FileName), game, StringComparison.OrdinalIgnoreCase))
                        throw new InvalidOperationException("请先关闭这份客户端,再安装或启动。");
                }
                catch (System.ComponentModel.Win32Exception) { throw new IOException("无法检查正在运行的客户端,请先关闭游戏。"); }
            }
        }
    }
    internal static FileStream Lock(string game)
    {
        var path = Managed(game, "operation.lock"); Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
    }
}