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 bb761610..2c8f7c83 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -870,11 +870,23 @@ 文件系统权限不足,无法转换服务器 - 查询游戏资源信息 + 下载游戏资源索引 游戏文件操作失败:{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/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 ec1748da..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 e673c525..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/Handler/LaunchExecutionEnsureGameNotRunningHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameNotRunningHandler.cs new file mode 100644 index 00000000..fc6f132b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameNotRunningHandler.cs @@ -0,0 +1,38 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Launching.Handler; + +internal sealed class LaunchExecutionEnsureGameNotRunningHandler : ILaunchExecutionDelegateHandler +{ + 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()) + { + 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/Handler/LaunchExecutionEnsureGameResourceHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs new file mode 100644 index 00000000..ecefe3d0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs @@ -0,0 +1,132 @@ +// 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.Handler; + +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; + } + + context.Logger.LogInformation("Game folder: {GameFolder}", gameFolder); + + 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/Handler/LaunchExecutionEnsureSchemeNotExistsHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureSchemeNotExistsHandler.cs new file mode 100644 index 00000000..7dc49fa9 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureSchemeNotExistsHandler.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 LaunchExecutionEnsureSchemeNotExistsHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (context.Scheme is null) + { + context.Result.Kind = LaunchExecutionResultKind.NoActiveScheme; + context.Result.ErrorMessage = SH.ViewModelLaunchGameSchemeNotSelected; + 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/Handler/LaunchExecutionSetChannelOptionsHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetChannelOptionsHandler.cs new file mode 100644 index 00000000..81125511 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetChannelOptionsHandler.cs @@ -0,0 +1,80 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.IO.Ini; +using Snap.Hutao.Service.Game.Configuration; +using System.IO; + +namespace Snap.Hutao.Service.Game.Launching.Handler; + +internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (!context.Options.TryGetGamePathAndFilePathByName(GameConstants.ConfigFileName, out string gamePath, out string? configPath)) + { + context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath; + context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid; + return; + } + + context.Logger.LogInformation("Game config file path: {ConfigPath}", configPath); + + List elements = default!; + try + { + using (FileStream readStream = File.OpenRead(configPath)) + { + elements = [.. IniSerializer.Deserialize(readStream)]; + } + } + catch (FileNotFoundException) + { + context.Result.Kind = LaunchExecutionResultKind.GameConfigFileNotFound; + context.Result.ErrorMessage = SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath); + return; + } + catch (DirectoryNotFoundException) + { + context.Result.Kind = LaunchExecutionResultKind.GameConfigDirectoryNotFound; + context.Result.ErrorMessage = SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath); + return; + } + catch (UnauthorizedAccessException) + { + context.Result.Kind = LaunchExecutionResultKind.GameConfigInsufficientPermissions; + context.Result.ErrorMessage = SH.ServiceGameSetMultiChannelUnauthorizedAccess; + return; + } + + bool changed = false; + + foreach (IniElement element in elements) + { + if (element is IniParameter parameter) + { + if (parameter.Key is ChannelOptions.ChannelName) + { + changed = parameter.Set(context.Scheme.Channel.ToString("D")) || changed; + continue; + } + + if (parameter.Key is ChannelOptions.SubChannelName) + { + changed = parameter.Set(context.Scheme.SubChannel.ToString("D")) || changed; + continue; + } + } + } + + if (changed) + { + using (FileStream writeStream = File.Create(configPath)) + { + IniSerializer.Serialize(writeStream, elements); + } + } + + await next().ConfigureAwait(false); + } +} \ No newline at end of file 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/Handler/LaunchExecutionSetWindowsHDRHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetWindowsHDRHandler.cs new file mode 100644 index 00000000..ea2b1205 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionSetWindowsHDRHandler.cs @@ -0,0 +1,20 @@ +// 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 LaunchExecutionSetWindowsHDRHandler : ILaunchExecutionDelegateHandler +{ + public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) + { + if (context.Options.IsWindowsHDREnabled) + { + context.Logger.LogInformation("Set Windows HDR"); + 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/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/Handler/LaunchExecutionStatusProgressHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionStatusProgressHandler.cs new file mode 100644 index 00000000..574f6cf2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/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.Handler; + +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/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/ILaunchExecutionDelegateHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/ILaunchExecutionDelegateHandler.cs new file mode 100644 index 00000000..78ff79ad --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/ILaunchExecutionDelegateHandler.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Launching; + +internal delegate ValueTask LaunchExecutionDelegate(); + +internal interface ILaunchExecutionDelegateHandler +{ + ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next); +} \ 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 new file mode 100644 index 00000000..e9cb20ac --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs @@ -0,0 +1,48 @@ +// 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; + +[ConstructorGenerated] +internal sealed partial class LaunchExecutionContext +{ + private readonly ILogger logger; + private readonly IServiceProvider serviceProvider; + 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; } + + public ILogger Logger { get => logger; } + + public LaunchOptions Options { get => options; } + + public IViewModelSupportLaunchExecution ViewModel { get; set; } = default!; + + public LaunchScheme Scheme { get; set; } = default!; + + public GameAccount? Account { 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/LaunchExecutionInvoker.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionInvoker.cs new file mode 100644 index 00000000..1e746e6b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionInvoker.cs @@ -0,0 +1,50 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core; +using Snap.Hutao.Service.Game.Launching.Handler; + +namespace Snap.Hutao.Service.Game.Launching; + +[Injection(InjectAs.Transient)] +internal sealed class LaunchExecutionInvoker +{ + private readonly Queue handlers; + + 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()); + handlers.Enqueue(new LaunchExecutionGameProcessStartHandler()); + handlers.Enqueue(new LaunchExecutionStarwardPlayTimeStatisticsHandler()); + handlers.Enqueue(new LaunchExecutionUnlockFpsHandler()); + handlers.Enqueue(new LaunchExecutionGameProcessExitHandler()); + } + + public async ValueTask InvokeAsync(LaunchExecutionContext context) + { + await InvokeHandlerAsync(context).ConfigureAwait(false); + return context.Result; + } + + private async ValueTask InvokeHandlerAsync(LaunchExecutionContext context) + { + if (handlers.TryDequeue(out ILaunchExecutionDelegateHandler? 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; + } +} \ 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 new file mode 100644 index 00000000..f4272c36 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionResult.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Launching; + +internal sealed class LaunchExecutionResult +{ + public LaunchExecutionResultKind Kind { get; set; } + + public string ErrorMessage { get; set; } = default!; +} \ 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/Package/GamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs deleted file mode 100644 index f828323f..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 ffb3d54b..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/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 deleted file mode 100644 index 5f883a65..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.SetPlayingActivity(isOversea).ConfigureAwait(false); - } - - return tracker; - } - - public async ValueTask DisposeAsync() - { - if (previousSetDiscordActivityWhenPlaying) - { - await service.discordService.SetNormalActivity().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/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..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); } } 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