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