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(); }
}
}
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(); }
}
}