add handlers

This commit is contained in:
Lightczx
2024-01-05 17:29:30 +08:00
parent f2f858de15
commit 87ee81e7fa
26 changed files with 440 additions and 62 deletions

View File

@@ -190,7 +190,7 @@ internal sealed partial class Activation : IActivation
serviceProvider
.GetRequiredService<IDiscordService>()
.SetNormalActivity()
.SetNormalActivityAsync()
.SafeForget();
}

View File

@@ -870,14 +870,20 @@
<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="ServiceGameLaunchExecutionSetChannelOptionsGamePathNotValid" xml:space="preserve">
<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>

View File

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

View File

@@ -5,7 +5,7 @@ namespace Snap.Hutao.Service.Discord;
internal interface IDiscordService
{
ValueTask SetNormalActivity();
ValueTask SetNormalActivityAsync();
ValueTask SetPlayingActivity(bool isOversea);
ValueTask SetPlayingActivityAsync(bool isOversea);
}

View File

@@ -100,7 +100,7 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
}
/// <inheritdoc/>
public ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
public ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageConvertStatus> progress)
{
return gamePackageService.EnsureGameResourceAsync(launchScheme, progress);
}

View File

@@ -71,7 +71,7 @@ internal interface IGameServiceFacade
/// <param name="launchScheme">目标启动方案</param>
/// <param name="progress">进度</param>
/// <returns>是否替换成功</returns>
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageConvertStatus> progress);
/// <summary>
/// 修改注册表中的账号信息

View File

@@ -1,17 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.ViewModel.Game;
namespace Snap.Hutao.Service.Game.Launching;
internal sealed class LaunchExecutionContext
[ConstructorGenerated]
internal sealed partial class LaunchExecutionContext
{
private readonly ILogger<LaunchExecutionContext> logger;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly LaunchOptions options;
public LaunchExecutionResult Result { get; } = new();
public ILogger Logger { get; set; } = default!;
public IServiceProvider ServiceProvider { get => serviceProvider; }
public ITaskContext TaskContext { get => taskContext; }
public ILogger Logger { get => logger; }
public LaunchOptions Options { get => options; }
public IViewModelSupportLaunchExecution ViewModel { get; set; } = default!;
public LaunchScheme Scheme { get; set; } = default!;
public LaunchOptions Options { get; set; } = default!;
public GameAccount? Account { get; set; }
public IProgress<LaunchStatus> Progress { get; set; }
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Launching;
internal sealed class LaunchExecutionEnsureGameNotRunningHandler : ILaunchExecutionDelegateHandler
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
if (IsGameRunning())
{
context.Result.Kind = LaunchExecutionResultKind.GameProcessRunning;
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGameIsRunning;
return;
}
await next().ConfigureAwait(false);
}
private static bool IsGameRunning()
{
// GetProcesses once and manually loop is O(n)
foreach (ref readonly System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan())
{
if (string.Equals(process.ProcessName, GameConstants.YuanShenProcessName, StringComparison.OrdinalIgnoreCase) ||
string.Equals(process.ProcessName, GameConstants.GenshinImpactProcessName, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,130 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Win32.SafeHandles;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.PathAbstraction;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using Snap.Hutao.Web.Response;
using System.Collections.Immutable;
using System.IO;
namespace Snap.Hutao.Service.Game.Launching;
internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutionDelegateHandler
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
IServiceProvider serviceProvider = context.ServiceProvider;
IContentDialogFactory contentDialogFactory = serviceProvider.GetRequiredService<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;
}
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;
}
}
}

View File

