From 76183901da9a91c527c14e18236c94593330c695 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Fri, 5 Jan 2024 22:33:10 +0800 Subject: [PATCH] clean up --- .../Snap.Hutao/Resource/Localization/SH.resx | 3 + .../GameChannelOptionsService.cs | 59 ------ .../IGameChannelOptionsService.cs | 4 - .../Service/Game/GameServiceFacade.cs | 32 +-- .../Service/Game/IGameServiceFacade.cs | 24 --- ...nchExecutionEnsureGameNotRunningHandler.cs | 32 +-- ...aunchExecutionEnsureGameResourceHandler.cs | 4 +- ...chExecutionEnsureSchemeNotExistsHandler.cs | 3 +- .../LaunchExecutionGameProcessExitHandler.cs | 20 ++ ...ecutionGameProcessInitializationHandler.cs | 61 ++++++ .../LaunchExecutionGameProcessStartHandler.cs | 25 +++ ...LaunchExecutionSetChannelOptionsHandler.cs | 4 +- ...aunchExecutionSetDiscordActivityHandler.cs | 39 ++++ .../LaunchExecutionSetGameAccountHandler.cs | 26 +++ .../LaunchExecutionSetWindowsHDRHandler.cs | 3 +- ...cutionStarwardPlayTimeStatisticsHandler.cs | 31 +++ .../LaunchExecutionStatusProgressHandler.cs | 2 +- .../LaunchExecutionUnlockFpsHandler.cs | 42 ++++ .../Game/Launching/LaunchExecutionContext.cs | 15 +- ...ecutionGameProcessInitializationHandler.cs | 117 ----------- .../Game/Launching/LaunchExecutionInvoker.cs | 9 +- .../Game/Launching/LaunchExecutionResult.cs | 15 -- .../Launching/LaunchExecutionResultKind.cs | 20 ++ .../LaunchExecutionSetGameAccountHandler.cs | 21 -- .../Game/Package/GamePackageService.cs | 102 ---------- .../Game/Package/IGamePackageService.cs | 11 -- .../Game/Process/GameProcessService.cs | 186 ------------------ .../Game/Process/IGameProcessService.cs | 11 -- .../Service/Game/Process/Starward.cs | 19 -- .../Snap.Hutao/View/Card/LaunchGameCard.xaml | 25 ++- .../ViewModel/Game/LaunchGameViewModel.cs | 67 ++----- .../ViewModel/Game/LaunchGameViewModelSlim.cs | 39 ++-- 32 files changed, 369 insertions(+), 702 deletions(-) rename src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/{ => Handler}/LaunchExecutionEnsureGameNotRunningHandler.cs (73%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/{ => Handler}/LaunchExecutionEnsureGameResourceHandler.cs (97%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/{ => Handler}/LaunchExecutionEnsureSchemeNotExistsHandler.cs (79%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessExitHandler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessInitializationHandler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessStartHandler.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/{ => Handler}/LaunchExecutionSetChannelOptionsHandler.cs (94%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetDiscordActivityHandler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetGameAccountHandler.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/{ => Handler}/LaunchExecutionSetWindowsHDRHandler.cs (82%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStarwardPlayTimeStatisticsHandler.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/{ => Handler}/LaunchExecutionStatusProgressHandler.cs (93%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionGameProcessInitializationHandler.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResultKind.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetGameAccountHandler.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Process/IGameProcessService.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index e698c9cb..2c8f7c83 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -875,6 +875,9 @@ 游戏文件操作失败:{0} + + 解锁帧率上限失败 + 游戏进程运行中 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs index 92e34f62..f8832249 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs @@ -38,63 +38,4 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer return new(channel, subChannel, isOversea); } } - - public bool SetChannelOptions(LaunchScheme scheme) - { - if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath)) - { - return false; - } - - List elements = default!; - try - { - using (FileStream readStream = File.OpenRead(configPath)) - { - elements = [.. IniSerializer.Deserialize(readStream)]; - } - } - catch (FileNotFoundException ex) - { - ThrowHelper.GameFileOperation(SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath), ex); - } - catch (DirectoryNotFoundException ex) - { - ThrowHelper.GameFileOperation(SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath), ex); - } - catch (UnauthorizedAccessException ex) - { - ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelUnauthorizedAccess, ex); - } - - bool changed = false; - - foreach (IniElement element in elements) - { - if (element is IniParameter parameter) - { - if (parameter.Key is ChannelOptions.ChannelName) - { - changed = parameter.Set(scheme.Channel.ToString("D")) || changed; - continue; - } - - if (parameter.Key is ChannelOptions.SubChannelName) - { - changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed; - continue; - } - } - } - - if (changed) - { - using (FileStream writeStream = File.Create(configPath)) - { - IniSerializer.Serialize(writeStream, elements); - } - } - - return changed; - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs index a07fbc9a..671a54af 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/IGameChannelOptionsService.cs @@ -1,13 +1,9 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Service.Game.Scheme; - namespace Snap.Hutao.Service.Game.Configuration; internal interface IGameChannelOptionsService { ChannelOptions GetChannelOptions(); - - bool SetChannelOptions(LaunchScheme scheme); } \ 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 99135902..6247aeed 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs @@ -5,10 +5,8 @@ using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Primitive; using Snap.Hutao.Service.Game.Account; using Snap.Hutao.Service.Game.Configuration; -using Snap.Hutao.Service.Game.Package; +using Snap.Hutao.Service.Game.Launching.Handler; using Snap.Hutao.Service.Game.PathAbstraction; -using Snap.Hutao.Service.Game.Process; -using Snap.Hutao.Service.Game.Scheme; using System.Collections.ObjectModel; namespace Snap.Hutao.Service.Game; @@ -23,8 +21,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade { private readonly IGameChannelOptionsService gameChannelOptionsService; private readonly IGameAccountService gameAccountService; - private readonly IGameProcessService gameProcessService; - private readonly IGamePackageService gamePackageService; private readonly IGamePathService gamePathService; /// @@ -45,12 +41,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade return gameChannelOptionsService.GetChannelOptions(); } - /// - public bool SetChannelOptions(LaunchScheme scheme) - { - return gameChannelOptionsService.SetChannelOptions(scheme); - } - /// public ValueTask DetectGameAccountAsync(SchemeType scheme) { @@ -63,12 +53,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade return gameAccountService.DetectCurrentGameAccount(scheme); } - /// - public bool SetGameAccount(GameAccount account) - { - return gameAccountService.SetGameAccount(account); - } - /// public void AttachGameAccountToUid(GameAccount gameAccount, string uid) { @@ -90,18 +74,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade /// public bool IsGameRunning() { - return gameProcessService.IsGameRunning(); - } - - /// - public ValueTask LaunchAsync(IProgress progress) - { - return gameProcessService.LaunchAsync(progress); - } - - /// - public ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) - { - return gamePackageService.EnsureGameResourceAsync(launchScheme, progress); + return LaunchExecutionEnsureGameNotRunningHandler.IsGameRunning(out _); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs index 885461e9..c2bae875 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs @@ -49,8 +49,6 @@ internal interface IGameServiceFacade /// 是否正在运行 bool IsGameRunning(); - ValueTask LaunchAsync(IProgress progress); - /// /// 异步修改游戏账号名称 /// @@ -65,27 +63,5 @@ internal interface IGameServiceFacade /// 任务 ValueTask RemoveGameAccountAsync(GameAccount gameAccount); - /// - /// 替换游戏资源 - /// - /// 目标启动方案 - /// 进度 - /// 是否替换成功 - ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress); - - /// - /// 修改注册表中的账号信息 - /// - /// 账号 - /// 是否设置成功 - bool SetGameAccount(GameAccount account); - - /// - /// 设置多通道值 - /// - /// 方案 - /// 是否更改了ini文件 - bool SetChannelOptions(LaunchScheme scheme); - GameAccount? DetectCurrentGameAccount(SchemeType scheme); } \ 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/Handler/LaunchExecutionEnsureGameNotRunningHandler.cs similarity index 73% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameNotRunningHandler.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameNotRunningHandler.cs index 41893a8a..fc6f132b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameNotRunningHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameNotRunningHandler.cs @@ -1,23 +1,11 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Service.Game.Launching; +namespace Snap.Hutao.Service.Game.Launching.Handler; 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() + public static bool IsGameRunning([NotNullWhen(true)] out System.Diagnostics.Process? runningProcess) { // GetProcesses once and manually loop is O(n) foreach (ref readonly System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan()) @@ -25,10 +13,26 @@ internal sealed class LaunchExecutionEnsureGameNotRunningHandler : ILaunchExecut if (string.Equals(process.ProcessName, GameConstants.YuanShenProcessName, StringComparison.OrdinalIgnoreCase) || string.Equals(process.ProcessName, GameConstants.GenshinImpactProcessName, StringComparison.OrdinalIgnoreCase)) { + runningProcess = process; return true; } } + runningProcess = default; return false; } + + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (IsGameRunning(out System.Diagnostics.Process? process)) + { + context.Logger.LogInformation("Game process detected, id: {Id}", process.Id); + + context.Result.Kind = LaunchExecutionResultKind.GameProcessRunning; + context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGameIsRunning; + return; + } + + await next().ConfigureAwait(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/Handler/LaunchExecutionEnsureGameResourceHandler.cs similarity index 97% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameResourceHandler.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs index c8f47110..ecefe3d0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureGameResourceHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs @@ -13,7 +13,7 @@ using Snap.Hutao.Web.Response; using System.Collections.Immutable; using System.IO; -namespace Snap.Hutao.Service.Game.Launching; +namespace Snap.Hutao.Service.Game.Launching.Handler; internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutionDelegateHandler { @@ -51,6 +51,8 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio return false; } + context.Logger.LogInformation("Game folder: {GameFolder}", gameFolder); + if (!CheckDirectoryPermissions(gameFolder)) { context.Result.Kind = LaunchExecutionResultKind.GameDirectoryInsufficientPermissions; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureSchemeNotExistsHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureSchemeNotExistsHandler.cs similarity index 79% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureSchemeNotExistsHandler.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureSchemeNotExistsHandler.cs index 1cc14fe6..7dc49fa9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionEnsureSchemeNotExistsHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureSchemeNotExistsHandler.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Service.Game.Launching; +namespace Snap.Hutao.Service.Game.Launching.Handler; internal sealed class LaunchExecutionEnsureSchemeNotExistsHandler : ILaunchExecutionDelegateHandler { @@ -14,6 +14,7 @@ internal sealed class LaunchExecutionEnsureSchemeNotExistsHandler : ILaunchExecu return; } + context.Logger.LogInformation("Scheme[{Scheme}] is selected", context.Scheme.DisplayName); await next().ConfigureAwait(false); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessExitHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessExitHandler.cs new file mode 100644 index 00000000..cbd10715 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessExitHandler.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Launching.Handler; + +internal sealed class LaunchExecutionGameProcessExitHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (!context.Process.HasExited) + { + context.Progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit)); + await context.Process.WaitForExitAsync().ConfigureAwait(false); + } + + context.Logger.LogInformation("Game process exited with code {ExitCode}", context.Process.ExitCode); + context.Progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited)); + await next().ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessInitializationHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessInitializationHandler.cs new file mode 100644 index 00000000..ce7cac1a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessInitializationHandler.cs @@ -0,0 +1,61 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core; +using System.IO; + +namespace Snap.Hutao.Service.Game.Launching.Handler; + +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 (context.Process = InitializeGameProcess(context, gamePath)) + { + await next().ConfigureAwait(false); + } + } + + 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(); + } + + context.Logger.LogInformation("Command Line Arguments: {commandLine}", commandLine); + + return new() + { + StartInfo = new() + { + Arguments = commandLine, + FileName = gamePath, + UseShellExecute = true, + Verb = "runas", + WorkingDirectory = Path.GetDirectoryName(gamePath), + }, + }; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessStartHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessStartHandler.cs new file mode 100644 index 00000000..24fc23b4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionGameProcessStartHandler.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Windows.Win32.Foundation; + +namespace Snap.Hutao.Service.Game.Launching.Handler; + +internal sealed class LaunchExecutionGameProcessStartHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + try + { + context.Process.Start(); + context.Logger.LogInformation("Process started"); + } + catch (Win32Exception ex) when (ex.HResult == HRESULT.E_FAIL) + { + return; + } + + context.Progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted)); + await next().ConfigureAwait(false); + } +} \ 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/Handler/LaunchExecutionSetChannelOptionsHandler.cs similarity index 94% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetChannelOptionsHandler.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetChannelOptionsHandler.cs index eebd3299..81125511 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetChannelOptionsHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetChannelOptionsHandler.cs @@ -5,7 +5,7 @@ using Snap.Hutao.Core.IO.Ini; using Snap.Hutao.Service.Game.Configuration; using System.IO; -namespace Snap.Hutao.Service.Game.Launching; +namespace Snap.Hutao.Service.Game.Launching.Handler; internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecutionDelegateHandler { @@ -18,6 +18,8 @@ internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecution return; } + context.Logger.LogInformation("Game config file path: {ConfigPath}", configPath); + List elements = default!; try { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetDiscordActivityHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetDiscordActivityHandler.cs new file mode 100644 index 00000000..ecad554c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetDiscordActivityHandler.cs @@ -0,0 +1,39 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Discord; + +namespace Snap.Hutao.Service.Game.Launching.Handler; + +internal sealed class LaunchExecutionSetDiscordActivityHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + bool previousSetDiscordActivityWhenPlaying = context.Options.SetDiscordActivityWhenPlaying; + + try + { + if (previousSetDiscordActivityWhenPlaying) + { + context.Logger.LogInformation("Set discord activity as playing"); + await context.ServiceProvider + .GetRequiredService() + .SetPlayingActivityAsync(context.Scheme.IsOversea) + .ConfigureAwait(false); + } + + await next().ConfigureAwait(false); + } + finally + { + if (previousSetDiscordActivityWhenPlaying) + { + context.Logger.LogInformation("Recover discord activity"); + await context.ServiceProvider + .GetRequiredService() + .SetNormalActivityAsync() + .ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetGameAccountHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetGameAccountHandler.cs new file mode 100644 index 00000000..39635011 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetGameAccountHandler.cs @@ -0,0 +1,26 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game.Account; + +namespace Snap.Hutao.Service.Game.Launching.Handler; + +internal sealed class LaunchExecutionSetGameAccountHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (context.Account is not null) + { + context.Logger.LogInformation("Set game account to [{Account}]", context.Account.Name); + + if (!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/Handler/LaunchExecutionSetWindowsHDRHandler.cs similarity index 82% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetWindowsHDRHandler.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetWindowsHDRHandler.cs index 747622e4..ea2b1205 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetWindowsHDRHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetWindowsHDRHandler.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Service.Game.Account; -namespace Snap.Hutao.Service.Game.Launching; +namespace Snap.Hutao.Service.Game.Launching.Handler; internal sealed class LaunchExecutionSetWindowsHDRHandler : ILaunchExecutionDelegateHandler { @@ -11,6 +11,7 @@ internal sealed class LaunchExecutionSetWindowsHDRHandler : ILaunchExecutionDele { if (context.Options.IsWindowsHDREnabled) { + context.Logger.LogInformation("Set Windows HDR"); RegistryInterop.SetWindowsHDR(context.Scheme.IsOversea); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStarwardPlayTimeStatisticsHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStarwardPlayTimeStatisticsHandler.cs new file mode 100644 index 00000000..92d93686 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStarwardPlayTimeStatisticsHandler.cs @@ -0,0 +1,31 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Windows.System; + +namespace Snap.Hutao.Service.Game.Launching.Handler; + +internal sealed class LaunchExecutionStarwardPlayTimeStatisticsHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (context.Options.UseStarwardPlayTimeStatistics) + { + context.Logger.LogInformation("Using starward to count game time"); + await LaunchStarwardForPlayTimeStatisticsAsync(context).ConfigureAwait(false); + } + + await next().ConfigureAwait(false); + } + + private static async ValueTask LaunchStarwardForPlayTimeStatisticsAsync(LaunchExecutionContext context) + { + string gameBiz = context.Scheme.IsOversea ? "hk4e_global" : "hk4e_cn"; + Uri starwardPlayTimeUri = $"starward://playtime/{gameBiz}".ToUri(); + if (await Launcher.QueryUriSupportAsync(starwardPlayTimeUri, LaunchQuerySupportType.Uri) is LaunchQuerySupportStatus.Available) + { + context.Logger.LogInformation("Launching starward"); + await Launcher.LaunchUriAsync(starwardPlayTimeUri); + } + } +} \ 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/Handler/LaunchExecutionStatusProgressHandler.cs similarity index 93% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionStatusProgressHandler.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStatusProgressHandler.cs index 1cebd6b8..574f6cf2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionStatusProgressHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStatusProgressHandler.cs @@ -3,7 +3,7 @@ using Snap.Hutao.Factory.Progress; -namespace Snap.Hutao.Service.Game.Launching; +namespace Snap.Hutao.Service.Game.Launching.Handler; internal sealed class LaunchExecutionStatusProgressHandler : ILaunchExecutionDelegateHandler { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs new file mode 100644 index 00000000..dc007240 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionUnlockFpsHandler.cs @@ -0,0 +1,42 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core; +using Snap.Hutao.Factory.Progress; +using Snap.Hutao.Service.Game.Unlocker; + +namespace Snap.Hutao.Service.Game.Launching.Handler; + +internal sealed class LaunchExecutionUnlockFpsHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + RuntimeOptions runtimeOptions = context.ServiceProvider.GetRequiredService(); + if (runtimeOptions.IsElevated && context.Options.IsAdvancedLaunchOptionsEnabled && context.Options.UnlockFps) + { + context.Logger.LogInformation("Unlocking FPS"); + context.Progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps)); + + IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService(); + IProgress progress = progressFactory.CreateForMainThread(status => context.Progress.Report(LaunchStatus.FromUnlockStatus(status))); + GameFpsUnlocker unlocker = context.ServiceProvider.CreateInstance(context.Process); + + try + { + await unlocker.UnlockAsync(new(100, 20000, 3000), progress, context.CancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + context.Logger.LogCritical(ex, "Unlocking FPS failed"); + + context.Result.Kind = LaunchExecutionResultKind.GameFpsUnlockingFailed; + context.Result.ErrorMessage = ex.Message; + + // The Unlocker can't unlock the process + context.Process.Kill(); + } + } + + await next().ConfigureAwait(false); + } +} \ No newline at end of file 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 589b556c..e9cb20ac 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs @@ -15,8 +15,19 @@ internal sealed partial class LaunchExecutionContext private readonly ITaskContext taskContext; private readonly LaunchOptions options; + [SuppressMessage("", "SH007")] + public LaunchExecutionContext(IServiceProvider serviceProvider,IViewModelSupportLaunchExecution viewModel, LaunchScheme? scheme, GameAccount? account) + : this(serviceProvider) + { + ViewModel = viewModel; + Scheme = scheme!; + Account = account; + } + public LaunchExecutionResult Result { get; } = new(); + public CancellationToken CancellationToken { get; set; } + public IServiceProvider ServiceProvider { get => serviceProvider; } public ITaskContext TaskContext { get => taskContext; } @@ -31,5 +42,7 @@ internal sealed partial class LaunchExecutionContext public GameAccount? Account { get; set; } - public IProgress Progress { get; set; } + public IProgress Progress { get; set; } = default!; + + public System.Diagnostics.Process Process { get; set; } = default!; } \ 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 deleted file mode 100644 index 24c1dc5d..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionGameProcessInitializationHandler.cs +++ /dev/null @@ -1,117 +0,0 @@ -// 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 a61feb12..1e746e6b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionInvoker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionInvoker.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Core; +using Snap.Hutao.Service.Game.Launching.Handler; namespace Snap.Hutao.Service.Game.Launching; @@ -22,6 +23,10 @@ internal sealed class LaunchExecutionInvoker handlers.Enqueue(new LaunchExecutionStatusProgressHandler()); handlers.Enqueue(new LaunchExecutionGameProcessInitializationHandler()); handlers.Enqueue(new LaunchExecutionSetDiscordActivityHandler()); + handlers.Enqueue(new LaunchExecutionGameProcessStartHandler()); + handlers.Enqueue(new LaunchExecutionStarwardPlayTimeStatisticsHandler()); + handlers.Enqueue(new LaunchExecutionUnlockFpsHandler()); + handlers.Enqueue(new LaunchExecutionGameProcessExitHandler()); } public async ValueTask InvokeAsync(LaunchExecutionContext context) @@ -34,8 +39,10 @@ internal sealed class LaunchExecutionInvoker { if (handlers.TryDequeue(out ILaunchExecutionDelegateHandler? handler)) { - context.Logger.LogInformation("Handler[{Handler}] begin execution", TypeNameHelper.GetTypeDisplayName(handler)); + string typeName = TypeNameHelper.GetTypeDisplayName(handler, false); + context.Logger.LogInformation("Handler[{Handler}] begin execution", typeName); await handler.OnExecutionAsync(context, () => InvokeHandlerAsync(context)).ConfigureAwait(false); + context.Logger.LogInformation("Handler[{Handler}] end execution", typeName); } return context; 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 2719050d..f4272c36 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResult.cs @@ -8,19 +8,4 @@ internal sealed class LaunchExecutionResult public LaunchExecutionResultKind Kind { get; set; } public string ErrorMessage { get; set; } = default!; -} - -internal enum LaunchExecutionResultKind -{ - Ok, - NoActiveScheme, - NoActiveGamePath, - GameProcessRunning, - GameConfigFileNotFound, - GameConfigDirectoryNotFound, - GameConfigInsufficientPermissions, - GameDirectoryInsufficientPermissions, - GameResourceIndexQueryInvalidResponse, - GameResourcePackageConvertInternalError, - GameAccountRegistryWriteResultNotMatch, } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResultKind.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResultKind.cs new file mode 100644 index 00000000..63eb448a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResultKind.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Launching; + +internal enum LaunchExecutionResultKind +{ + Ok, + NoActiveScheme, + NoActiveGamePath, + GameProcessRunning, + GameConfigFileNotFound, + GameConfigDirectoryNotFound, + GameConfigInsufficientPermissions, + GameDirectoryInsufficientPermissions, + GameResourceIndexQueryInvalidResponse, + GameResourcePackageConvertInternalError, + GameAccountRegistryWriteResultNotMatch, + GameFpsUnlockingFailed, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetGameAccountHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetGameAccountHandler.cs deleted file mode 100644 index d8b9aa27..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionSetGameAccountHandler.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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/Package/GamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs deleted file mode 100644 index 9bcfaa07..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Microsoft.Win32.SafeHandles; -using Snap.Hutao.Service.Game.Scheme; -using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; -using Snap.Hutao.Web.Response; -using System.IO; -using static Snap.Hutao.Service.Game.GameConstants; - -namespace Snap.Hutao.Service.Game.Package; - -[ConstructorGenerated] -[Injection(InjectAs.Singleton, typeof(IGamePackageService))] -internal sealed partial class GamePackageService : IGamePackageService -{ - private readonly PackageConverter packageConverter; - private readonly IServiceProvider serviceProvider; - private readonly LaunchOptions launchOptions; - private readonly ITaskContext taskContext; - - public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) - { - if (!launchOptions.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName)) - { - return false; - } - - if (!CheckDirectoryPermissions(gameFolder)) - { - progress.Report(new(SH.ServiceGameEnsureGameResourceInsufficientDirectoryPermissions)); - return false; - } - - progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation)); - Response response = await serviceProvider - .GetRequiredService() - .GetResourceAsync(launchScheme) - .ConfigureAwait(false); - - if (!response.IsOk()) - { - return false; - } - - GameResource resource = response.Data; - - if (!launchScheme.ExecutableMatches(gameFileName)) - { - // We can't start the game when we failed to convert game - if (!await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false)) - { - return false; - } - - // We need to change the gamePath if we switched. - string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName; - - await taskContext.SwitchToMainThreadAsync(); - launchOptions.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, exeName)); - } - - 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/Package/IGamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs deleted file mode 100644 index 85024b90..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IGamePackageService.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Service.Game.Scheme; - -namespace Snap.Hutao.Service.Game.Package; - -internal interface IGamePackageService -{ - ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress); -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs deleted file mode 100644 index 63e49a12..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core; -using Snap.Hutao.Factory.Progress; -using Snap.Hutao.Service.Discord; -using Snap.Hutao.Service.Game.Account; -using Snap.Hutao.Service.Game.Scheme; -using Snap.Hutao.Service.Game.Unlocker; -using System.IO; -using static Snap.Hutao.Service.Game.GameConstants; - -namespace Snap.Hutao.Service.Game.Process; - -/// -/// 进程互操作 -/// -[ConstructorGenerated] -[Injection(InjectAs.Singleton, typeof(IGameProcessService))] -internal sealed partial class GameProcessService : IGameProcessService -{ - private readonly IServiceProvider serviceProvider; - private readonly IProgressFactory progressFactory; - private readonly IDiscordService discordService; - private readonly RuntimeOptions runtimeOptions; - private readonly LaunchOptions launchOptions; - - private volatile bool isGameRunning; - - public bool IsGameRunning() - { - if (isGameRunning) - { - return true; - } - - // Original two GetProcessesByName is O(2n) - // GetProcesses once and manually loop is O(n) - foreach (ref System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan()) - { - if (process.ProcessName is YuanShenProcessName or GenshinImpactProcessName) - { - return true; - } - } - - return false; - } - - public async ValueTask LaunchAsync(IProgress progress) - { - if (IsGameRunning()) - { - return; - } - - if (!launchOptions.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName)) - { - ArgumentException.ThrowIfNullOrEmpty(gamePath); - return; // null check passing, actually never reach. - } - - bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileName); - - if (launchOptions.IsWindowsHDREnabled) - { - RegistryInterop.SetWindowsHDR(isOversea); - } - - progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing)); - using (System.Diagnostics.Process game = InitializeGameProcess(gamePath)) - { - 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 System.Diagnostics.Process InitializeGameProcess(string gamePath) - { - 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), - }, - }; - } - - private ValueTask UnlockFpsAsync(System.Diagnostics.Process game, IProgress progress, CancellationToken token = default) - { -#pragma warning disable CA1859 - IGameFpsUnlocker unlocker = serviceProvider.CreateInstance(game); -#pragma warning restore CA1859 - UnlockTimingOptions options = new(100, 20000, 3000); - IProgress lockerProgress = progressFactory.CreateForMainThread(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus))); - return unlocker.UnlockAsync(options, lockerProgress, token); - } - - private class GameRunningTracker : IAsyncDisposable - { - private readonly GameProcessService service; - private readonly bool previousSetDiscordActivityWhenPlaying; - - private GameRunningTracker(GameProcessService service, bool isOversea) - { - service.isGameRunning = true; - previousSetDiscordActivityWhenPlaying = service.launchOptions.SetDiscordActivityWhenPlaying; - this.service = service; - } - - public static async ValueTask CreateAsync(GameProcessService service, bool isOversea) - { - GameRunningTracker tracker = new(service, isOversea); - if (tracker.previousSetDiscordActivityWhenPlaying) - { - await service.discordService.SetPlayingActivityAsync(isOversea).ConfigureAwait(false); - } - - return tracker; - } - - public async ValueTask DisposeAsync() - { - if (previousSetDiscordActivityWhenPlaying) - { - await service.discordService.SetNormalActivityAsync().ConfigureAwait(false); - } - - service.isGameRunning = false; - } - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/IGameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/IGameProcessService.cs deleted file mode 100644 index 2f39d442..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/IGameProcessService.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Service.Game.Process; - -internal interface IGameProcessService -{ - bool IsGameRunning(); - - ValueTask LaunchAsync(IProgress progress); -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs deleted file mode 100644 index 1a827666..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/Starward.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Windows.System; - -namespace Snap.Hutao.Service.Game.Process; - -internal static class Starward -{ - public static async ValueTask LaunchForPlayTimeStatisticsAsync(bool isOversea) - { - string gameBiz = isOversea ? "hk4e_global" : "hk4e_cn"; - Uri starwardPlayTimeUri = $"starward://playtime/{gameBiz}".ToUri(); - if (await Launcher.QueryUriSupportAsync(starwardPlayTimeUri, LaunchQuerySupportType.Uri) is LaunchQuerySupportStatus.Available) - { - await Launcher.LaunchUriAsync(starwardPlayTimeUri); - } - } -} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml index 1384c03b..b48381ec 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml @@ -40,6 +40,7 @@ - - - + VerticalAlignment="Bottom" + Spacing="8"> + + + + + \ 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 f282f804..b9a8486c 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -3,27 +3,22 @@ using CommunityToolkit.WinUI.Collections; using Microsoft.Extensions.Caching.Memory; -using Snap.Hutao.Control.Extension; using Snap.Hutao.Core; using Snap.Hutao.Core.Diagnostics.CodeAnalysis; using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Factory.ContentDialog; -using Snap.Hutao.Factory.Progress; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service; using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Launching; using Snap.Hutao.Service.Game.Locator; -using Snap.Hutao.Service.Game.Package; using Snap.Hutao.Service.Game.PathAbstraction; using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Service.Notification; using Snap.Hutao.Service.User; -using Snap.Hutao.View.Dialog; using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.IO; -using Windows.Win32.Foundation; namespace Snap.Hutao.ViewModel.Game; @@ -33,18 +28,16 @@ namespace Snap.Hutao.ViewModel.Game; [HighQuality] [ConstructorGenerated] [Injection(InjectAs.Scoped)] -internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel +internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel, IViewModelSupportLaunchExecution { /// /// 启动游戏目标 Uid /// public const string DesiredUid = nameof(DesiredUid); - private readonly IContentDialogFactory contentDialogFactory; private readonly LaunchStatusOptions launchStatusOptions; private readonly IGameLocatorFactory gameLocatorFactory; private readonly ILogger logger; - private readonly IProgressFactory progressFactory; private readonly IInfoBarService infoBarService; private readonly ResourceClient resourceClient; private readonly RuntimeOptions runtimeOptions; @@ -172,9 +165,16 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel } } + public void SetGamePathEntriesAndSelectedGamePathEntry(ImmutableList gamePathEntries, GamePathEntry? selectedEntry) + { + GamePathEntries = gamePathEntries; + SelectedGamePathEntry = selectedEntry; + } + protected override ValueTask InitializeUIAsync() { - SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions(); + ImmutableList gamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry); + SetGamePathEntriesAndSelectedGamePathEntry(gamePathEntries, entry); return ValueTask.FromResult(true); } @@ -207,51 +207,18 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel [Command("LaunchCommand")] private async Task LaunchAsync() { - if (SelectedScheme is null) - { - infoBarService.Error(SH.ViewModelLaunchGameSchemeNotSelected); - return; - } - try { - gameService.SetChannelOptions(SelectedScheme); + LaunchExecutionContext context = new(Ioc.Default, this, SelectedScheme, SelectedGameAccount); + LaunchExecutionResult result = await new LaunchExecutionInvoker().InvokeAsync(context).ConfigureAwait(false); - LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - IProgress convertProgress = progressFactory.CreateForMainThread(state => dialog.State = state); - - using (await dialog.BlockAsync(taskContext).ConfigureAwait(false)) + if (result.Kind is not LaunchExecutionResultKind.Ok) { - // Always ensure game resources - if (!await gameService.EnsureGameResourceAsync(SelectedScheme, convertProgress).ConfigureAwait(false)) - { - infoBarService.Warning(SH.ViewModelLaunchGameEnsureGameResourceFail, dialog.State?.Name ?? string.Empty); - return; - } - else - { - await taskContext.SwitchToMainThreadAsync(); - SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions(); - } + infoBarService.Warning(result.ErrorMessage); } - - if (SelectedGameAccount is not null && !gameService.SetGameAccount(SelectedGameAccount)) - { - infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail); - return; - } - - IProgress launchProgress = progressFactory.CreateForMainThread(status => launchStatusOptions.LaunchStatus = status); - await gameService.LaunchAsync(launchProgress).ConfigureAwait(false); } catch (Exception ex) { - if (ex is Win32Exception win32Exception && win32Exception.HResult == HRESULT.E_FAIL) - { - // User canceled the operation. ignore - return; - } - logger.LogCritical(ex, "Launch failed"); infoBarService.Error(ex); } @@ -371,10 +338,4 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel }; } } - - private void SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions() - { - GamePathEntries = launchOptions.GetGamePathEntries(out GamePathEntry? entry); - SelectedGamePathEntry = entry; - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs index d61fbf5e..a7d29315 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs @@ -3,13 +3,14 @@ using CommunityToolkit.WinUI.Collections; using Snap.Hutao.Core.ExceptionService; -using Snap.Hutao.Factory.Progress; using Snap.Hutao.Model.Entity; using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Launching; +using Snap.Hutao.Service.Game.PathAbstraction; using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Service.Notification; +using System.Collections.Immutable; using System.Collections.ObjectModel; -using Windows.Win32.Foundation; namespace Snap.Hutao.ViewModel.Game; @@ -18,10 +19,10 @@ namespace Snap.Hutao.ViewModel.Game; /// [Injection(InjectAs.Transient)] [ConstructorGenerated(CallBaseConstructor = true)] -internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim +internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim, IViewModelSupportLaunchExecution { private readonly LaunchStatusOptions launchStatusOptions; - private readonly IProgressFactory progressFactory; + private readonly ILogger logger; private readonly IInfoBarService infoBarService; private readonly IGameServiceFacade gameService; private readonly ITaskContext taskContext; @@ -30,6 +31,8 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli private GameAccount? selectedGameAccount; private GameAccountFilter? gameAccountFilter; + public LaunchStatusOptions LaunchStatusOptions { get => launchStatusOptions; } + public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); } /// @@ -37,6 +40,10 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli /// public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); } + public void SetGamePathEntriesAndSelectedGamePathEntry(ImmutableList gamePathEntries, GamePathEntry? selectedEntry) + { + } + /// protected override async Task OpenUIAsync() { @@ -69,29 +76,21 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli private async Task LaunchAsync() { IInfoBarService infoBarService = ServiceProvider.GetRequiredService(); + LaunchScheme? scheme = LaunchGameShared.GetCurrentLaunchSchemeFromConfigFile(gameService, infoBarService); try { - if (SelectedGameAccount is not null) - { - if (!gameService.SetGameAccount(SelectedGameAccount)) - { - infoBarService.Warning(SH.ViewModelLaunchGameSwitchGameAccountFail); - return; - } - } + LaunchExecutionContext context = new(Ioc.Default, this, scheme, SelectedGameAccount); + LaunchExecutionResult result = await new LaunchExecutionInvoker().InvokeAsync(context).ConfigureAwait(false); - IProgress launchProgress = progressFactory.CreateForMainThread(status => launchStatusOptions.LaunchStatus = status); - await gameService.LaunchAsync(launchProgress).ConfigureAwait(false); + if (result.Kind is not LaunchExecutionResultKind.Ok) + { + infoBarService.Warning(result.ErrorMessage); + } } catch (Exception ex) { - if (ex is Win32Exception win32Exception && win32Exception.HResult == HRESULT.E_FAIL) - { - // User canceled the operation. ignore - return; - } - + logger.LogCritical(ex, "Launch failed"); infoBarService.Error(ex); } }