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

XFEWarframe

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Text.Json;
using XFEExtension.NetCore.WinUIHelper.Implements.Services;
using XFEWarframe.Client;
using XFEWarframe.Player.Interface.Services;
using XFEWarframe.Player.Models;
using XFEWarframe.Player.Profiles.CrossVersionProfiles;
using XFEWarframe.Player.Utilities;

namespace XFEWarframe.Player.Implements.Services;

public sealed class PlayerService : GlobalServiceBase, IPlayerService
{
    private readonly ClientInstaller installer = new();
    private readonly HttpClient http = new(new HttpClientHandler { AllowAutoRedirect = false }) { Timeout = TimeSpan.FromSeconds(5) };
    private CancellationTokenSource? operation;
    private Task? activeOperation;
    private bool initialized;
    public ClientState State { get; } = new();

    public Task InitializeAsync()
    {
        if (initialized) return Task.CompletedTask;
        var explicitSettings = AppPath.Argument("settings");
        var legacyFile = explicitSettings ?? Path.Combine(AppPath.DataDirectory, "settings.json");
        if ((explicitSettings is not null || !SystemProfile.LegacySettingsImported) && File.Exists(legacyFile))
        {
            var old = JsonSerializer.Deserialize(File.ReadAllText(legacyFile), ClientJson.Default.PlayerSettings)
                ?? throw new InvalidDataException("旧版客户端设置无效。");
            SystemProfile.GameDirectory = old.GameDirectory;
            SystemProfile.ServerUrl = old.ServerUrl;
        }
        SystemProfile.LegacySettingsImported = true;
        State.GameDirectory = SystemProfile.GameDirectory;
        State.ServerUrl = SystemProfile.ServerUrl;
        State.Language = SystemProfile.Language;
        State.UseDirectX12 = SystemProfile.UseDirectX12;
        Detect();
        initialized = true;
        return Task.CompletedTask;
    }

    public void Detect()
    {
        State.DetectedGames.Clear();
        foreach (var game in GameDiscovery.Find()) State.DetectedGames.Add(game);
        if (string.IsNullOrWhiteSpace(State.GameDirectory))
            State.GameDirectory = State.DetectedGames.FirstOrDefault()?.Directory ?? "";
        RefreshSummary();
    }

    public void RefreshSummary()
    {
        var executable = Path.Combine(State.GameDirectory.Trim(), "Warframe.x64.exe");
        State.Version = File.Exists(executable) ? FileVersionInfo.GetVersionInfo(executable).FileVersion ?? "未知版本" : "尚未选择";
        var state = string.IsNullOrWhiteSpace(State.GameDirectory) ? null : ClientInstaller.ReadState(State.GameDirectory);
        State.IsInstalled = state is not null;
        State.Installation = state is null ? "尚未安装兼容组件" : $"兼容组件 {state.Version} · 已安装";
    }

    public void SavePreferences()
    {
        State.ServerUrl = ClientProtocol.NormalizeServer(State.ServerUrl);
        SystemProfile.GameDirectory = State.GameDirectory.Trim();
        SystemProfile.ServerUrl = State.ServerUrl;
        SystemProfile.Language = State.Language;
        SystemProfile.UseDirectX12 = State.UseDirectX12;
        SystemProfile.SaveProfile();
    }

    private string SaveAndValidate()
    {
        var game = ClientInstaller.ValidateGame(State.GameDirectory.Trim());
        State.GameDirectory = game;
        SavePreferences();
        return game;
    }

    private Task RunOperationAsync(Func<CancellationToken, IProgress<string>, Task> action)
    {
        if (State.IsBusy) throw new InvalidOperationException("已有客户端操作正在进行。");
        activeOperation = ExecuteAsync(action);
        return activeOperation;
    }

    private async Task ExecuteAsync(Func<CancellationToken, IProgress<string>, Task> action)
    {
        using var source = CancellationTokenSource.CreateLinkedTokenSource(App.Lifetime);
        operation = source;
        State.IsBusy = true;
        var progress = new Progress<string>(message =>
        {
            if (!App.Lifetime.IsCancellationRequested) State.OperationStatus = message;
        });
        try { await action(source.Token, progress); }
        catch (OperationCanceledException)
        {
            State.OperationStatus = "操作已取消;已完成必要的清理。";
            throw;
        }
        catch { State.OperationStatus = "操作未完成,请查看提示或诊断日志。"; throw; }
        finally
        {
            operation = null;
            State.IsBusy = false;
            RefreshSummary();
        }
    }

