From c3b694c71c89113e20616793f75960f5a9d2eb7a Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Fri, 29 Nov 2024 16:41:14 +0800 Subject: [PATCH] fix server conversion error --- ...aunchExecutionEnsureGameResourceHandler.cs | 193 +++++++++++++----- .../Game/Launching/LaunchExecutionContext.cs | 15 +- .../Service/Game/Package/IPackageConverter.cs | 2 +- .../Service/Game/Package/PackageConverter.cs | 44 ++++ .../Game/Package/PackageConverterContext.cs | 10 - .../PackageConverterDeprecationContext.cs | 24 +++ .../Package/ScatteredFilesPackageConverter.cs | 34 +-- .../Package/SophonChunksPackageConverter.cs | 34 +-- .../Game/RestrictedGamePathAccessExtension.cs | 35 +++- .../ViewModel/Game/LaunchGameViewModel.cs | 2 +- ...iewModelSupportLaunchExecutionExtension.cs | 15 ++ 11 files changed, 269 insertions(+), 139 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterDeprecationContext.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/ViewModel/Game/ViewModelSupportLaunchExecutionExtension.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs index 6ea01343c4..d3a10c46af 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/Handler/LaunchExecutionEnsureGameResourceHandler.cs @@ -22,7 +22,7 @@ namespace Snap.Hutao.Service.Game.Launching.Handler; -internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutionDelegateHandler +internal sealed partial class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutionDelegateHandler { public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next) { @@ -31,7 +31,9 @@ public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchEx return; } - if (ShouldConvert(context, gameFileSystem)) + UnsafeRelaxedGameFileSystemReference reference = new(gameFileSystem); + + if (ShouldConvert(context, reference)) { IServiceProvider serviceProvider = context.ServiceProvider; IContentDialogFactory contentDialogFactory = serviceProvider.GetRequiredService(); @@ -43,24 +45,31 @@ public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchEx .GetRequiredService() .CreateForMainThread(state => dialog.State = state); - if (!await EnsureGameResourceAsync(context, gameFileSystem, convertProgress).ConfigureAwait(false)) + if (!await EnsureGameResourceAsync(context, reference, convertProgress).ConfigureAwait(false)) { // context.Result is set in EnsureGameResourceAsync return; } + // If EnsureGameResourceAsync succeeded, The GameFileSystem is no longer valid. + if (!context.TryGetGameFileSystem(out gameFileSystem)) + { + return; + } + // Backup config file, recover when an incompatible launcher deleted it. - context.ServiceProvider.GetRequiredService().Backup(gameFileSystem.GetGameConfigurationFilePath()); + context.ServiceProvider.GetRequiredService() + .Backup(gameFileSystem.GetGameConfigurationFilePath()); await context.TaskContext.SwitchToMainThreadAsync(); - context.UpdateGamePathEntry(); + context.PerformGamePathEntrySynchronization(); } } await next().ConfigureAwait(false); } - private static bool ShouldConvert(LaunchExecutionContext context, IGameFileSystem gameFileSystem) + private static bool ShouldConvert(LaunchExecutionContext context, UnsafeRelaxedGameFileSystemReference reference) { // Configuration file changed if (context.ChannelOptionsChanged) @@ -68,7 +77,7 @@ private static bool ShouldConvert(LaunchExecutionContext context, IGameFileSyste return true; } - if (context.TargetScheme.IsOversea ^ gameFileSystem.IsOversea()) + if (context.TargetScheme.IsOversea ^ reference.IsOversea()) { return true; } @@ -76,7 +85,7 @@ private static bool ShouldConvert(LaunchExecutionContext context, IGameFileSyste if (!context.TargetScheme.IsOversea) { // [It's Bilibili channel xor PCGameSDK.dll exists] means we need to convert - if (context.TargetScheme.Channel is ChannelType.Bili ^ File.Exists(gameFileSystem.GetPcGameSdkFilePath())) + if (context.TargetScheme.Channel is ChannelType.Bili ^ File.Exists(reference.GetPcGameSdkFilePath())) { return true; } @@ -85,9 +94,9 @@ private static bool ShouldConvert(LaunchExecutionContext context, IGameFileSyste return false; } - private static async ValueTask EnsureGameResourceAsync(LaunchExecutionContext context, IGameFileSystem gameFileSystem, IProgress progress) + private static async ValueTask EnsureGameResourceAsync(LaunchExecutionContext context, UnsafeRelaxedGameFileSystemReference reference, IProgress progress) { - string gameFolder = gameFileSystem.GetGameDirectory(); + string gameFolder = reference.GetGameDirectory(); context.Logger.LogInformation("Game folder: {GameFolder}", gameFolder); if (!CheckDirectoryPermissions(gameFolder)) @@ -101,64 +110,30 @@ private static async ValueTask EnsureGameResourceAsync(LaunchExecutionCont HoyoPlayClient hoyoPlayClient = context.ServiceProvider.GetRequiredService(); - Response sdkResponse = await hoyoPlayClient.GetChannelSDKAsync(context.TargetScheme).ConfigureAwait(false); - if (!ResponseValidator.TryValidateWithoutUINotification(sdkResponse, out GameChannelSDKsWrapper? channelSdks)) - { - context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; - context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(sdkResponse); - return false; - } - - Response deprecatedFileResponse = await hoyoPlayClient.GetDeprecatedFileConfigurationsAsync(context.TargetScheme).ConfigureAwait(false); - if (!ResponseValidator.TryValidateWithoutUINotification(deprecatedFileResponse, out DeprecatedFileConfigurationsWrapper? deprecatedFileConfigs)) - { - context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; - context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(deprecatedFileResponse); - return false; - } - IHttpClientFactory httpClientFactory = context.ServiceProvider.GetRequiredService(); using (HttpClient httpClient = httpClientFactory.CreateClient(GamePackageService.HttpClientName)) { - PackageConverterType type = context.ServiceProvider.GetRequiredService().PackageConverterType; - - PackageConverterContext.CommonReferences common = new( - httpClient, - context.CurrentScheme, - context.TargetScheme, - gameFileSystem, - channelSdks.GameChannelSDKs.SingleOrDefault(), - deprecatedFileConfigs.DeprecatedFileConfigurations.SingleOrDefault(), - progress); - + PackageConverterContext.CommonReferences common = new(httpClient, context.CurrentScheme, context.TargetScheme, reference, progress); PackageConverterContext packageConverterContext; + PackageConverterType type = context.ServiceProvider.GetRequiredService().PackageConverterType; switch (type) { case PackageConverterType.ScatteredFiles: - Response packagesResponse = await hoyoPlayClient.GetPackagesAsync(context.TargetScheme).ConfigureAwait(false); - if (!ResponseValidator.TryValidateWithoutUINotification(packagesResponse, out GamePackagesWrapper? gamePackages)) + if (await TryGetPackagesAsync(hoyoPlayClient, context).ConfigureAwait(false) is not (true, { } gamePackages)) { - context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; - context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(packagesResponse); return false; } packageConverterContext = new(common, gamePackages.GamePackages.Single()); break; case PackageConverterType.SophonChunks: - Response currentBranchesResponse = await hoyoPlayClient.GetBranchesAsync(context.CurrentScheme).ConfigureAwait(false); - if (!ResponseValidator.TryValidateWithoutUINotification(currentBranchesResponse, out GameBranchesWrapper? currentBranches)) + if (await TryGetCurrentBranchesAsync(hoyoPlayClient, context).ConfigureAwait(false) is not (true, { } currentBranches)) { - context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; - context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(currentBranchesResponse); return false; } - Response targetBranchesResponse = await hoyoPlayClient.GetBranchesAsync(context.TargetScheme).ConfigureAwait(false); - if (!ResponseValidator.TryValidateWithoutUINotification(targetBranchesResponse, out GameBranchesWrapper? targetBranches)) + if (await TryGetTargetBranchesAsync(hoyoPlayClient, context).ConfigureAwait(false) is not (true, { } targetBranches)) { - context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; - context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(targetBranchesResponse); return false; } @@ -173,7 +148,7 @@ private static async ValueTask EnsureGameResourceAsync(LaunchExecutionCont IPackageConverter packageConverter = context.ServiceProvider.GetRequiredKeyedService(type); - if (context.TargetScheme.IsOversea ^ gameFileSystem.IsOversea()) + if (context.TargetScheme.IsOversea ^ reference.IsOversea()) { if (!await packageConverter.EnsureGameResourceAsync(packageConverterContext).ConfigureAwait(false)) { @@ -182,18 +157,101 @@ private static async ValueTask EnsureGameResourceAsync(LaunchExecutionCont return false; } - // We need to change the gamePath if we switched. + // The GameFileSystem no longer valid, and meanwhile we need to change the gamePath. string executableName = context.TargetScheme.IsOversea ? GameConstants.GenshinImpactFileName : GameConstants.YuanShenFileName; await context.TaskContext.SwitchToMainThreadAsync(); - context.Options.UpdateGamePath(Path.Combine(gameFolder, executableName)); + context.UpdateGamePath(Path.Combine(gameFolder, executableName)); + + if (!context.TryGetGameFileSystem(out IGameFileSystem? newValue)) + { + return false; + } + + reference.Value = newValue; + } + + if (await TryGetChannelSdkAsync(hoyoPlayClient, context).ConfigureAwait(false) is not (true, { } channelSdks)) + { + return false; } - await packageConverter.EnsureDeprecatedFilesAndSdkAsync(packageConverterContext).ConfigureAwait(false); + if (await TryGetDeprecatedFileConfigurationsAsync(hoyoPlayClient, context).ConfigureAwait(false) is not (true, { } deprecatedFileConfigs)) + { + return false; + } + + PackageConverterDeprecationContext deprecationContext = new(httpClient, reference, channelSdks.GameChannelSDKs.SingleOrDefault(), deprecatedFileConfigs.DeprecatedFileConfigurations.SingleOrDefault()); + await packageConverter.EnsureDeprecatedFilesAndSdkAsync(deprecationContext).ConfigureAwait(false); return true; } } + private static async ValueTask> TryGetChannelSdkAsync(HoyoPlayClient hoyoPlayClient, LaunchExecutionContext context) + { + Response response = await hoyoPlayClient.GetChannelSDKAsync(context.TargetScheme).ConfigureAwait(false); + if (!ResponseValidator.TryValidateWithoutUINotification(response, out GameChannelSDKsWrapper? data)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; + context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(response); + return new(false, default!); + } + + return new(true, data); + } + + private static async ValueTask> TryGetDeprecatedFileConfigurationsAsync(HoyoPlayClient hoyoPlayClient, LaunchExecutionContext context) + { + Response response = await hoyoPlayClient.GetDeprecatedFileConfigurationsAsync(context.TargetScheme).ConfigureAwait(false); + if (!ResponseValidator.TryValidateWithoutUINotification(response, out DeprecatedFileConfigurationsWrapper? data)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; + context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(response); + return new(false, default!); + } + + return new(true, data); + } + + private static async ValueTask> TryGetPackagesAsync(HoyoPlayClient hoyoPlayClient, LaunchExecutionContext context) + { + Response response = await hoyoPlayClient.GetPackagesAsync(context.TargetScheme).ConfigureAwait(false); + if (!ResponseValidator.TryValidateWithoutUINotification(response, out GamePackagesWrapper? data)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; + context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(response); + return new(false, default!); + } + + return new(true, data); + } + + private static async ValueTask> TryGetCurrentBranchesAsync(HoyoPlayClient hoyoPlayClient, LaunchExecutionContext context) + { + Response response = await hoyoPlayClient.GetBranchesAsync(context.CurrentScheme).ConfigureAwait(false); + if (!ResponseValidator.TryValidateWithoutUINotification(response, out GameBranchesWrapper? data)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; + context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(response); + return new(false, default!); + } + + return new(true, data); + } + + private static async ValueTask> TryGetTargetBranchesAsync(HoyoPlayClient hoyoPlayClient, LaunchExecutionContext context) + { + Response response = await hoyoPlayClient.GetBranchesAsync(context.TargetScheme).ConfigureAwait(false); + if (!ResponseValidator.TryValidateWithoutUINotification(response, out GameBranchesWrapper? data)) + { + context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse; + context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(response); + return new(false, default!); + } + + return new(true, data); + } + private static bool CheckDirectoryPermissions(string folder) { if (LocalSetting.Get(SettingKeys.OverridePackageConvertDirectoryPermissionsRequirement, false)) @@ -227,4 +285,33 @@ private static bool CheckDirectoryPermissions(string folder) return false; } } + + private sealed partial class UnsafeRelaxedGameFileSystemReference : IGameFileSystem + { + public UnsafeRelaxedGameFileSystemReference(IGameFileSystem value) + { + Value = value; + } + + public IGameFileSystem Value + { + private get; + set + { + field.Dispose(); + field = value; + } + } + + public string GameFilePath { get => Value.GameFilePath; } + + public GameAudioSystem Audio { get => Value.Audio; } + + public bool IsDisposed { get => Value.IsDisposed; } + + public void Dispose() + { + Value.Dispose(); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs index 8f8f86508a..ac88b69ff0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/LaunchExecutionContext.cs @@ -77,14 +77,23 @@ public bool TryGetGameFileSystem([NotNullWhen(true)] out IGameFileSystem? gameFi return true; } - public void UpdateGamePathEntry() + public void PerformGamePathEntrySynchronization() { // Invalidate game file system gameFileSystem?.Dispose(); gameFileSystem = null; - ImmutableArray gamePathEntries = Options.GetGamePathEntries(out GamePathEntry? selectedEntry); - ViewModel.SetGamePathEntriesAndSelectedGamePathEntry(gamePathEntries, selectedEntry); + ViewModel.SetGamePathEntriesAndSelectedGamePathEntry(Options); + } + + public void UpdateGamePath(string gamePath) + { + // Invalidate game file system + gameFileSystem?.Dispose(); + gameFileSystem = null; + + Options.GamePath = gamePath; + PerformGamePathEntrySynchronization(); } public void Dispose() diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IPackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IPackageConverter.cs index 655a1f1648..225d0e5120 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IPackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/IPackageConverter.cs @@ -7,5 +7,5 @@ internal interface IPackageConverter { ValueTask EnsureGameResourceAsync(PackageConverterContext context); - ValueTask EnsureDeprecatedFilesAndSdkAsync(PackageConverterContext context); + ValueTask EnsureDeprecatedFilesAndSdkAsync(PackageConverterDeprecationContext context); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs new file mode 100644 index 0000000000..5b8e568a6a --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -0,0 +1,44 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.IO; +using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile; +using System.IO; +using System.IO.Compression; + +namespace Snap.Hutao.Service.Game.Package; + +internal abstract class PackageConverter : IPackageConverter +{ + public abstract ValueTask EnsureGameResourceAsync(PackageConverterContext context); + + public virtual async ValueTask EnsureDeprecatedFilesAndSdkAsync(PackageConverterDeprecationContext context) + { + // Just try to delete these files, always download from server when needed + string gameDirectory = context.GameFileSystem.GetGameDirectory(); + FileOperation.Delete(Path.Combine(gameDirectory, GameConstants.YuanShenData, "Plugins\\PCGameSDK.dll")); + FileOperation.Delete(Path.Combine(gameDirectory, GameConstants.GenshinImpactData, "Plugins\\PCGameSDK.dll")); + FileOperation.Delete(Path.Combine(gameDirectory, GameConstants.YuanShenData, "Plugins\\EOSSDK-Win64-Shipping.dll")); + FileOperation.Delete(Path.Combine(gameDirectory, GameConstants.GenshinImpactData, "Plugins\\EOSSDK-Win64-Shipping.dll")); + FileOperation.Delete(Path.Combine(gameDirectory, GameConstants.YuanShenData, "Plugins\\PluginEOSSDK.dll")); + FileOperation.Delete(Path.Combine(gameDirectory, GameConstants.GenshinImpactData, "Plugins\\PluginEOSSDK.dll")); + FileOperation.Delete(Path.Combine(gameDirectory, "sdk_pkg_version")); + + if (context.GameChannelSdk is not null) + { + using (Stream sdkWebStream = await context.HttpClient.GetStreamAsync(context.GameChannelSdk.ChannelSdkPackage.Url).ConfigureAwait(false)) + { + ZipFile.ExtractToDirectory(sdkWebStream, gameDirectory, true); + } + } + + if (context.DeprecatedFiles is not null) + { + foreach (DeprecatedFile file in context.DeprecatedFiles.DeprecatedFiles) + { + string filePath = Path.Combine(gameDirectory, file.Name); + FileOperation.Move(filePath, $"{filePath}.backup", true); + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterContext.cs index 2f4554317c..4439e2ca3b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterContext.cs @@ -86,10 +86,6 @@ private PackageConverterContext(CommonReferences common) public IGameFileSystem GameFileSystem { get => Common.GameFileSystem; } - public GameChannelSDK? GameChannelSDK { get => Common.GameChannelSDK; } - - public DeprecatedFilesWrapper? DeprecatedFiles { get => Common.DeprecatedFiles; } - public IProgress Progress { get => Common.Progress; } public readonly string GetScatteredFilesUrl(string file) @@ -124,8 +120,6 @@ internal readonly struct CommonReferences public readonly LaunchScheme CurrentScheme; public readonly LaunchScheme TargetScheme; public readonly IGameFileSystem GameFileSystem; - public readonly GameChannelSDK? GameChannelSDK; - public readonly DeprecatedFilesWrapper? DeprecatedFiles; public readonly IProgress Progress; public CommonReferences( @@ -133,16 +127,12 @@ public CommonReferences( LaunchScheme currentScheme, LaunchScheme targetScheme, IGameFileSystem gameFileSystem, - GameChannelSDK? gameChannelSDK, - DeprecatedFilesWrapper? deprecatedFiles, IProgress progress) { HttpClient = httpClient; CurrentScheme = currentScheme; TargetScheme = targetScheme; GameFileSystem = gameFileSystem; - GameChannelSDK = gameChannelSDK; - DeprecatedFiles = deprecatedFiles; Progress = progress; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterDeprecationContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterDeprecationContext.cs new file mode 100644 index 0000000000..1ad913a92d --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterDeprecationContext.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK; +using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile; +using System.Net.Http; + +namespace Snap.Hutao.Service.Game.Package; + +internal readonly struct PackageConverterDeprecationContext +{ + public readonly HttpClient HttpClient; + public readonly IGameFileSystem GameFileSystem; + public readonly GameChannelSDK? GameChannelSdk; + public readonly DeprecatedFilesWrapper? DeprecatedFiles; + + public PackageConverterDeprecationContext(HttpClient httpClient, IGameFileSystem gameFileSystem, GameChannelSDK? gameChannelSdk, DeprecatedFilesWrapper? deprecatedFiles) + { + HttpClient = httpClient; + GameFileSystem = gameFileSystem; + GameChannelSdk = gameChannelSdk; + DeprecatedFiles = deprecatedFiles; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ScatteredFilesPackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ScatteredFilesPackageConverter.cs index 4ccddcb559..66f534e9ad 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ScatteredFilesPackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ScatteredFilesPackageConverter.cs @@ -19,7 +19,7 @@ namespace Snap.Hutao.Service.Game.Package; [ConstructorGenerated] [Injection(InjectAs.Transient, typeof(IPackageConverter), Key = PackageConverterType.ScatteredFiles)] -internal sealed partial class ScatteredFilesPackageConverter : IPackageConverter +internal sealed partial class ScatteredFilesPackageConverter : PackageConverter { private const string PackageVersion = "pkg_version"; @@ -29,7 +29,7 @@ internal sealed partial class ScatteredFilesPackageConverter : IPackageConverter [GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")] private static partial Regex DataFolderRegex { get; } - public async ValueTask EnsureGameResourceAsync(PackageConverterContext context) + public override async ValueTask EnsureGameResourceAsync(PackageConverterContext context) { // 以 国服 -> 国际服 为例 // 1. 下载国际服的 pkg_version 文件,转换为索引字典 @@ -67,36 +67,6 @@ public async ValueTask EnsureGameResourceAsync(PackageConverterContext con return await ReplaceGameResourceAsync(context, diffOperations).ConfigureAwait(false); } - public async ValueTask EnsureDeprecatedFilesAndSdkAsync(PackageConverterContext context) - { - // Just try to delete these files, always download from server when needed - string gameDirectory = context.GameFileSystem.GetGameDirectory(); - FileOperation.Delete(Path.Combine(gameDirectory, YuanShenData, "Plugins\\PCGameSDK.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, GenshinImpactData, "Plugins\\PCGameSDK.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, YuanShenData, "Plugins\\EOSSDK-Win64-Shipping.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, GenshinImpactData, "Plugins\\EOSSDK-Win64-Shipping.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, YuanShenData, "Plugins\\PluginEOSSDK.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, GenshinImpactData, "Plugins\\PluginEOSSDK.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, "sdk_pkg_version")); - - if (context.GameChannelSDK is not null) - { - using (Stream sdkWebStream = await context.HttpClient.GetStreamAsync(context.GameChannelSDK.ChannelSdkPackage.Url).ConfigureAwait(false)) - { - ZipFile.ExtractToDirectory(sdkWebStream, gameDirectory, true); - } - } - - if (context.DeprecatedFiles is not null) - { - foreach (DeprecatedFile file in context.DeprecatedFiles.DeprecatedFiles) - { - string filePath = Path.Combine(gameDirectory, file.Name); - FileOperation.Move(filePath, $"{filePath}.backup", true); - } - } - } - private static IEnumerable GetItemOperationInfos(RelativePathVersionItemDictionary remote, RelativePathVersionItemDictionary local) { foreach ((string remoteName, VersionItem remoteItem) in remote) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/SophonChunksPackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/SophonChunksPackageConverter.cs index c67c12f400..8bc2ae4545 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/SophonChunksPackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/SophonChunksPackageConverter.cs @@ -26,13 +26,13 @@ namespace Snap.Hutao.Service.Game.Package; [ConstructorGenerated] [Injection(InjectAs.Transient, typeof(IPackageConverter), Key = PackageConverterType.SophonChunks)] -internal sealed partial class SophonChunksPackageConverter : IPackageConverter +internal sealed partial class SophonChunksPackageConverter : PackageConverter { private readonly IMemoryStreamFactory memoryStreamFactory; private readonly ILogger logger; private readonly IServiceProvider serviceProvider; - public async ValueTask EnsureGameResourceAsync(PackageConverterContext context) + public override async ValueTask EnsureGameResourceAsync(PackageConverterContext context) { // 基本步骤与 ScatteredPackageConverter 相同 // 以 国服 -> 国际服 为例 @@ -75,36 +75,6 @@ public async ValueTask EnsureGameResourceAsync(PackageConverterContext con return ReplaceGameResource(context, diffOperations); } - public async ValueTask EnsureDeprecatedFilesAndSdkAsync(PackageConverterContext context) - { - // Just try to delete these files, always download from server when needed - string gameDirectory = context.GameFileSystem.GetGameDirectory(); - FileOperation.Delete(Path.Combine(gameDirectory, YuanShenData, "Plugins\\PCGameSDK.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, GenshinImpactData, "Plugins\\PCGameSDK.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, YuanShenData, "Plugins\\EOSSDK-Win64-Shipping.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, GenshinImpactData, "Plugins\\EOSSDK-Win64-Shipping.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, YuanShenData, "Plugins\\PluginEOSSDK.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, GenshinImpactData, "Plugins\\PluginEOSSDK.dll")); - FileOperation.Delete(Path.Combine(gameDirectory, "sdk_pkg_version")); - - if (context.GameChannelSDK is not null) - { - using (Stream sdkWebStream = await context.HttpClient.GetStreamAsync(context.GameChannelSDK.ChannelSdkPackage.Url).ConfigureAwait(false)) - { - ZipFile.ExtractToDirectory(sdkWebStream, gameDirectory, true); - } - } - - if (context.DeprecatedFiles is not null) - { - foreach (DeprecatedFile file in context.DeprecatedFiles.DeprecatedFiles) - { - string filePath = Path.Combine(gameDirectory, file.Name); - FileOperation.Move(filePath, $"{filePath}.backup", true); - } - } - } - private static IEnumerable GetDiffOperations(SophonDecodedBuild currentDecodedBuild, SophonDecodedBuild targetDecodedBuild) { foreach ((SophonDecodedManifest currentManifest, SophonDecodedManifest targetManifest) in currentDecodedBuild.Manifests.Zip(targetDecodedBuild.Manifests)) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/RestrictedGamePathAccessExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/RestrictedGamePathAccessExtension.cs index 2041616b2f..ac6a748cfe 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/RestrictedGamePathAccessExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/RestrictedGamePathAccessExtension.cs @@ -29,31 +29,45 @@ public static bool TryGetGameFileSystem(this IRestrictedGamePathAccess access, [ return true; } - public static ImmutableArray GetGamePathEntries(this IRestrictedGamePathAccess access, out GamePathEntry? selected) + public static ImmutableArray PerformGamePathEntrySynchronization(this IRestrictedGamePathAccess access, out GamePathEntry? selected) { string gamePath = access.GamePath; + // The game path is null or empty, this means no game path is selected, just return the entries. if (string.IsNullOrEmpty(gamePath)) { selected = default; return access.GamePathEntries; } - if (access.GamePathEntries.SingleOrDefault(entry => string.Equals(entry.Path, access.GamePath, StringComparison.OrdinalIgnoreCase)) is { } existed) + // The game path is in the entries, just return the entries. + if (access.GamePathEntries.SingleOrDefault(entry => string.Equals(entry.Path, gamePath, StringComparison.OrdinalIgnoreCase)) is { } existed) { selected = existed; return access.GamePathEntries; } - selected = GamePathEntry.Create(access.GamePath); - return access.GamePathEntries = access.GamePathEntries.Add(selected); + // We need update the entries when game path not in the entries. + if (!access.GamePathLock.TryWriterLock(out AsyncReaderWriterLock.Releaser releaser)) + { + throw HutaoException.InvalidOperation("Cannot get game path entries while it is being used."); + } + + using (releaser) + { + // The game path is not in the entries, add it to the entries. + selected = GamePathEntry.Create(access.GamePath); + return access.GamePathEntries = access.GamePathEntries.Add(selected); + } } public static ImmutableArray RemoveGamePathEntry(this IRestrictedGamePathAccess access, GamePathEntry? entry, out GamePathEntry? selected) { + // Although normally this should not happen, we still handle it for compatibility. if (entry is null) { - return access.GetGamePathEntries(out selected); + // Removes no entry, just return the entries. + return access.PerformGamePathEntrySynchronization(out selected); } if (!access.GamePathLock.TryWriterLock(out AsyncReaderWriterLock.Releaser releaser)) @@ -63,14 +77,18 @@ public static ImmutableArray RemoveGamePathEntry(this IRestricted using (releaser) { + // Clear game path if it's selected. if (string.Equals(access.GamePath, entry.Path, StringComparison.OrdinalIgnoreCase)) { access.GamePath = string.Empty; } access.GamePathEntries = access.GamePathEntries.Remove(entry); - return access.GetGamePathEntries(out selected); } + + // Synchronization takes write lock when game path changed, + // so we release the write lock before calling. + return access.PerformGamePathEntrySynchronization(out selected); } public static ImmutableArray UpdateGamePath(this IRestrictedGamePathAccess access, string gamePath) @@ -83,7 +101,10 @@ public static ImmutableArray UpdateGamePath(this IRestrictedGameP using (releaser) { access.GamePath = gamePath; - return access.GetGamePathEntries(out _); } + + // Synchronization takes write lock when game path changed, + // so we release the write lock before calling. + return access.PerformGamePathEntrySynchronization(out _); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs index 4e8afcabc1..bee25e5b12 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -178,7 +178,7 @@ public async ValueTask ReceiveAsync(INavigationExtraData data) protected override ValueTask LoadOverrideAsync() { - SetGamePathEntriesAndSelectedGamePathEntry(LaunchOptions.GetGamePathEntries(out GamePathEntry? entry), entry); + this.SetGamePathEntriesAndSelectedGamePathEntry(LaunchOptions); return ValueTask.FromResult(true); } diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/ViewModelSupportLaunchExecutionExtension.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/ViewModelSupportLaunchExecutionExtension.cs new file mode 100644 index 0000000000..420c25e584 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/ViewModelSupportLaunchExecutionExtension.cs @@ -0,0 +1,15 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.PathAbstraction; + +namespace Snap.Hutao.ViewModel.Game; + +internal static class ViewModelSupportLaunchExecutionExtension +{ + public static void SetGamePathEntriesAndSelectedGamePathEntry(this IViewModelSupportLaunchExecution viewModel, IRestrictedGamePathAccess access) + { + viewModel.SetGamePathEntriesAndSelectedGamePathEntry(access.PerformGamePathEntrySynchronization(out GamePathEntry? selected), selected); + } +} \ No newline at end of file