@@ -0,0 +1,117 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Service.Discord;
using System.IO;
namespace Snap.Hutao.Service.Game.Launching;
internal sealed class LaunchExecutionGameProcessInitializationHandler : ILaunchExecutionDelegateHandler
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
if (!context.Options.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
{
context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
return;
}
context.Progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
using (System.Diagnostics.Process game = InitializeGameProcess(context, gamePath))
{
await next().ConfigureAwait(false);
// TODO: move to new handlers
await using (await GameRunningTracker.CreateAsync(this, isOversea).ConfigureAwait(false))
{
game.Start();
progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted));
if (launchOptions.UseStarwardPlayTimeStatistics)
{
await Starward.LaunchForPlayTimeStatisticsAsync(isOversea).ConfigureAwait(false);
}
if (runtimeOptions.IsElevated && launchOptions.IsAdvancedLaunchOptionsEnabled && launchOptions.UnlockFps)
{
progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
try
{
await UnlockFpsAsync(game, progress).ConfigureAwait(false);
}
catch (InvalidOperationException)
{
// The Unlocker can't unlock the process
game.Kill();
throw;
}
finally
{
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
}
}
else
{
progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit));
await game.WaitForExitAsync().ConfigureAwait(false);
progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
}
}
}
}
private static System.Diagnostics.Process InitializeGameProcess(LaunchExecutionContext context, string gamePath)
{
LaunchOptions launchOptions = context.Options;
string commandLine = string.Empty;
if (launchOptions.IsEnabled)
{
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
// https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html
commandLine = new CommandLineBuilder()
.AppendIf(launchOptions.IsBorderless, "-popupwindow")
.AppendIf(launchOptions.IsExclusive, "-window-mode", "exclusive")
.Append("-screen-fullscreen", launchOptions.IsFullScreen ? 1 : 0)
.AppendIf(launchOptions.IsScreenWidthEnabled, "-screen-width", launchOptions.ScreenWidth)
.AppendIf(launchOptions.IsScreenHeightEnabled, "-screen-height", launchOptions.ScreenHeight)
.AppendIf(launchOptions.IsMonitorEnabled, "-monitor", launchOptions.Monitor.Value)
.AppendIf(launchOptions.IsUseCloudThirdPartyMobile, "-platform_type CLOUD_THIRD_PARTY_MOBILE")
.ToString();
}
return new()
{
StartInfo = new()
{
Arguments = commandLine,
FileName = gamePath,
UseShellExecute = true,
Verb = "runas",
WorkingDirectory = Path.GetDirectoryName(gamePath),
},
};
}
}
internal sealed class LaunchExecutionSetDiscordActivityHandler : ILaunchExecutionDelegateHandler
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
IDiscordService discordService = context.ServiceProvider.GetRequiredService<IDiscordService>();
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);
}
}
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
namespace Snap.Hutao.Service.Game.Launching;
[Injection(InjectAs.Transient)]
@@ -11,25 +13,29 @@ internal sealed class LaunchExecutionInvoker
public LaunchExecutionInvoker()
{
handlers = [];
handlers.Enqueue(new LaunchExecutionEnsureGameNotRunningHandler());
handlers.Enqueue(new LaunchExecutionEnsureSchemeNotExistsHandler());
handlers.Enqueue(new LaunchExecutionSetChannelOptionsHandler());
handlers.Enqueue(new LaunchExecutionEnsureGameResourceHandler());
handlers.Enqueue(new LaunchExecutionSetGameAccountHandler());
handlers.Enqueue(new LaunchExecutionSetWindowsHDRHandler());
handlers.Enqueue(new LaunchExecutionStatusProgressHandler());
handlers.Enqueue(new LaunchExecutionGameProcessInitializationHandler());
handlers.Enqueue(new LaunchExecutionSetDiscordActivityHandler());
}
public async ValueTask<LaunchExecutionResult> LaunchAsync(LaunchExecutionContext context)
public async ValueTask<LaunchExecutionResult> InvokeAsync(LaunchExecutionContext context)
{
await ExecuteAsync(context).ConfigureAwait(false);
await InvokeHandlerAsync(context).ConfigureAwait(false);
return context.Result;
}
private async ValueTask<LaunchExecutionContext> ExecuteAsync(LaunchExecutionContext context)
private async ValueTask<LaunchExecutionContext> InvokeHandlerAsync(LaunchExecutionContext context)
{
if (handlers.TryDequeue(out ILaunchExecutionDelegateHandler? handler))
{
string handlerName = handler.GetType().Name;
context.Logger.LogInformation("Handler[{Handler}] begin execute", handlerName);
await handler.OnExecutionAsync(context, () => ExecuteAsync(context)).ConfigureAwait(false);
context.Logger.LogInformation("Handler[{Handler}] end execute", handlerName);
context.Logger.LogInformation("Handler[{Handler}] begin execution", TypeNameHelper.GetTypeDisplayName(handler));
await handler.OnExecutionAsync(context, () => InvokeHandlerAsync(context)).ConfigureAwait(false);
}
return context;

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Launching;
internal sealed class LaunchExecutionPackageConvertHandler : ILaunchExecutionDelegateHandler
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
}
}

View File

