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