From 87ee81e7fafaae9efb546d06874e5a56c7a88511 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Fri, 5 Jan 2024 17:29:30 +0800 Subject: [PATCH] add handlers --- .../Snap.Hutao/Core/LifeCycle/Activation.cs | 2 +- .../Snap.Hutao/Resource/Localization/SH.resx | 10 +- .../Service/Discord/DiscordService.cs | 4 +- .../Service/Discord/IDiscordService.cs | 4 +- .../Service/Game/GameServiceFacade.cs | 2 +- .../Service/Game/IGameServiceFacade.cs | 2 +- .../Game/Launching/LaunchExecutionContext.cs | 24 +++- ...nchExecutionEnsureGameNotRunningHandler.cs | 34 +++++ ...aunchExecutionEnsureGameResourceHandler.cs | 130 ++++++++++++++++++ ...ecutionGameProcessInitializationHandler.cs | 117 ++++++++++++++++ .../Game/Launching/LaunchExecutionInvoker.cs | 22 +-- .../LaunchExecutionPackageConvertHandler.cs | 11 -- .../Game/Launching/LaunchExecutionResult.cs | 7 +- ...LaunchExecutionSetChannelOptionsHandler.cs | 8 +- .../LaunchExecutionSetGameAccountHandler.cs | 21 +++ .../LaunchExecutionSetWindowsHDRHandler.cs | 19 +++ .../LaunchExecutionStatusProgressHandler.cs | 18 +++ .../Game/Package/GamePackageService.cs | 2 +- .../Game/Package/IGamePackageService.cs | 2 +- ...placeStatus.cs => PackageConvertStatus.cs} | 16 +-- .../Service/Game/Package/PackageConverter.cs | 12 +- .../Game/Process/GameProcessService.cs | 4 +- .../LaunchGamePackageConvertDialog.xaml.cs | 2 +- .../Game/IViewModelSupportLaunchExecution.cs | 12 ++ .../ViewModel/Game/LaunchGameViewModel.cs | 2 +- .../Web/Response/ResponseExtension.cs | 15 ++ 26 files changed, 440 insertions(+), 62 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameNotRunningHandler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameResourceHandler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionGameProcessInitializationHandler.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionPackageConvertHandler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetGameAccountHandler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetWindowsHDRHandler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionStatusProgressHandler.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/Package/{PackageReplaceStatus.cs => PackageConvertStatus.cs} (62%) create mode 100644 src/Snap.Hutao/Snap.Hutao/ViewModel/Game/IViewModelSupportLaunchExecution.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs index 959dc35d..4e4bbfb4 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/Activation.cs @@ -190,7 +190,7 @@ internal sealed partial class Activation : IActivation serviceProvider .GetRequiredService() - .SetNormalActivity() + .SetNormalActivityAsync() .SafeForget(); } diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 3e49c037..e698c9cb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -870,14 +870,20 @@ 文件系统权限不足,无法转换服务器 - 查询游戏资源信息 + 下载游戏资源索引 游戏文件操作失败:{0} - + + 游戏进程运行中 + + 请选择游戏路径 + + 下载游戏资源索引失败: {0} + 游戏进程已退出 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs index 8f1654d3..91b62bc5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/DiscordService.cs @@ -11,14 +11,14 @@ internal sealed partial class DiscordService : IDiscordService, IDisposable { private readonly RuntimeOptions runtimeOptions; - public async ValueTask SetPlayingActivity(bool isOversea) + public async ValueTask SetPlayingActivityAsync(bool isOversea) { _ = isOversea ? await DiscordController.SetPlayingGenshinImpactAsync().ConfigureAwait(false) : await DiscordController.SetPlayingYuanShenAsync().ConfigureAwait(false); } - public async ValueTask SetNormalActivity() + public async ValueTask SetNormalActivityAsync() { _ = await DiscordController.SetDefaultActivityAsync(runtimeOptions.AppLaunchTime).ConfigureAwait(false); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs index 46717554..ef9e0e1e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Discord/IDiscordService.cs @@ -5,7 +5,7 @@ namespace Snap.Hutao.Service.Discord; internal interface IDiscordService { - ValueTask SetNormalActivity(); + ValueTask SetNormalActivityAsync(); - ValueTask SetPlayingActivity(bool isOversea); + ValueTask SetPlayingActivityAsync(bool isOversea); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs index ec1748da..99135902 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs @@ -100,7 +100,7 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade } /// - public ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) + public ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) { return gamePackageService.EnsureGameResourceAsync(launchScheme, progress); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs index e673c525..885461e9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs @@ -71,7 +71,7 @@ internal interface IGameServiceFacade /// 目标启动方案 /// 进度 /// 是否替换成功 - ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress); + ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress); /// /// 修改注册表中的账号信息 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs index 0e2e2b86..589b556c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs @@ -1,17 +1,35 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Game.Scheme; +using Snap.Hutao.ViewModel.Game; namespace Snap.Hutao.Service.Game.Launching; -internal sealed class LaunchExecutionContext +[ConstructorGenerated] +internal sealed partial class LaunchExecutionContext { + private readonly ILogger logger; + private readonly IServiceProvider serviceProvider; + private readonly ITaskContext taskContext; + private readonly LaunchOptions options; + public LaunchExecutionResult Result { get; } = new(); - public ILogger Logger { get; set; } = default!; + public IServiceProvider ServiceProvider { get => serviceProvider; } + + public ITaskContext TaskContext { get => taskContext; } + + public ILogger Logger { get => logger; } + + public LaunchOptions Options { get => options; } + + public IViewModelSupportLaunchExecution ViewModel { get; set; } = default!; public LaunchScheme Scheme { get; set; } = default!; - public LaunchOptions Options { get; set; } = default!; + public GameAccount? Account { get; set; } + + public IProgress Progress { get; set; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameNotRunningHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameNotRunningHandler.cs new file mode 100644 index 00000000..41893a8a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameNotRunningHandler.cs @@ -0,0 +1,34 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Launching; + +internal sealed class LaunchExecutionEnsureGameNotRunningHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (IsGameRunning()) + { + context.Result.Kind = LaunchExecutionResultKind.GameProcessRunning; + context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGameIsRunning; + return; + } + + await next().ConfigureAwait(false); + } + + private static bool IsGameRunning() + { + // GetProcesses once and manually loop is O(n) + foreach (ref readonly System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan()) + { + if (string.Equals(process.ProcessName, GameConstants.YuanShenProcessName, StringComparison.OrdinalIgnoreCase) || + string.Equals(process.ProcessName, GameConstants.GenshinImpactProcessName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameResourceHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameResourceHandler.cs new file mode 100644 index 00000000..c8f47110 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameResourceHandler.cs @@ -0,0 +1,130 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.Win32.SafeHandles; +using Snap.Hutao.Control.Extension; +using Snap.Hutao.Factory.ContentDialog; +using Snap.Hutao.Factory.Progress; +using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.PathAbstraction; +using Snap.Hutao.View.Dialog; +using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; +using Snap.Hutao.Web.Response; +using System.Collections.Immutable; +using System.IO; + +namespace Snap.Hutao.Service.Game.Launching; + +internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + IServiceProvider serviceProvider = context.ServiceProvider; + IContentDialogFactory contentDialogFactory = serviceProvider.GetRequiredService(); + IProgressFactory progressFactory = serviceProvider.GetRequiredService(); + + LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + IProgress convertProgress = progressFactory.CreateForMainThread(state => dialog.State = state); + + using (await dialog.BlockAsync(context.TaskContext).ConfigureAwait(false)) + { + if (!await EnsureGameResourceAsync(context, convertProgress).ConfigureAwait(false)) + { + // context.Result is set in EnsureGameResourceAsync + return; + } + + await context.TaskContext.SwitchToMainThreadAsync(); + ImmutableList gamePathEntries = context.Options.GetGamePathEntries(out GamePathEntry? selected); + context.ViewModel.SetGamePathEntriesAndSelectedGamePathEntry(gamePathEntries, selected); + } + + await next().ConfigureAwait(false); + } + + private static async ValueTask EnsureGameResourceAsync(LaunchExecutionContext context, IProgress progress) + { + if (!context.Options.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName)) + { + context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath; + context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid; + return false; + } + + if (!CheckDirectoryPermissions(gameFolder)) + { + context.Result.Kind = LaunchExecutionResultKind.GameDirectoryInsufficientPermissions; + context.Result.ErrorMessage = SH.ServiceGameEnsureGameResourceInsufficientDirectoryPermissions; + return false; + } + + progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation)); + + ResourceClient resourceClient = context.ServiceProvider.GetRequiredService(); + Response response = await resourceClient.GetResourceAsync(context.Scheme).ConfigureAwait(false); + + if (!response.TryGetDataWithoutUINotification(out GameResource? resource)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; + context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(response); + return false; + } + + PackageConverter packageConverter = context.ServiceProvider.GetRequiredService(); + + if (!context.Scheme.ExecutableMatches(gameFileName)) + { + if (!await packageConverter.EnsureGameResourceAsync(context.Scheme, resource, gameFolder, progress).ConfigureAwait(false)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourcePackageConvertInternalError; + context.Result.ErrorMessage = SH.ViewModelLaunchGameEnsureGameResourceFail; + return false; + } + + // We need to change the gamePath if we switched. + string executableName = context.Scheme.IsOversea ? GameConstants.GenshinImpactFileName : GameConstants.YuanShenFileName; + + await context.TaskContext.SwitchToMainThreadAsync(); + context.Options.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, executableName)); + } + + await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false); + return true; + } + + private static bool CheckDirectoryPermissions(string folder) + { + // Program Files has special permissions limitation. + string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); + if (folder.StartsWith(programFiles, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + try + { + string tempFilePath = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp"); + string tempFilePathMove = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp"); + + // Test create file + using (SafeFileHandle handle = File.OpenHandle(tempFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, preallocationSize: 32 * 1024)) + { + // Test write file + RandomAccess.Write(handle, "SNAP HUTAO DIRECTORY PERMISSION CHECK"u8, 0); + RandomAccess.FlushToDisk(handle); + } + + // Test move file + File.Move(tempFilePath, tempFilePathMove); + + // Test delete file + File.Delete(tempFilePathMove); + + return true; + } + catch (Exception) + { + return false; + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionGameProcessInitializationHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionGameProcessInitializationHandler.cs new file mode 100644 index 00000000..24c1dc5d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionGameProcessInitializationHandler.cs @@ -0,0 +1,117 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core; +using Snap.Hutao.Service.Discord; +using System.IO; + +namespace Snap.Hutao.Service.Game.Launching; + +internal sealed class LaunchExecutionGameProcessInitializationHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (!context.Options.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName)) + { + context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath; + context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid; + return; + } + + context.Progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing)); + using (System.Diagnostics.Process game = InitializeGameProcess(context, gamePath)) + { + await next().ConfigureAwait(false); + + // TODO: move to new handlers + await using (await GameRunningTracker.CreateAsync(this, isOversea).ConfigureAwait(false)) + { + game.Start(); + progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted)); + + if (launchOptions.UseStarwardPlayTimeStatistics) + { + await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false); + } + + if (runtimeOptions.IsElevated && launchOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps) + { + progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps)); + try + { + await UnlockFpsAsync(game, progress).ConfigureAwait(false); + } + catch (InvalidOperationException) + { + // The Unlocker can't unlock the process + game.Kill(); + throw; + } + finally + { + progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); + } + } + else + { + progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit)); + await game.WaitForExitAsync().ConfigureAwait(false); + progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); + } + } + } + } + + private static System.Diagnostics.Process InitializeGameProcess(LaunchExecutionContext context, string gamePath) + { + LaunchOptions launchOptions = context.Options; + + string commandLine = string.Empty; + if (launchOptions.IsEnabled) + { + // https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html + // https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html + commandLine = new CommandLineBuilder() + .AppendIf(launchOptions.IsBorderless, "-popupwindow") + .AppendIf(launchOptions.IsExclusive, "-window-mode", "exclusive") + .Append("-screen-fullscreen", launchOptions.IsFullScreen ? 1 : 0) + .AppendIf(launchOptions.IsScreenWidthEnabled, "-screen-width", launchOptions.ScreenWidth) + .AppendIf(launchOptions.IsScreenHeightEnabled, "-screen-height", launchOptions.ScreenHeight) + .AppendIf(launchOptions.IsMonitorEnabled, "-monitor", launchOptions.Monitor.Value) + .AppendIf(launchOptions.IsUseCloudThirdPartyMobile, "-platform_type CLOUD_THIRD_PARTY_MOBILE") + .ToString(); + } + + return new() + { + StartInfo = new() + { + Arguments = commandLine, + FileName = gamePath, + UseShellExecute = true, + Verb = "runas", + WorkingDirectory = Path.GetDirectoryName(gamePath), + }, + }; + } +} + +internal sealed class LaunchExecutionSetDiscordActivityHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + IDiscordService discordService = context.ServiceProvider.GetRequiredService(); + bool previousSetDiscordActivityWhenPlaying = context.Options.SetDiscordActivityWhenPlaying; + if (previousSetDiscordActivityWhenPlaying) + { + await discordService.SetPlayingActivityAsync(context.Scheme.IsOversea).ConfigureAwait(false); + } + + await next().ConfigureAwait(false); + + if (previousSetDiscordActivityWhenPlaying) + { + await discordService.SetNormalActivityAsync().ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionInvoker.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionInvoker.cs index 1e2f8829..a61feb12 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionInvoker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionInvoker.cs @@ -1,6 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using Snap.Hutao.Core; + namespace Snap.Hutao.Service.Game.Launching; [Injection(InjectAs.Transient)] @@ -11,25 +13,29 @@ internal sealed class LaunchExecutionInvoker public LaunchExecutionInvoker() { handlers = []; + handlers.Enqueue(new LaunchExecutionEnsureGameNotRunningHandler()); handlers.Enqueue(new LaunchExecutionEnsureSchemeNotExistsHandler()); handlers.Enqueue(new LaunchExecutionSetChannelOptionsHandler()); + handlers.Enqueue(new LaunchExecutionEnsureGameResourceHandler()); + handlers.Enqueue(new LaunchExecutionSetGameAccountHandler()); + handlers.Enqueue(new LaunchExecutionSetWindowsHDRHandler()); + handlers.Enqueue(new LaunchExecutionStatusProgressHandler()); + handlers.Enqueue(new LaunchExecutionGameProcessInitializationHandler()); + handlers.Enqueue(new LaunchExecutionSetDiscordActivityHandler()); } - public async ValueTask LaunchAsync(LaunchExecutionContext context) + public async ValueTask InvokeAsync(LaunchExecutionContext context) { - await ExecuteAsync(context).ConfigureAwait(false); + await InvokeHandlerAsync(context).ConfigureAwait(false); return context.Result; } - private async ValueTask ExecuteAsync(LaunchExecutionContext context) + private async ValueTask InvokeHandlerAsync(LaunchExecutionContext context) { if (handlers.TryDequeue(out ILaunchExecutionDelegateHandler? handler)) { - string handlerName = handler.GetType().Name; - - context.Logger.LogInformation("Handler[{Handler}] begin execute", handlerName); - await handler.OnExecutionAsync(context, () => ExecuteAsync(context)).ConfigureAwait(false); - context.Logger.LogInformation("Handler[{Handler}] end execute", handlerName); + context.Logger.LogInformation("Handler[{Handler}] begin execution", TypeNameHelper.GetTypeDisplayName(handler)); + await handler.OnExecutionAsync(context, () => InvokeHandlerAsync(context)).ConfigureAwait(false); } return context; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionPackageConvertHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionPackageConvertHandler.cs deleted file mode 100644 index 2d7fc6c8..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionPackageConvertHandler.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Service.Game.Launching; - -internal sealed class LaunchExecutionPackageConvertHandler : ILaunchExecutionDelegateHandler -{ - public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) - { - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResult.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResult.cs index 4461cf8d..2719050d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResult.cs @@ -15,7 +15,12 @@ internal enum LaunchExecutionResultKind Ok, NoActiveScheme, NoActiveGamePath, + GameProcessRunning, GameConfigFileNotFound, GameConfigDirectoryNotFound, - GameConfigUnauthorizedAccess, + GameConfigInsufficientPermissions, + GameDirectoryInsufficientPermissions, + GameResourceIndexQueryInvalidResponse, + GameResourcePackageConvertInternalError, + GameAccountRegistryWriteResultNotMatch, } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetChannelOptionsHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetChannelOptionsHandler.cs index 60bd081b..eebd3299 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetChannelOptionsHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetChannelOptionsHandler.cs @@ -9,14 +9,12 @@ namespace Snap.Hutao.Service.Game.Launching; internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecutionDelegateHandler { - private const string ConfigFileName = "config.ini"; - public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) { - if (!context.Options.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath)) + if (!context.Options.TryGetGamePathAndFilePathByName(GameConstants.ConfigFileName, out string gamePath, out string? configPath)) { context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath; - context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionSetChannelOptionsGamePathNotValid; + context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid; return; } @@ -42,7 +40,7 @@ internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecution } catch (UnauthorizedAccessException) { - context.Result.Kind = LaunchExecutionResultKind.GameConfigUnauthorizedAccess; + context.Result.Kind = LaunchExecutionResultKind.GameConfigInsufficientPermissions; context.Result.ErrorMessage = SH.ServiceGameSetMultiChannelUnauthorizedAccess; return; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetGameAccountHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetGameAccountHandler.cs new file mode 100644 index 00000000..d8b9aa27 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetGameAccountHandler.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Account; + +namespace Snap.Hutao.Service.Game.Launching; + +internal sealed class LaunchExecutionSetGameAccountHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (context.Account is not null && !RegistryInterop.Set(context.Account)) + { + context.Result.Kind = LaunchExecutionResultKind.GameAccountRegistryWriteResultNotMatch; + context.Result.ErrorMessage = SH.ViewModelLaunchGameSwitchGameAccountFail; + return; + } + + await next().ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetWindowsHDRHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetWindowsHDRHandler.cs new file mode 100644 index 00000000..747622e4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetWindowsHDRHandler.cs @@ -0,0 +1,19 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Account; + +namespace Snap.Hutao.Service.Game.Launching; + +internal sealed class LaunchExecutionSetWindowsHDRHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (context.Options.IsWindowsHDREnabled) + { + RegistryInterop.SetWindowsHDR(context.Scheme.IsOversea); + } + + await next().ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionStatusProgressHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionStatusProgressHandler.cs new file mode 100644 index 00000000..1cebd6b8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionStatusProgressHandler.cs @@ -0,0 +1,18 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Factory.Progress; + +namespace Snap.Hutao.Service.Game.Launching; + +internal sealed class LaunchExecutionStatusProgressHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService(); + LaunchStatusOptions statusOptions = context.ServiceProvider.GetRequiredService(); + context.Progress = progressFactory.CreateForMainThread(status => statusOptions.LaunchStatus = status); + + await next().ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs index f828323f..9bcfaa07 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs @@ -19,7 +19,7 @@ internal sealed partial class GamePackageService : IGamePackageService private readonly LaunchOptions launchOptions; private readonly ITaskContext taskContext; - public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) + public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) { if (!launchOptions.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName)) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs index ffb3d54b..85024b90 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs @@ -7,5 +7,5 @@ namespace Snap.Hutao.Service.Game.Package; internal interface IGamePackageService { - ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress); + ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConvertStatus.cs similarity index 62% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConvertStatus.cs index b57eb3e3..f73bb093 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConvertStatus.cs @@ -8,25 +8,15 @@ namespace Snap.Hutao.Service.Game.Package; /// /// 包更新状态 /// -internal sealed class PackageReplaceStatus +internal sealed class PackageConvertStatus { - /// - /// 构造一个新的包更新状态 - /// - /// 描述 - public PackageReplaceStatus(string name) + public PackageConvertStatus(string name) { Name = name; Description = name; } - /// - /// 构造一个新的包更新状态 - /// - /// 名称 - /// 读取的字节数 - /// 总字节数 - public PackageReplaceStatus(string name, long bytesRead, long totalBytes) + public PackageConvertStatus(string name, long bytesRead, long totalBytes) { Percent = (double)bytesRead / totalBytes; Name = name; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs index 8b34eed5..0742851c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -34,7 +34,7 @@ internal sealed partial class PackageConverter private readonly HttpClient httpClient; private readonly ILogger logger; - public async ValueTask EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress progress) + public async ValueTask EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress progress) { // 以 国服 => 国际 为例 // 1. 下载国际服的 pkg_version 文件,转换为索引字典 @@ -188,7 +188,7 @@ internal sealed partial class PackageConverter } } - private async ValueTask PrepareCacheFilesAsync(List operations, PackageConverterFileSystemContext context, IProgress progress) + private async ValueTask PrepareCacheFilesAsync(List operations, PackageConverterFileSystemContext context, IProgress progress) { foreach (PackageItemOperationInfo info in operations) { @@ -204,7 +204,7 @@ internal sealed partial class PackageConverter } } - private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress progress) + private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress progress) { // 还原正确的远程地址 string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName); @@ -230,7 +230,7 @@ internal sealed partial class PackageConverter Directory.CreateDirectory(directory); string remoteUrl = context.GetScatteredFilesUrl(remoteName); - HttpShardCopyWorkerOptions options = new() + HttpShardCopyWorkerOptions options = new() { HttpClient = httpClient, SourceUrl = remoteUrl, @@ -238,7 +238,7 @@ internal sealed partial class PackageConverter StatusFactory = (bytesRead, totalBytes) => new(remoteName, bytesRead, totalBytes), }; - using (HttpShardCopyWorker worker = await HttpShardCopyWorker.CreateAsync(options).ConfigureAwait(false)) + using (HttpShardCopyWorker worker = await HttpShardCopyWorker.CreateAsync(options).ConfigureAwait(false)) { try { @@ -258,7 +258,7 @@ internal sealed partial class PackageConverter } } - private async ValueTask ReplaceGameResourceAsync(List operations, PackageConverterFileSystemContext context, IProgress progress) + private async ValueTask ReplaceGameResourceAsync(List operations, PackageConverterFileSystemContext context, IProgress progress) { // 执行下载与移动操作 foreach (PackageItemOperationInfo info in operations) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs index 5f883a65..63e49a12 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs @@ -167,7 +167,7 @@ internal sealed partial class GameProcessService : IGameProcessService GameRunningTracker tracker = new(service, isOversea); if (tracker.previousSetDiscordActivityWhenPlaying) { - await service.discordService.SetPlayingActivity(isOversea).ConfigureAwait(false); + await service.discordService.SetPlayingActivityAsync(isOversea).ConfigureAwait(false); } return tracker; @@ -177,7 +177,7 @@ internal sealed partial class GameProcessService : IGameProcessService { if (previousSetDiscordActivityWhenPlaying) { - await service.discordService.SetNormalActivity().ConfigureAwait(false); + await service.discordService.SetNormalActivityAsync().ConfigureAwait(false); } service.isGameRunning = false; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs index 62b3d631..fc0ee20b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs @@ -11,7 +11,7 @@ namespace Snap.Hutao.View.Dialog; /// 启动游戏客户端转换对话框 /// [HighQuality] -[DependencyProperty("State", typeof(PackageReplaceStatus))] +[DependencyProperty("State", typeof(PackageConvertStatus))] internal sealed partial class LaunchGamePackageConvertDialog : ContentDialog { /// diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/IViewModelSupportLaunchExecution.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/IViewModelSupportLaunchExecution.cs new file mode 100644 index 00000000..d01eae50 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/IViewModelSupportLaunchExecution.cs @@ -0,0 +1,12 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.PathAbstraction; +using System.Collections.Immutable; + +namespace Snap.Hutao.ViewModel.Game; + +internal interface IViewModelSupportLaunchExecution +{ + void SetGamePathEntriesAndSelectedGamePathEntry(ImmutableList gamePathEntries, GamePathEntry? selectedEntry); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index 97ce485d..f282f804 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -218,7 +218,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel gameService.SetChannelOptions(SelectedScheme); LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - IProgress convertProgress = progressFactory.CreateForMainThread(state => dialog.State = state); + IProgress convertProgress = progressFactory.CreateForMainThread(state => dialog.State = state); using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/ResponseExtension.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/ResponseExtension.cs index a88cd6ae..76039129 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/ResponseExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/ResponseExtension.cs @@ -24,4 +24,19 @@ internal static class ResponseExtension return false; } } + + public static bool TryGetDataWithoutUINotification(this Response response, [NotNullWhen(true)] out TData? data) + { + if (response.ReturnCode == 0) + { + ArgumentNullException.ThrowIfNull(response.Data); + data = response.Data; + return true; + } + else + { + data = default; + return false; + } + } } \ No newline at end of file