@@ -15,7 +15,12 @@ internal enum LaunchExecutionResultKind
Ok,
NoActiveScheme,
NoActiveGamePath,
GameProcessRunning,
GameConfigFileNotFound,
GameConfigDirectoryNotFound,
GameConfigUnauthorizedAccess,
GameConfigInsufficientPermissions,
GameDirectoryInsufficientPermissions,
GameResourceIndexQueryInvalidResponse,
GameResourcePackageConvertInternalError,
GameAccountRegistryWriteResultNotMatch,
}

View File

@@ -9,14 +9,12 @@ namespace Snap.Hutao.Service.Game.Launching;
internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecutionDelegateHandler
{
private const string ConfigFileName = "config.ini";
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
if (!context.Options.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath))
if (!context.Options.TryGetGamePathAndFilePathByName(GameConstants.ConfigFileName, out string gamePath, out string? configPath))
{
context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionSetChannelOptionsGamePathNotValid;
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
return;
}
@@ -42,7 +40,7 @@ internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecution
}
catch (UnauthorizedAccessException)
{
context.Result.Kind = LaunchExecutionResultKind.GameConfigUnauthorizedAccess;
context.Result.Kind = LaunchExecutionResultKind.GameConfigInsufficientPermissions;
context.Result.ErrorMessage = SH.ServiceGameSetMultiChannelUnauthorizedAccess;
return;
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Account;
namespace Snap.Hutao.Service.Game.Launching;
internal sealed class LaunchExecutionSetGameAccountHandler : ILaunchExecutionDelegateHandler
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
if (context.Account is not null && !RegistryInterop.Set(context.Account))
{
context.Result.Kind = LaunchExecutionResultKind.GameAccountRegistryWriteResultNotMatch;
context.Result.ErrorMessage = SH.ViewModelLaunchGameSwitchGameAccountFail;
return;
}
await next().ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Account;
namespace Snap.Hutao.Service.Game.Launching;
internal sealed class LaunchExecutionSetWindowsHDRHandler : ILaunchExecutionDelegateHandler
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
if (context.Options.IsWindowsHDREnabled)
{
RegistryInterop.SetWindowsHDR(context.Scheme.IsOversea);
}
await next().ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Factory.Progress;
namespace Snap.Hutao.Service.Game.Launching;
internal sealed class LaunchExecutionStatusProgressHandler : ILaunchExecutionDelegateHandler
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService<IProgressFactory>();
LaunchStatusOptions statusOptions = context.ServiceProvider.GetRequiredService<LaunchStatusOptions>();
context.Progress = progressFactory.CreateForMainThread<LaunchStatus>(status => statusOptions.LaunchStatus = status);
await next().ConfigureAwait(false);
}
}

View File

@@ -19,7 +19,7 @@ internal sealed partial class GamePackageService : IGamePackageService
private readonly LaunchOptions launchOptions;
private readonly ITaskContext taskContext;
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageConvertStatus> progress)
{
if (!launchOptions.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName))
{

View File

@@ -7,5 +7,5 @@ namespace Snap.Hutao.Service.Game.Package;
internal interface IGamePackageService
{
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageConvertStatus> progress);
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -167,7 +167,7 @@ internal sealed partial class GameProcessService : IGameProcessService
GameRunningTracker tracker = new(service, isOversea);
if (tracker.previousSetDiscordActivityWhenPlaying)
{
await service.discordService.SetPlayingActivity(isOversea).ConfigureAwait(false);
await service.discordService.SetPlayingActivityAsync(isOversea).ConfigureAwait(false);
}
return tracker;
@@ -177,7 +177,7 @@ internal sealed partial class GameProcessService : IGameProcessService
{
if (previousSetDiscordActivityWhenPlaying)
{
await service.discordService.SetNormalActivity().ConfigureAwait(false);
await service.discordService.SetNormalActivityAsync().ConfigureAwait(false);
}
service.isGameRunning = false;

View File

@@ -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>

View File

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

View File

@@ -218,7 +218,7 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel
gameService.SetChannelOptions(SelectedScheme);
LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
IProgress<PackageReplaceStatus> convertProgress = progressFactory.CreateForMainThread<PackageReplaceStatus>(state => dialog.State = state);
IProgress<PackageConvertStatus> convertProgress = progressFactory.CreateForMainThread<PackageConvertStatus>(state => dialog.State = state);
using (await dialog.BlockAsync(taskContext).ConfigureAwait(false))
{

View File

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