mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
5 Commits
fix/schedu
...
feat/launc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76183901da | ||
|
|
87ee81e7fa | ||
|
|
f2f858de15 | ||
|
|
c434521004 | ||
|
|
27ed2cefc1 |
@@ -190,7 +190,7 @@ internal sealed partial class Activation : IActivation
|
||||
|
||||
serviceProvider
|
||||
.GetRequiredService<IDiscordService>()
|
||||
.SetNormalActivity()
|
||||
.SetNormalActivityAsync()
|
||||
.SafeForget();
|
||||
}
|
||||
|
||||
|
||||
@@ -870,11 +870,23 @@
|
||||
<value>文件系统权限不足,无法转换服务器</value>
|
||||
</data>
|
||||
<data name="ServiceGameEnsureGameResourceQueryResourceInformation" xml:space="preserve">
|
||||
<value>查询游戏资源信息</value>
|
||||
<value>下载游戏资源索引</value>
|
||||
</data>
|
||||
<data name="ServiceGameFileOperationExceptionMessage" xml:space="preserve">
|
||||
<value>游戏文件操作失败:{0}</value>
|
||||
</data>
|
||||
<data name="ServiceGameLaunchExecutionGameFpsUnlockFailed" xml:space="preserve">
|
||||
<value>解锁帧率上限失败</value>
|
||||
</data>
|
||||
<data name="ServiceGameLaunchExecutionGameIsRunning" xml:space="preserve">
|
||||
<value>游戏进程运行中</value>
|
||||
</data>
|
||||
<data name="ServiceGameLaunchExecutionGamePathNotValid" xml:space="preserve">
|
||||
<value>请选择游戏路径</value>
|
||||
</data>
|
||||
<data name="ServiceGameLaunchExecutionGameResourceQueryIndexFailed" xml:space="preserve">
|
||||
<value>下载游戏资源索引失败: {0}</value>
|
||||
</data>
|
||||
<data name="ServiceGameLaunchPhaseProcessExited" xml:space="preserve">
|
||||
<value>游戏进程已退出</value>
|
||||
</data>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Snap.Hutao.Service.Discord;
|
||||
|
||||
internal interface IDiscordService
|
||||
{
|
||||
ValueTask SetNormalActivity();
|
||||
ValueTask SetNormalActivityAsync();
|
||||
|
||||
ValueTask SetPlayingActivity(bool isOversea);
|
||||
ValueTask SetPlayingActivityAsync(bool isOversea);
|
||||
}
|
||||
@@ -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<IniElement> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -45,12 +41,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
||||
return gameChannelOptionsService.GetChannelOptions();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetChannelOptions(LaunchScheme scheme)
|
||||
{
|
||||
return gameChannelOptionsService.SetChannelOptions(scheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
|
||||
{
|
||||
@@ -63,12 +53,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
||||
return gameAccountService.DetectCurrentGameAccount(scheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetGameAccount(GameAccount account)
|
||||
{
|
||||
return gameAccountService.SetGameAccount(account);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
|
||||
{
|
||||
@@ -90,18 +74,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
||||
/// <inheritdoc/>
|
||||
public bool IsGameRunning()
|
||||
{
|
||||
return gameProcessService.IsGameRunning();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
|
||||
{
|
||||
return gameProcessService.LaunchAsync(progress);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
return gamePackageService.EnsureGameResourceAsync(launchScheme, progress);
|
||||
return LaunchExecutionEnsureGameNotRunningHandler.IsGameRunning(out _);
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,6 @@ internal interface IGameServiceFacade
|
||||
/// <returns>是否正在运行</returns>
|
||||
bool IsGameRunning();
|
||||
|
||||
ValueTask LaunchAsync(IProgress<LaunchStatus> progress);
|
||||
|
||||
/// <summary>
|
||||
/// 异步修改游戏账号名称
|
||||
/// </summary>
|
||||
@@ -65,27 +63,5 @@ internal interface IGameServiceFacade
|
||||
/// <returns>任务</returns>
|
||||
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
/// <summary>
|
||||
/// 替换游戏资源
|
||||
/// </summary>
|
||||
/// <param name="launchScheme">目标启动方案</param>
|
||||
/// <param name="progress">进度</param>
|
||||
/// <returns>是否替换成功</returns>
|
||||
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
|
||||
|
||||
/// <summary>
|
||||
/// 修改注册表中的账号信息
|
||||
/// </summary>
|
||||
/// <param name="account">账号</param>
|
||||
/// <returns>是否设置成功</returns>
|
||||
bool SetGameAccount(GameAccount account);
|
||||
|
||||
/// <summary>
|
||||
/// 设置多通道值
|
||||
/// </summary>
|
||||
/// <param name="scheme">方案</param>
|
||||
/// <returns>是否更改了ini文件</returns>
|
||||
bool SetChannelOptions(LaunchScheme scheme);
|
||||
|
||||
GameAccount? DetectCurrentGameAccount(SchemeType scheme);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionEnsureGameNotRunningHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public static bool IsGameRunning([NotNullWhen(true)] out System.Diagnostics.Process? runningProcess)
|
||||
{
|
||||
// GetProcesses once and manually loop is O(n)
|
||||
foreach (ref readonly System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan())
|
||||
{
|
||||
if (string.Equals(process.ProcessName, GameConstants.YuanShenProcessName, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(process.ProcessName, GameConstants.GenshinImpactProcessName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
runningProcess = process;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
runningProcess = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (IsGameRunning(out System.Diagnostics.Process? process))
|
||||
{
|
||||
context.Logger.LogInformation("Game process detected, id: {Id}", process.Id);
|
||||
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameProcessRunning;
|
||||
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGameIsRunning;
|
||||
return;
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
IServiceProvider serviceProvider = context.ServiceProvider;
|
||||
IContentDialogFactory contentDialogFactory = serviceProvider.GetRequiredService<IContentDialogFactory>();
|
||||
IProgressFactory progressFactory = serviceProvider.GetRequiredService<IProgressFactory>();
|
||||
|
||||
LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
|
||||
IProgress<PackageConvertStatus> convertProgress = progressFactory.CreateForMainThread<PackageConvertStatus>(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<GamePathEntry> gamePathEntries = context.Options.GetGamePathEntries(out GamePathEntry? selected);
|
||||
context.ViewModel.SetGamePathEntriesAndSelectedGamePathEntry(gamePathEntries, selected);
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async ValueTask<bool> EnsureGameResourceAsync(LaunchExecutionContext context, IProgress<PackageConvertStatus> progress)
|
||||
{
|
||||
if (!context.Options.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName))
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
|
||||
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
|
||||
return false;
|
||||
}
|
||||
|
||||
context.Logger.LogInformation("Game folder: {GameFolder}", gameFolder);
|
||||
|
||||
if (!CheckDirectoryPermissions(gameFolder))
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameDirectoryInsufficientPermissions;
|
||||
context.Result.ErrorMessage = SH.ServiceGameEnsureGameResourceInsufficientDirectoryPermissions;
|
||||
return false;
|
||||
}
|
||||
|
||||
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
|
||||
|
||||
ResourceClient resourceClient = context.ServiceProvider.GetRequiredService<ResourceClient>();
|
||||
Response<GameResource> 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<PackageConverter>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionEnsureSchemeNotExistsHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (context.Scheme is null)
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.NoActiveScheme;
|
||||
context.Result.ErrorMessage = SH.ViewModelLaunchGameSchemeNotSelected;
|
||||
return;
|
||||
}
|
||||
|
||||
context.Logger.LogInformation("Scheme[{Scheme}] is selected", context.Scheme.DisplayName);
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.IO.Ini;
|
||||
using Snap.Hutao.Service.Game.Configuration;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (!context.Options.TryGetGamePathAndFilePathByName(GameConstants.ConfigFileName, out string gamePath, out string? configPath))
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
|
||||
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
|
||||
return;
|
||||
}
|
||||
|
||||
context.Logger.LogInformation("Game config file path: {ConfigPath}", configPath);
|
||||
|
||||
List<IniElement> elements = default!;
|
||||
try
|
||||
{
|
||||
using (FileStream readStream = File.OpenRead(configPath))
|
||||
{
|
||||
elements = [.. IniSerializer.Deserialize(readStream)];
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameConfigFileNotFound;
|
||||
context.Result.ErrorMessage = SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath);
|
||||
return;
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameConfigDirectoryNotFound;
|
||||
context.Result.ErrorMessage = SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath);
|
||||
return;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameConfigInsufficientPermissions;
|
||||
context.Result.ErrorMessage = SH.ServiceGameSetMultiChannelUnauthorizedAccess;
|
||||
return;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
|
||||
foreach (IniElement element in elements)
|
||||
{
|
||||
if (element is IniParameter parameter)
|
||||
{
|
||||
if (parameter.Key is ChannelOptions.ChannelName)
|
||||
{
|
||||
changed = parameter.Set(context.Scheme.Channel.ToString("D")) || changed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parameter.Key is ChannelOptions.SubChannelName)
|
||||
{
|
||||
changed = parameter.Set(context.Scheme.SubChannel.ToString("D")) || changed;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
using (FileStream writeStream = File.Create(configPath))
|
||||
{
|
||||
IniSerializer.Serialize(writeStream, elements);
|
||||
}
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -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<IDiscordService>()
|
||||
.SetPlayingActivityAsync(context.Scheme.IsOversea)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (previousSetDiscordActivityWhenPlaying)
|
||||
{
|
||||
context.Logger.LogInformation("Recover discord activity");
|
||||
await context.ServiceProvider
|
||||
.GetRequiredService<IDiscordService>()
|
||||
.SetNormalActivityAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Game.Account;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionSetWindowsHDRHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (context.Options.IsWindowsHDREnabled)
|
||||
{
|
||||
context.Logger.LogInformation("Set Windows HDR");
|
||||
RegistryInterop.SetWindowsHDR(context.Scheme.IsOversea);
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionStatusProgressHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService<IProgressFactory>();
|
||||
LaunchStatusOptions statusOptions = context.ServiceProvider.GetRequiredService<LaunchStatusOptions>();
|
||||
context.Progress = progressFactory.CreateForMainThread<LaunchStatus>(status => statusOptions.LaunchStatus = status);
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -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<RuntimeOptions>();
|
||||
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<IProgressFactory>();
|
||||
IProgress<UnlockerStatus> progress = progressFactory.CreateForMainThread<UnlockerStatus>(status => context.Progress.Report(LaunchStatus.FromUnlockStatus(status)));
|
||||
GameFpsUnlocker unlocker = context.ServiceProvider.CreateInstance<GameFpsUnlocker>(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);
|
||||
}
|
||||
}
|
||||
@@ -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<LaunchExecutionContext> LaunchExecutionDelegate();
|
||||
|
||||
internal interface ILaunchExecutionDelegateHandler
|
||||
{
|
||||
ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.ViewModel.Game;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching;
|
||||
|
||||
[ConstructorGenerated]
|
||||
internal sealed partial class LaunchExecutionContext
|
||||
{
|
||||
private readonly ILogger<LaunchExecutionContext> logger;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly LaunchOptions options;
|
||||
|
||||
[SuppressMessage("", "SH007")]
|
||||
public LaunchExecutionContext(IServiceProvider serviceProvider,IViewModelSupportLaunchExecution viewModel, LaunchScheme? scheme, GameAccount? account)
|
||||
: this(serviceProvider)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
Scheme = scheme!;
|
||||
Account = account;
|
||||
}
|
||||
|
||||
public LaunchExecutionResult Result { get; } = new();
|
||||
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
public IServiceProvider ServiceProvider { get => serviceProvider; }
|
||||
|
||||
public ITaskContext TaskContext { get => taskContext; }
|
||||
|
||||
public ILogger Logger { get => logger; }
|
||||
|
||||
public LaunchOptions Options { get => options; }
|
||||
|
||||
public IViewModelSupportLaunchExecution ViewModel { get; set; } = default!;
|
||||
|
||||
public LaunchScheme Scheme { get; set; } = default!;
|
||||
|
||||
public GameAccount? Account { get; set; }
|
||||
|
||||
public IProgress<LaunchStatus> Progress { get; set; } = default!;
|
||||
|
||||
public System.Diagnostics.Process Process { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching;
|
||||
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal sealed class LaunchExecutionInvoker
|
||||
{
|
||||
private readonly Queue<ILaunchExecutionDelegateHandler> handlers;
|
||||
|
||||
public LaunchExecutionInvoker()
|
||||
{
|
||||
handlers = [];
|
||||
handlers.Enqueue(new LaunchExecutionEnsureGameNotRunningHandler());
|
||||
handlers.Enqueue(new LaunchExecutionEnsureSchemeNotExistsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionSetChannelOptionsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionEnsureGameResourceHandler());
|
||||
handlers.Enqueue(new LaunchExecutionSetGameAccountHandler());
|
||||
handlers.Enqueue(new LaunchExecutionSetWindowsHDRHandler());
|
||||
handlers.Enqueue(new LaunchExecutionStatusProgressHandler());
|
||||
handlers.Enqueue(new LaunchExecutionGameProcessInitializationHandler());
|
||||
handlers.Enqueue(new LaunchExecutionSetDiscordActivityHandler());
|
||||
handlers.Enqueue(new LaunchExecutionGameProcessStartHandler());
|
||||
handlers.Enqueue(new LaunchExecutionStarwardPlayTimeStatisticsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionUnlockFpsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionGameProcessExitHandler());
|
||||
}
|
||||
|
||||
public async ValueTask<LaunchExecutionResult> InvokeAsync(LaunchExecutionContext context)
|
||||
{
|
||||
await InvokeHandlerAsync(context).ConfigureAwait(false);
|
||||
return context.Result;
|
||||
}
|
||||
|
||||
private async ValueTask<LaunchExecutionContext> InvokeHandlerAsync(LaunchExecutionContext context)
|
||||
{
|
||||
if (handlers.TryDequeue(out ILaunchExecutionDelegateHandler? handler))
|
||||
{
|
||||
string typeName = TypeNameHelper.GetTypeDisplayName(handler, false);
|
||||
context.Logger.LogInformation("Handler[{Handler}] begin execution", typeName);
|
||||
await handler.OnExecutionAsync(context, () => InvokeHandlerAsync(context)).ConfigureAwait(false);
|
||||
context.Logger.LogInformation("Handler[{Handler}] end execution", typeName);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching;
|
||||
|
||||
internal sealed class LaunchExecutionResult
|
||||
{
|
||||
public LaunchExecutionResultKind Kind { get; set; }
|
||||
|
||||
public string ErrorMessage { get; set; } = default!;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> 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<GameResource> response = await serviceProvider
|
||||
.GetRequiredService<ResourceClient>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
|
||||
}
|
||||
@@ -8,25 +8,15 @@ namespace Snap.Hutao.Service.Game.Package;
|
||||
/// <summary>
|
||||
/// 包更新状态
|
||||
/// </summary>
|
||||
internal sealed class PackageReplaceStatus
|
||||
internal sealed class PackageConvertStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的包更新状态
|
||||
/// </summary>
|
||||
/// <param name="name">描述</param>
|
||||
public PackageReplaceStatus(string name)
|
||||
public PackageConvertStatus(string name)
|
||||
{
|
||||
Name = name;
|
||||
Description = name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的包更新状态
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="bytesRead">读取的字节数</param>
|
||||
/// <param name="totalBytes">总字节数</param>
|
||||
public PackageReplaceStatus(string name, long bytesRead, long totalBytes)
|
||||
public PackageConvertStatus(string name, long bytesRead, long totalBytes)
|
||||
{
|
||||
Percent = (double)bytesRead / totalBytes;
|
||||
Name = name;
|
||||
@@ -34,7 +34,7 @@ internal sealed partial class PackageConverter
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly ILogger<PackageConverter> logger;
|
||||
|
||||
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageReplaceStatus> progress)
|
||||
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageConvertStatus> progress)
|
||||
{
|
||||
// 以 国服 => 国际 为例
|
||||
// 1. 下载国际服的 pkg_version 文件,转换为索引字典
|
||||
@@ -188,7 +188,7 @@ internal sealed partial class PackageConverter
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
|
||||
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
|
||||
{
|
||||
foreach (PackageItemOperationInfo info in operations)
|
||||
{
|
||||
@@ -204,7 +204,7 @@ internal sealed partial class PackageConverter
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
|
||||
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> 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<PackageReplaceStatus> options = new()
|
||||
HttpShardCopyWorkerOptions<PackageConvertStatus> options = new()
|
||||
{
|
||||
HttpClient = httpClient,
|
||||
SourceUrl = remoteUrl,
|
||||
@@ -238,7 +238,7 @@ internal sealed partial class PackageConverter
|
||||
StatusFactory = (bytesRead, totalBytes) => new(remoteName, bytesRead, totalBytes),
|
||||
};
|
||||
|
||||
using (HttpShardCopyWorker<PackageReplaceStatus> worker = await HttpShardCopyWorker<PackageReplaceStatus>.CreateAsync(options).ConfigureAwait(false))
|
||||
using (HttpShardCopyWorker<PackageConvertStatus> worker = await HttpShardCopyWorker<PackageConvertStatus>.CreateAsync(options).ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -258,7 +258,7 @@ internal sealed partial class PackageConverter
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
|
||||
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
|
||||
{
|
||||
// 执行下载与移动操作
|
||||
foreach (PackageItemOperationInfo info in operations)
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 进程互操作
|
||||
/// </summary>
|
||||
[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<LaunchStatus> 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<LaunchStatus> progress, CancellationToken token = default)
|
||||
{
|
||||
#pragma warning disable CA1859
|
||||
IGameFpsUnlocker unlocker = serviceProvider.CreateInstance<GameFpsUnlocker>(game);
|
||||
#pragma warning restore CA1859
|
||||
UnlockTimingOptions options = new(100, 20000, 3000);
|
||||
IProgress<UnlockerStatus> lockerProgress = progressFactory.CreateForMainThread<UnlockerStatus>(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<GameRunningTracker> CreateAsync(GameProcessService service, bool isOversea)
|
||||
{
|
||||
GameRunningTracker tracker = new(service, isOversea);
|
||||
if (tracker.previousSetDiscordActivityWhenPlaying)
|
||||
{
|
||||
await service.discordService.SetPlayingActivity(isOversea).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return tracker;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (previousSetDiscordActivityWhenPlaying)
|
||||
{
|
||||
await service.discordService.SetNormalActivity().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
service.isGameRunning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<LaunchStatus> progress);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@
|
||||
</Grid.RowDefinitions>
|
||||
<FontIcon
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="{ThemeResource TitleTextBlockFontSize}"
|
||||
@@ -61,16 +62,22 @@
|
||||
Content="{StaticResource FontIconContentSetting}"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
ToolTipService.ToolTip="{shcm:ResourceString Name=ViewPageHomeLaunchGameSettingAction}"/>
|
||||
<shc:SizeRestrictedContentControl
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Grid.ColumnSpan="3"
|
||||
VerticalAlignment="Bottom">
|
||||
<ComboBox
|
||||
DisplayMemberPath="Name"
|
||||
ItemsSource="{Binding GameAccountsView}"
|
||||
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
|
||||
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
|
||||
</shc:SizeRestrictedContentControl>
|
||||
VerticalAlignment="Bottom"
|
||||
Spacing="8">
|
||||
<TextBlock
|
||||
Opacity="0.7"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding LaunchStatusOptions.LaunchStatus.Description, Mode=OneWay}"/>
|
||||
<shc:SizeRestrictedContentControl VerticalAlignment="Bottom">
|
||||
<ComboBox
|
||||
DisplayMemberPath="Name"
|
||||
ItemsSource="{Binding GameAccountsView}"
|
||||
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
|
||||
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
|
||||
</shc:SizeRestrictedContentControl>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Button>
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.View.Dialog;
|
||||
/// 启动游戏客户端转换对话框
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("State", typeof(PackageReplaceStatus))]
|
||||
[DependencyProperty("State", typeof(PackageConvertStatus))]
|
||||
internal sealed partial class LaunchGamePackageConvertDialog : ContentDialog
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -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<GamePathEntry> gamePathEntries, GamePathEntry? selectedEntry);
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 启动游戏目标 Uid
|
||||
/// </summary>
|
||||
public const string DesiredUid = nameof(DesiredUid);
|
||||
|
||||
private readonly IContentDialogFactory contentDialogFactory;
|
||||
private readonly LaunchStatusOptions launchStatusOptions;
|
||||
private readonly IGameLocatorFactory gameLocatorFactory;
|
||||
private readonly ILogger<LaunchGameViewModel> 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<GamePathEntry> gamePathEntries, GamePathEntry? selectedEntry)
|
||||
{
|
||||
GamePathEntries = gamePathEntries;
|
||||
SelectedGamePathEntry = selectedEntry;
|
||||
}
|
||||
|
||||
protected override ValueTask<bool> InitializeUIAsync()
|
||||
{
|
||||
SyncGamePathEntriesAndSelectedGamePathEntryFromLaunchOptions();
|
||||
ImmutableList<GamePathEntry> 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<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
|
||||
IProgress<PackageReplaceStatus> convertProgress = progressFactory.CreateForMainThread<PackageReplaceStatus>(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<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Transient)]
|
||||
[ConstructorGenerated(CallBaseConstructor = true)]
|
||||
internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim<View.Page.LaunchGamePage>
|
||||
internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim<View.Page.LaunchGamePage>, IViewModelSupportLaunchExecution
|
||||
{
|
||||
private readonly LaunchStatusOptions launchStatusOptions;
|
||||
private readonly IProgressFactory progressFactory;
|
||||
private readonly ILogger<LaunchGameViewModelSlim> 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); }
|
||||
|
||||
/// <summary>
|
||||
@@ -37,6 +40,10 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
|
||||
/// </summary>
|
||||
public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); }
|
||||
|
||||
public void SetGamePathEntriesAndSelectedGamePathEntry(ImmutableList<GamePathEntry> gamePathEntries, GamePathEntry? selectedEntry)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OpenUIAsync()
|
||||
{
|
||||
@@ -69,29 +76,21 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
|
||||
private async Task LaunchAsync()
|
||||
{
|
||||
IInfoBarService infoBarService = ServiceProvider.GetRequiredService<IInfoBarService>();
|
||||
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<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ internal sealed partial class GuideViewModel : Abstraction.ViewModel
|
||||
.Select(category => new DownloadSummary(serviceProvider, category))
|
||||
.ToObservableCollection();
|
||||
|
||||
await Parallel.ForEachAsync(DownloadSummaries, async (summary, token) =>
|
||||
await Parallel.ForEachAsync([..DownloadSummaries], async (summary, token) =>
|
||||
{
|
||||
if (await summary.DownloadAndExtractAsync().ConfigureAwait(false))
|
||||
{
|
||||
|
||||
@@ -24,4 +24,19 @@ internal static class ResponseExtension
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryGetDataWithoutUINotification<TData>(this Response<TData> response, [NotNullWhen(true)] out TData? data)
|
||||
{
|
||||
if (response.ReturnCode == 0)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(response.Data);
|
||||
data = response.Data;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
data = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user