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