    public Task CheckAsync() => RunOperationAsync(async (cancel, progress) =>
    {
        var game = SaveAndValidate();
        progress.Report("读取客户端并检查兼容性…");
        ShowReport(await Task.Run(() => Compatibility.Inspect(game), cancel));
        State.OperationStatus = "兼容性检查完成;没有修改游戏文件。";
    });

    public Task InstallAsync() => RunOperationAsync(async (cancel, progress) =>
    {
        var game = SaveAndValidate();
        var server = State.ServerUrl;
        var payload = AppPath.Argument("payload") ?? ClientInstaller.FindPayload();
        ShowReport(await Task.Run(() => installer.Install(game, server, payload, progress, cancel), cancel));
        State.OperationStatus = "安装完成,可以返回首页开始游戏。";
    });

    public Task UninstallAsync() => RunOperationAsync(async (cancel, progress) =>
    {
        var game = ClientInstaller.ValidateGame(State.GameDirectory.Trim());
        await Task.Run(() => installer.Uninstall(game, progress), cancel);
        State.OperationStatus = "已恢复安装前的引导文件。";
        State.CompatibilityStatus = "兼容组件已卸载";
        State.CheckDetails = "";
    });

    private void ShowReport(CompatibilityReport report)
    {
        State.CompatibilityStatus = report.Compatible
            ? $"{report.Version} · {report.Checks.Count} 项签名检查通过"
            : $"{report.Version} · 需要更新兼容规则";
        State.CheckDetails = string.Join('\n', report.Checks.Select(x =>
            $"{(x.Expected == x.Found ? "✓" : "!")} {x.Name}    {x.Found}/{x.Expected}")) +
            "\n" + string.Join('\n', report.Problems) +
            "\n\n签名检查仅验证静态兼容性,实际登录与游戏行为需联调确认。";
    }

    public async Task ProbeServerAsync()
    {
        try
        {
            var identity = await ServerConnection.ReadAsync(State.ServerUrl, App.Lifetime);
            State.ServerName = identity.ServerName;
            State.ServerStatus = "服务器在线";
        }
        catch (Exception ex) { State.ServerStatus = ex.Message; State.ServerName = "尚未连接服务器"; throw; }
    }

    public Task PlayAsync() => RunOperationAsync(async (cancel, progress) =>
    {
        var game = SaveAndValidate();
        var installed = await Task.Run(() => ClientInstaller.VerifyInstalled(game), cancel);
        if (installed.ServerUrl != State.ServerUrl)
            throw new InvalidOperationException("服务器地址已更改,请在安装页重新安装以保存连接配置。");
        progress.Report("检查服务器连接…");
        await ProbeServerAsync();
        cancel.ThrowIfCancellationRequested();
        progress.Report("正在启动游戏并加载兼容组件…");
        var language = State.Language;
        var dx12 = State.UseDirectX12;
        var pid = await Task.Run(() => new GameLauncher().Launch(game, language, dx12, cancel), cancel);
        State.OperationStatus = $"游戏已启动 · 进程 {pid}";
    });

    public async Task ReadLogsAsync()
    {
        var game = State.GameDirectory;
        State.Logs = await Task.Run(() =>
        {
            if (string.IsNullOrWhiteSpace(game)) return "请先选择客户端目录。";
            var folder = ClientInstaller.Managed(game, "logs");
            var latest = Directory.Exists(folder) ? new DirectoryInfo(folder).GetFiles("client-*.log").MaxBy(x => x.LastWriteTimeUtc) : null;
            if (latest is null) return "暂无启动记录。";
            using var stream = new FileStream(latest.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            if (stream.Length > 64 * 1024) stream.Seek(-64 * 1024, SeekOrigin.End);
            using var reader = new StreamReader(stream);
            return latest.Name + "\n\n" + reader.ReadToEnd();
        }, App.Lifetime);
    }

    public void Cancel() => operation?.Cancel();

    public async Task StopAsync()
    {
        Cancel();
        try { if (activeOperation is not null) await activeOperation; }
        catch (OperationCanceledException) { }
        catch (Exception ex) { AppPath.LogException(ex); }
        finally { http.Dispose(); }
    }
}