From 9fdedd78d06367674bc19a7262c0f3c61ed3f2d6 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Thu, 28 Dec 2023 17:06:45 +0800 Subject: [PATCH] refactor Launch Game Pipeline --- .../Snap.Hutao/Resource/Localization/SH.resx | 3 + .../Game/Account/GameAccountService.cs | 95 +++++----- .../Game/Account/IGameAccountService.cs | 6 +- .../Service/Game/Account/RegistryInterop.cs | 16 +- .../Game/Configuration/ChannelOptions.cs | 42 ++--- .../GameChannelOptionsService.cs | 23 ++- .../Snap.Hutao/Service/Game/GameConstants.cs | 29 +-- .../Service/Game/GameServiceFacade.cs | 5 +- .../Game/GameServiceFacadeExtension.cs | 20 ++ .../Service/Game/IGameServiceFacade.cs | 5 +- .../Service/Game/LaunchOptionsExtension.cs | 31 ++- .../Game/Locator/GameLocatorFactory.cs | 13 +- .../Locator/GameLocatorFactoryExtensions.cs | 12 ++ .../Service/Game/Locator/ManualGameLocator.cs | 4 +- .../Game/Locator/RegistryLauncherLocator.cs | 60 +++--- .../Game/Locator/UnityLogGameLocator.cs | 2 +- .../Game/Package/GamePackageService.cs | 5 +- .../Service/Game/Package/PackageConverter.cs | 51 ++--- ...s => PackageConverterFileSystemContext.cs} | 7 +- ...ionInfo.cs => PackageItemOperationInfo.cs} | 6 +- ...ionType.cs => PackageItemOperationType.cs} | 2 +- .../Game/Package/PackageReplaceStatus.cs | 22 +-- .../Game/PathAbstraction/GamePathService.cs | 22 +-- .../Game/Process/GameProcessService.cs | 4 +- .../Service/Game/Scheme/LaunchScheme.cs | 8 +- .../Service/Game/Unlocker/GameFpsUnlocker.cs | 4 +- .../Snap.Hutao/View/Card/LaunchGameCard.xaml | 2 +- .../Snap.Hutao/View/TitleView.xaml.cs | 1 - .../ViewModel/Game/GameAccountFilter.cs | 27 +++ .../ViewModel/Game/LaunchGameShared.cs | 39 ++++ .../ViewModel/Game/LaunchGameViewModel.cs | 177 +++++++++--------- .../ViewModel/Game/LaunchGameViewModelSlim.cs | 53 +++--- .../ViewModel/Setting/SettingViewModel.cs | 1 - 33 files changed, 423 insertions(+), 374 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacadeExtension.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactoryExtensions.cs rename src/Snap.Hutao/Snap.Hutao/Service/Game/Package/{PackageConvertContext.cs => PackageConverterFileSystemContext.cs} (85%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/Package/{ItemOperationInfo.cs => PackageItemOperationInfo.cs} (80%) rename src/Snap.Hutao/Snap.Hutao/Service/Game/Package/{ItemOperationType.cs => PackageItemOperationType.cs} (91%) create mode 100644 src/Snap.Hutao/Snap.Hutao/ViewModel/Game/GameAccountFilter.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameShared.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index d0dde46f..d35496b0 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1553,6 +1553,9 @@ 切换账号失败 + + 无法选择UID [{0}] 对应的账号 [{1}],该账号不属于当前服务器 + 操作完成 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs index 3079fd3d..5c6fd327 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/GameAccountService.cs @@ -5,7 +5,6 @@ using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Primitive; -using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.View.Dialog; using System.Collections.ObjectModel; @@ -26,68 +25,51 @@ internal sealed partial class GameAccountService : IGameAccountService get => gameAccounts ??= gameDbService.GetGameAccountCollection(); } - public async ValueTask DetectGameAccountAsync(LaunchScheme scheme) + public async ValueTask DetectGameAccountAsync(SchemeType schemeType) { ArgumentNullException.ThrowIfNull(gameAccounts); - SchemeType schemeType = scheme.GetSchemeType(); string? registrySdk = RegistryInterop.Get(schemeType); - if (!string.IsNullOrEmpty(registrySdk)) + if (string.IsNullOrEmpty(registrySdk)) { - GameAccount? account = null; - try - { - account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); - } - catch (InvalidOperationException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); - } - - if (account is null) - { - LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false); - - if (isOk) - { - account = GameAccount.From(name, registrySdk, schemeType); - - // sync database - await taskContext.SwitchToBackgroundAsync(); - await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false); - - // sync cache - await taskContext.SwitchToMainThreadAsync(); - gameAccounts.Add(account); - } - } - - return account; + return default; } - return default; + GameAccount? account = SingleGameAccountOrDefault(gameAccounts, registrySdk); + if (account is null) + { + LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false); + + if (isOk) + { + account = GameAccount.From(name, registrySdk, schemeType); + + // sync database + await taskContext.SwitchToBackgroundAsync(); + await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false); + + // sync cache + await taskContext.SwitchToMainThreadAsync(); + gameAccounts.Add(account); + } + } + + return account; } - public GameAccount? DetectCurrentGameAccount(LaunchScheme scheme) + public GameAccount? DetectCurrentGameAccount(SchemeType schemeType) { ArgumentNullException.ThrowIfNull(gameAccounts); - string? registrySdk = RegistryInterop.Get(scheme.GetSchemeType()); + string? registrySdk = RegistryInterop.Get(schemeType); - if (!string.IsNullOrEmpty(registrySdk)) + if (string.IsNullOrEmpty(registrySdk)) { - try - { - return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); - } - catch (InvalidOperationException ex) - { - ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); - } + return default; } - return null; + return SingleGameAccountOrDefault(gameAccounts, registrySdk); } public bool SetGameAccount(GameAccount account) @@ -103,12 +85,12 @@ internal sealed partial class GameAccountService : IGameAccountService public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount) { - await taskContext.SwitchToMainThreadAsync(); LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true); + (bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false); if (isOk) { + await taskContext.SwitchToMainThreadAsync(); gameAccount.UpdateName(name); // sync database @@ -119,11 +101,24 @@ internal sealed partial class GameAccountService : IGameAccountService public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount) { - await taskContext.SwitchToMainThreadAsync(); ArgumentNullException.ThrowIfNull(gameAccounts); + + await taskContext.SwitchToMainThreadAsync(); gameAccounts.Remove(gameAccount); await taskContext.SwitchToBackgroundAsync(); await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false); } + + private static GameAccount? SingleGameAccountOrDefault(ObservableCollection gameAccounts, string registrySdk) + { + try + { + return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk); + } + catch (InvalidOperationException ex) + { + throw ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex); + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs index 45a2e459..eaf88f1f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/IGameAccountService.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Entity; -using Snap.Hutao.Service.Game.Scheme; +using Snap.Hutao.Model.Entity.Primitive; using System.Collections.ObjectModel; namespace Snap.Hutao.Service.Game.Account; @@ -13,9 +13,9 @@ internal interface IGameAccountService void AttachGameAccountToUid(GameAccount gameAccount, string uid); - GameAccount? DetectCurrentGameAccount(LaunchScheme scheme); + GameAccount? DetectCurrentGameAccount(SchemeType schemeType); - ValueTask DetectGameAccountAsync(LaunchScheme scheme); + ValueTask DetectGameAccountAsync(SchemeType schemeType); ValueTask ModifyGameAccountAsync(GameAccount gameAccount); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs index 46f84c35..8cb125e2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Account/RegistryInterop.cs @@ -43,17 +43,17 @@ internal static class RegistryInterop (string keyName, string valueName) = GetKeyValueName(scheme); object? sdk = Registry.GetValue(keyName, valueName, Array.Empty()); - if (sdk is byte[] bytes) + if (sdk is not byte[] bytes) { - fixed (byte* pByte = bytes) - { - // 从注册表获取的字节数组带有 '\0' 结尾,需要舍去 - ReadOnlySpan span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte); - return Encoding.UTF8.GetString(span); - } + return null; } - return null; + fixed (byte* pByte = bytes) + { + // 从注册表获取的字节数组带有 '\0' 结尾,需要舍去 + ReadOnlySpan span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte); + return Encoding.UTF8.GetString(span); + } } private static (string KeyName, string ValueName) GetKeyValueName(SchemeType scheme) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/ChannelOptions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/ChannelOptions.cs index 7661b3f6..db401c8b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/ChannelOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/ChannelOptions.cs @@ -34,21 +34,6 @@ internal readonly struct ChannelOptions /// public readonly string? ConfigFilePath; - /// - /// 构造一个新的多通道 - /// - /// 通道 - /// 子通道 - /// 是否为国际服 - /// 配置文件路径 - public ChannelOptions(string? channel, string? subChannel, bool isOversea, string? configFilePath = null) - { - _ = Enum.TryParse(channel, out Channel); - _ = Enum.TryParse(subChannel, out SubChannel); - IsOversea = isOversea; - ConfigFilePath = configFilePath; - } - public ChannelOptions(ChannelType channel, SubChannelType subChannel, bool isOversea) { Channel = channel; @@ -56,24 +41,33 @@ internal readonly struct ChannelOptions IsOversea = isOversea; } - /// - /// 配置文件未找到 - /// - /// 是否为国际服 - /// 配置文件期望路径 - /// 选项 + public ChannelOptions(string? channel, string? subChannel, bool isOversea) + { + _ = Enum.TryParse(channel, out Channel); + _ = Enum.TryParse(subChannel, out SubChannel); + IsOversea = isOversea; + } + + private ChannelOptions(bool isOversea, string? configFilePath) + { + IsOversea = isOversea; + ConfigFilePath = configFilePath; + } + public static ChannelOptions FileNotFound(bool isOversea, string configFilePath) { - return new(null, null, isOversea, configFilePath); + return new(isOversea, configFilePath); } /// public override string ToString() { - return $"[ChannelType:{Channel}] [SubChannel:{SubChannel}] [IsOversea: {IsOversea}]"; + return $$""" + { ChannelType: {{Channel}}, SubChannel: {{SubChannel}}, IsOversea: {{IsOversea}}} + """; } - // DO NOT DELETE used in HashSet + // DO NOT DELETE, used in HashSet public override int GetHashCode() { return HashCode.Combine(Channel, SubChannel, IsOversea); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs index 2b63c89a..92e34f62 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Configuration/GameChannelOptionsService.cs @@ -17,9 +17,12 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer public ChannelOptions GetChannelOptions() { - string gamePath = launchOptions.GamePath; - string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName); - bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase); + if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath)) + { + throw ThrowHelper.InvalidOperation($"Invalid game path: {gamePath}"); + } + + bool isOversea = LaunchScheme.ExecutableIsOversea(Path.GetFileName(gamePath)); if (!File.Exists(configPath)) { @@ -38,10 +41,10 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer public bool SetChannelOptions(LaunchScheme scheme) { - string gamePath = launchOptions.GamePath; - string? directory = Path.GetDirectoryName(gamePath); - ArgumentException.ThrowIfNullOrEmpty(directory); - string configPath = Path.Combine(directory, ConfigFileName); + if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath)) + { + return false; + } List elements = default!; try @@ -70,14 +73,16 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer { if (element is IniParameter parameter) { - if (parameter.Key == "channel") + if (parameter.Key is ChannelOptions.ChannelName) { changed = parameter.Set(scheme.Channel.ToString("D")) || changed; + continue; } - if (parameter.Key == "sub_channel") + if (parameter.Key is ChannelOptions.SubChannelName) { changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed; + continue; } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameConstants.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameConstants.cs index 6457e500..c910fe12 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameConstants.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameConstants.cs @@ -9,38 +9,13 @@ namespace Snap.Hutao.Service.Game; [HighQuality] internal static class GameConstants { - /// - /// 设置文件 - /// public const string ConfigFileName = "config.ini"; - - /// - /// 国服文件名 - /// public const string YuanShenFileName = "YuanShen.exe"; - - /// - /// 外服文件名 - /// + public const string YuanShenFileNameUpper = "YUANSHEN.EXE"; public const string GenshinImpactFileName = "GenshinImpact.exe"; - - /// - /// 国服数据文件夹 - /// + public const string GenshinImpactFileNameUpper = "GENSHINIMPACT.EXE"; public const string YuanShenData = "YuanShen_Data"; - - /// - /// 国际服数据文件夹 - /// public const string GenshinImpactData = "GenshinImpact_Data"; - - /// - /// 国服进程名 - /// public const string YuanShenProcessName = "YuanShen"; - - /// - /// 外服进程名 - /// public const string GenshinImpactProcessName = "GenshinImpact"; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs index a0e619b1..ec1748da 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacade.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. 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; @@ -51,13 +52,13 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade } /// - public ValueTask DetectGameAccountAsync(LaunchScheme scheme) + public ValueTask DetectGameAccountAsync(SchemeType scheme) { return gameAccountService.DetectGameAccountAsync(scheme); } /// - public GameAccount? DetectCurrentGameAccount(LaunchScheme scheme) + public GameAccount? DetectCurrentGameAccount(SchemeType scheme) { return gameAccountService.DetectCurrentGameAccount(scheme); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacadeExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacadeExtension.cs new file mode 100644 index 00000000..6b110e58 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameServiceFacadeExtension.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Service.Game.Scheme; + +namespace Snap.Hutao.Service.Game; + +internal static class GameServiceFacadeExtension +{ + public static GameAccount? DetectCurrentGameAccount(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme) + { + return gameServiceFacade.DetectCurrentGameAccount(scheme.GetSchemeType()); + } + + public static ValueTask DetectGameAccountAsync(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme) + { + return gameServiceFacade.DetectGameAccountAsync(scheme.GetSchemeType()); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs index 24ba42da..e673c525 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameServiceFacade.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Primitive; using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Package; using Snap.Hutao.Service.Game.Scheme; @@ -28,7 +29,7 @@ internal interface IGameServiceFacade /// uid void AttachGameAccountToUid(GameAccount gameAccount, string uid); - ValueTask DetectGameAccountAsync(LaunchScheme scheme); + ValueTask DetectGameAccountAsync(SchemeType scheme); /// /// 异步获取游戏路径 @@ -86,5 +87,5 @@ internal interface IGameServiceFacade /// 是否更改了ini文件 bool SetChannelOptions(LaunchScheme scheme); - GameAccount? DetectCurrentGameAccount(LaunchScheme scheme); + GameAccount? DetectCurrentGameAccount(SchemeType scheme); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs index 5913e00e..06497721 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/LaunchOptionsExtension.cs @@ -9,12 +9,25 @@ namespace Snap.Hutao.Service.Game; internal static class LaunchOptionsExtension { - public static bool TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName) + public static bool TryGetGamePathAndGameDirectory(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameDirectory) + { + gamePath = options.GamePath; + + gameDirectory = Path.GetDirectoryName(gamePath); + if (string.IsNullOrEmpty(gameDirectory)) + { + return false; + } + + return true; + } + + public static bool TryGetGameDirectoryAndGameFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameDirectory, [NotNullWhen(true)] out string? gameFileName) { string gamePath = options.GamePath; - gameFolder = Path.GetDirectoryName(gamePath); - if (string.IsNullOrEmpty(gameFolder)) + gameDirectory = Path.GetDirectoryName(gamePath); + if (string.IsNullOrEmpty(gameDirectory)) { gameFileName = default; return false; @@ -42,6 +55,18 @@ internal static class LaunchOptionsExtension return true; } + public static bool TryGetGamePathAndFilePathByName(this LaunchOptions options, string fileName, out string gamePath, [NotNullWhen(true)] out string? filePath) + { + if (options.TryGetGamePathAndGameDirectory(out gamePath, out string? gameDirectory)) + { + filePath = Path.Combine(gameDirectory, fileName); + return true; + } + + filePath = default; + return false; + } + public static ImmutableList GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry) { string gamePath = options.GamePath; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactory.cs index 69872349..c57887ca 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactory.cs @@ -4,20 +4,13 @@ namespace Snap.Hutao.Service.Game.Locator; [ConstructorGenerated] -[Injection(InjectAs.Transient, typeof(IGameLocatorFactory))] +[Injection(InjectAs.Singleton, typeof(IGameLocatorFactory))] internal sealed partial class GameLocatorFactory : IGameLocatorFactory { - [SuppressMessage("", "SH301")] private readonly IServiceProvider serviceProvider; public IGameLocator Create(GameLocationSource source) { - return source switch - { - GameLocationSource.Registry => serviceProvider.GetRequiredService(), - GameLocationSource.UnityLog => serviceProvider.GetRequiredService(), - GameLocationSource.Manual => serviceProvider.GetRequiredService(), - _ => throw Must.NeverHappen(), - }; + return serviceProvider.GetRequiredKeyedService(source); } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactoryExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactoryExtensions.cs new file mode 100644 index 00000000..9349a6d2 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/GameLocatorFactoryExtensions.cs @@ -0,0 +1,12 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Locator; + +internal static class GameLocatorFactoryExtensions +{ + public static ValueTask> LocateAsync(this IGameLocatorFactory factory, GameLocationSource source) + { + return factory.Create(source).LocateGamePathAsync(); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs index 554970a2..3e201915 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/ManualGameLocator.cs @@ -11,7 +11,7 @@ namespace Snap.Hutao.Service.Game.Locator; /// [HighQuality] [ConstructorGenerated] -[Injection(InjectAs.Transient)] +[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Manual)] internal sealed partial class ManualGameLocator : IGameLocator { private readonly IFileSystemPickerInteraction fileSystemPickerInteraction; @@ -26,7 +26,7 @@ internal sealed partial class ManualGameLocator : IGameLocator if (isPickerOk) { string fileName = System.IO.Path.GetFileName(file); - if (fileName is GameConstants.YuanShenFileName or GameConstants.GenshinImpactFileName) + if (fileName.ToUpperInvariant() is GameConstants.YuanShenFileNameUpper or GameConstants.GenshinImpactFileNameUpper) { return ValueTask.FromResult>(new(true, file)); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs index 3ec14227..0903944c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/RegistryLauncherLocator.cs @@ -13,9 +13,10 @@ namespace Snap.Hutao.Service.Game.Locator; /// [HighQuality] [ConstructorGenerated] -[Injection(InjectAs.Transient)] +[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Registry)] internal sealed partial class RegistryLauncherLocator : IGameLocator { + private const string RegistryKeyName = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神"; private readonly ITaskContext taskContext; /// @@ -29,50 +30,37 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator { return result; } - else - { - string? path = Path.GetDirectoryName(result.Value); - ArgumentException.ThrowIfNullOrEmpty(path); - string configPath = Path.Combine(path, GameConstants.ConfigFileName); - string? escapedPath; - using (FileStream stream = File.OpenRead(configPath)) - { - IEnumerable elements = IniSerializer.Deserialize(stream); - escapedPath = elements - .OfType() - .FirstOrDefault(p => p.Key == "game_install_path")?.Value; - } - if (escapedPath is not null) - { - string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName); - return new(true, gamePath); - } + string? path = Path.GetDirectoryName(result.Value); + ArgumentException.ThrowIfNullOrEmpty(path); + string configPath = Path.Combine(path, GameConstants.ConfigFileName); + + string? escapedPath; + using (FileStream stream = File.OpenRead(configPath)) + { + IEnumerable elements = IniSerializer.Deserialize(stream); + escapedPath = elements + .OfType() + .FirstOrDefault(p => p.Key == "game_install_path")?.Value; + } + + if (!string.IsNullOrEmpty(escapedPath)) + { + string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName); + return new(true, gamePath); } return new(false, string.Empty); } - private static ValueResult LocateInternal(string key) + private static ValueResult LocateInternal(string valueName) { - using (RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神")) + if (Registry.GetValue(RegistryKeyName, valueName, null) is string path) { - if (uninstallKey is not null) - { - if (uninstallKey.GetValue(key) is string path) - { - return new(true, path); - } - else - { - return new(false, default!); - } - } - else - { - return new(false, default!); - } + return new(true, path); } + + return new(false, default!); } private static string Unescape(string str) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs index ca41a9cd..944e8a53 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Locator/UnityLogGameLocator.cs @@ -12,7 +12,7 @@ namespace Snap.Hutao.Service.Game.Locator; /// [HighQuality] [ConstructorGenerated] -[Injection(InjectAs.Transient)] +[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.UnityLog)] internal sealed partial class UnityLogGameLocator : IGameLocator { private readonly ITaskContext taskContext; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs index 09be982f..c8aeb30b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/GamePackageService.cs @@ -21,7 +21,7 @@ internal sealed partial class GamePackageService : IGamePackageService public async ValueTask EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress progress) { - if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName)) + if (!launchOptions.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName)) { return false; } @@ -47,8 +47,7 @@ internal sealed partial class GamePackageService : IGamePackageService if (!launchScheme.ExecutableMatches(gameFileName)) { - // We can't start the game - // when we failed to convert game + // We can't start the game when we failed to convert game if (!await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false)) { return false; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs index 560c586b..8b34eed5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -15,6 +15,7 @@ using System.IO.Compression; using System.Net.Http; using System.Text.RegularExpressions; using static Snap.Hutao.Service.Game.GameConstants; +using RelativePathVersionItemDictionary = System.Collections.Generic.Dictionary; namespace Snap.Hutao.Service.Game.Package; @@ -58,15 +59,15 @@ internal sealed partial class PackageConverter string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath; string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}"; - PackageConvertContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl); + PackageConverterFileSystemContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl); // Step 1 progress.Report(new(SH.ServiceGamePackageRequestPackageVerion)); - Dictionary remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false); - Dictionary localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false); + RelativePathVersionItemDictionary remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false); + RelativePathVersionItemDictionary localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false); // Step 2 - List diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList(); + List diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList(); diffOperations.SortBy(i => i.Type); // Step 3 @@ -116,16 +117,16 @@ internal sealed partial class PackageConverter } } - private static IEnumerable GetItemOperationInfos(Dictionary remote, Dictionary local) + private static IEnumerable GetItemOperationInfos(RelativePathVersionItemDictionary remote, RelativePathVersionItemDictionary local) { foreach ((string remoteName, VersionItem remoteItem) in remote) { if (local.TryGetValue(remoteName, out VersionItem? localItem)) { - if (!remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase)) + if (!(remoteItem.FileSize == localItem.FileSize && remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase))) { // 本地发现了同名且不同 MD5 的项,需要替换为服务器上的项 - yield return new(ItemOperationType.Replace, remoteItem, localItem); + yield return new(PackageItemOperationType.Replace, remoteItem, localItem); } // 同名同MD5,跳过 @@ -134,22 +135,22 @@ internal sealed partial class PackageConverter else { // 本地没有发现同名项 - yield return new(ItemOperationType.Add, remoteItem, remoteItem); + yield return new(PackageItemOperationType.Add, remoteItem, remoteItem); } } foreach ((_, VersionItem localItem) in local) { - yield return new(ItemOperationType.Backup, localItem, localItem); + yield return new(PackageItemOperationType.Backup, localItem, localItem); } } [GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")] private static partial Regex DataFolderRegex(); - private async ValueTask> GetVersionItemsAsync(Stream stream) + private async ValueTask GetVersionItemsAsync(Stream stream) { - Dictionary results = []; + RelativePathVersionItemDictionary results = []; using (StreamReader reader = new(stream)) { while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } row) @@ -164,7 +165,7 @@ internal sealed partial class PackageConverter return results; } - private async ValueTask> GetRemoteItemsAsync(string pkgVersionUrl) + private async ValueTask GetRemoteItemsAsync(string pkgVersionUrl) { try { @@ -179,7 +180,7 @@ internal sealed partial class PackageConverter } } - private async ValueTask> GetLocalItemsAsync(string gameFolder) + private async ValueTask GetLocalItemsAsync(string gameFolder) { using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion))) { @@ -187,23 +188,23 @@ internal sealed partial class PackageConverter } } - private async ValueTask PrepareCacheFilesAsync(List operations, PackageConvertContext context, IProgress progress) + private async ValueTask PrepareCacheFilesAsync(List operations, PackageConverterFileSystemContext context, IProgress progress) { - foreach (ItemOperationInfo info in operations) + foreach (PackageItemOperationInfo info in operations) { switch (info.Type) { - case ItemOperationType.Backup: + case PackageItemOperationType.Backup: continue; - case ItemOperationType.Replace: - case ItemOperationType.Add: + case PackageItemOperationType.Replace: + case PackageItemOperationType.Add: await SkipOrDownloadAsync(info, context, progress).ConfigureAwait(false); break; } } } - private async ValueTask SkipOrDownloadAsync(ItemOperationInfo info, PackageConvertContext context, IProgress progress) + private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress progress) { // 还原正确的远程地址 string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName); @@ -257,16 +258,16 @@ internal sealed partial class PackageConverter } } - private async ValueTask ReplaceGameResourceAsync(List operations, PackageConvertContext context, IProgress progress) + private async ValueTask ReplaceGameResourceAsync(List operations, PackageConverterFileSystemContext context, IProgress progress) { // 执行下载与移动操作 - foreach (ItemOperationInfo info in operations) + foreach (PackageItemOperationInfo info in operations) { (bool moveToBackup, bool moveToTarget) = info.Type switch { - ItemOperationType.Backup => (true, false), - ItemOperationType.Replace => (true, true), - ItemOperationType.Add => (false, true), + PackageItemOperationType.Backup => (true, false), + PackageItemOperationType.Replace => (true, true), + PackageItemOperationType.Add => (false, true), _ => (false, false), }; @@ -321,7 +322,7 @@ internal sealed partial class PackageConverter return true; } - private async ValueTask ReplacePackageVersionFilesAsync(PackageConvertContext context) + private async ValueTask ReplacePackageVersionFilesAsync(PackageConverterFileSystemContext context) { foreach (string versionFilePath in Directory.EnumerateFiles(context.GameFolder, "*pkg_version")) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConvertContext.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterFileSystemContext.cs similarity index 85% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConvertContext.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterFileSystemContext.cs index 605c6906..eb92df62 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConvertContext.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverterFileSystemContext.cs @@ -6,7 +6,7 @@ using static Snap.Hutao.Service.Game.GameConstants; namespace Snap.Hutao.Service.Game.Package; -internal readonly struct PackageConvertContext +internal readonly struct PackageConverterFileSystemContext { public readonly string GameFolder; public readonly string ServerCacheFolder; @@ -22,7 +22,7 @@ internal readonly struct PackageConvertContext public readonly string ScatteredFilesUrl; public readonly string PkgVersionUrl; - public PackageConvertContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl) + public PackageConverterFileSystemContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl) { GameFolder = gameFolder; ServerCacheFolder = Path.Combine(dataFolder, "ServerCache"); @@ -37,7 +37,8 @@ internal readonly struct PackageConvertContext ? (YuanShenData, GenshinImpactData) : (GenshinImpactData, YuanShenData); - (FromDataFolder, ToDataFolder) = (Path.Combine(GameFolder, FromDataFolderName), Path.Combine(GameFolder, ToDataFolderName)); + FromDataFolder = Path.Combine(GameFolder, FromDataFolderName); + ToDataFolder = Path.Combine(GameFolder, ToDataFolderName); ScatteredFilesUrl = scatteredFilesUrl; PkgVersionUrl = $"{scatteredFilesUrl}/pkg_version"; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageItemOperationInfo.cs similarity index 80% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageItemOperationInfo.cs index bb4acb4e..6b268be8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageItemOperationInfo.cs @@ -10,12 +10,12 @@ namespace Snap.Hutao.Service.Game.Package; /// [HighQuality] [DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")] -internal readonly struct ItemOperationInfo +internal readonly struct PackageItemOperationInfo { /// /// 操作的类型 /// - public readonly ItemOperationType Type; + public readonly PackageItemOperationType Type; /// /// 目标文件 @@ -33,7 +33,7 @@ internal readonly struct ItemOperationInfo /// 操作类型 /// 远程 /// 本地 - public ItemOperationInfo(ItemOperationType type, VersionItem remote, VersionItem local) + public PackageItemOperationInfo(PackageItemOperationType type, VersionItem remote, VersionItem local) { Type = type; Remote = remote; diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationType.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageItemOperationType.cs similarity index 91% rename from src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationType.cs rename to src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageItemOperationType.cs index 19b8a299..b253c34f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationType.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageItemOperationType.cs @@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Game.Package; /// 包文件操作的类型 /// [HighQuality] -internal enum ItemOperationType +internal enum PackageItemOperationType { /// /// 需要备份 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs index 1998f152..d78b5b5f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs @@ -2,14 +2,13 @@ // Licensed under the MIT license. using CommunityToolkit.Common; -using Snap.Hutao.Core.Abstraction; namespace Snap.Hutao.Service.Game.Package; /// /// 包更新状态 /// -internal sealed class PackageReplaceStatus : ICloneable +internal sealed class PackageReplaceStatus { /// /// 构造一个新的包更新状态 @@ -34,10 +33,6 @@ internal sealed class PackageReplaceStatus : ICloneable Description = $"{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}"; } - private PackageReplaceStatus() - { - } - public string Name { get; set; } = default!; /// @@ -54,19 +49,4 @@ internal sealed class PackageReplaceStatus : ICloneable /// 是否有进度 /// public bool IsIndeterminate { get => Percent < 0; } - - /// - /// 克隆 - /// - /// 克隆的实例 - public PackageReplaceStatus Clone() - { - // 进度需要在主线程上创建 - return new() - { - Name = Name, - Description = Description, - Percent = Percent, - }; - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs index a9294f1d..c040acaf 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/PathAbstraction/GamePathService.cs @@ -9,7 +9,7 @@ namespace Snap.Hutao.Service.Game.PathAbstraction; [Injection(InjectAs.Singleton, typeof(IGamePathService))] internal sealed partial class GamePathService : IGamePathService { - private readonly IServiceProvider serviceProvider; + private readonly IGameLocatorFactory gameLocatorFactory; private readonly LaunchOptions launchOptions; public async ValueTask> SilentGetGamePathAsync() @@ -17,24 +17,16 @@ internal sealed partial class GamePathService : IGamePathService // Cannot find in setting if (string.IsNullOrEmpty(launchOptions.GamePath)) { - IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService(); - bool isOk; string path; // Try locate by unity log - (isOk, path) = await locatorFactory - .Create(GameLocationSource.UnityLog) - .LocateGamePathAsync() - .ConfigureAwait(false); + (isOk, path) = await gameLocatorFactory.LocateAsync(GameLocationSource.UnityLog).ConfigureAwait(false); if (!isOk) { // Try locate by registry - (isOk, path) = await locatorFactory - .Create(GameLocationSource.Registry) - .LocateGamePathAsync() - .ConfigureAwait(false); + (isOk, path) = await gameLocatorFactory.LocateAsync(GameLocationSource.Registry).ConfigureAwait(false); } if (isOk) @@ -48,13 +40,11 @@ internal sealed partial class GamePathService : IGamePathService } } - if (!string.IsNullOrEmpty(launchOptions.GamePath)) - { - return new(true, launchOptions.GamePath); - } - else + if (string.IsNullOrEmpty(launchOptions.GamePath)) { return new(false, default!); } + + return new(true, launchOptions.GamePath); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs index c2191176..5f960dbb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Process/GameProcessService.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. using Snap.Hutao.Core; +using Snap.Hutao.Factory.Progress; using Snap.Hutao.Service.Discord; using Snap.Hutao.Service.Game.Scheme; using Snap.Hutao.Service.Game.Unlocker; @@ -18,6 +19,7 @@ namespace Snap.Hutao.Service.Game.Process; 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; @@ -138,7 +140,7 @@ internal sealed partial class GameProcessService : IGameProcessService IGameFpsUnlocker unlocker = serviceProvider.CreateInstance(game); #pragma warning restore CA1859 UnlockTimingOptions options = new(100, 20000, 3000); - Progress lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus))); + IProgress lockerProgress = progressFactory.CreateForMainThread(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus))); return unlocker.UnlockAsync(options, lockerProgress, token); } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs index 8cef987b..30857085 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Scheme/LaunchScheme.cs @@ -59,11 +59,11 @@ internal class LaunchScheme : IEquatable public static bool ExecutableIsOversea(string gameFileName) { - return gameFileName switch + return gameFileName.ToUpperInvariant() switch { - GameConstants.GenshinImpactFileName => true, - GameConstants.YuanShenFileName => false, - _ => throw Requires.Fail("无效的游戏可执行文件名称:{0}", gameFileName), + GameConstants.GenshinImpactFileNameUpper => true, + GameConstants.YuanShenFileNameUpper => false, + _ => throw Requires.Fail("Invalid game executable file name:{0}", gameFileName), }; } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs index cd6ea8c6..1a6c8f65 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Unlocker/GameFpsUnlocker.cs @@ -228,7 +228,7 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker nuint rip = localMemoryUserAssemblyAddress + (uint)offset; rip += 5U; - rip += (nuint)(*(int*)(rip + 2) + 6); + rip += (nuint)(*(int*)(rip + 2U) + 6); nuint address = userAssembly.Address + (rip - localMemoryUserAssemblyAddress); @@ -236,6 +236,8 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker SpinWait.SpinUntil(() => UnsafeReadProcessMemory(gameProcess, address, out ptr) && ptr != 0); rip = ptr - unityPlayer.Address + localMemoryUnityPlayerAddress; + + // CALL or JMP while (*(byte*)rip == 0xE8 || *(byte*)rip == 0xE9) { rip += (nuint)(*(int*)(rip + 1) + 5); diff --git a/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml index bc97e50c..1384c03b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Card/LaunchGameCard.xaml @@ -67,7 +67,7 @@ VerticalAlignment="Bottom"> diff --git a/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs index 94d20cd0..e8c1b830 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/TitleView.xaml.cs @@ -1,7 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Snap.Hutao.ViewModel; diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/GameAccountFilter.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/GameAccountFilter.cs new file mode 100644 index 00000000..d3a52c75 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/GameAccountFilter.cs @@ -0,0 +1,27 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Primitive; + +namespace Snap.Hutao.ViewModel.Game; + +internal sealed class GameAccountFilter +{ + private readonly SchemeType? type; + + public GameAccountFilter(SchemeType? type) + { + this.type = type; + } + + public bool Filter(object? item) + { + if (type is null) + { + return true; + } + + return item is GameAccount account && account.Type == type; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameShared.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameShared.cs new file mode 100644 index 00000000..ba8e48c5 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameShared.cs @@ -0,0 +1,39 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Service.Game; +using Snap.Hutao.Service.Game.Configuration; +using Snap.Hutao.Service.Game.Scheme; +using Snap.Hutao.Service.Notification; + +namespace Snap.Hutao.ViewModel.Game; + +internal static class LaunchGameShared +{ + public static LaunchScheme? GetCurrentLaunchSchemeFromConfigFile(IGameServiceFacade gameService, IInfoBarService infoBarService) + { + ChannelOptions options = gameService.GetChannelOptions(); + if (string.IsNullOrEmpty(options.ConfigFilePath)) + { + try + { + return KnownLaunchSchemes.Get().Single(scheme => scheme.Equals(options)); + } + catch (InvalidOperationException) + { + if (!IgnoredInvalidChannelOptions.Contains(options)) + { + // 后台收集 + throw ThrowHelper.NotSupported($"不支持的 MultiChannel: {options}"); + } + } + } + else + { + infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath)); + } + + return default; + } +} \ 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 c0affef2..b4de5f22 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModel.cs @@ -1,6 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.WinUI.Collections; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Caching.Memory; using Snap.Hutao.Control.Extension; @@ -56,44 +57,23 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel private readonly AppOptions appOptions; private LaunchScheme? selectedScheme; - private ObservableCollection? gameAccounts; + private AdvancedCollectionView? gameAccountsView; private GameAccount? selectedGameAccount; private GameResource? gameResource; private bool gamePathSelectedAndValid; private ImmutableList gamePathEntries; private GamePathEntry? selectedGamePathEntry; + private GameAccountFilter? gameAccountFilter; public List KnownSchemes { get; } = KnownLaunchSchemes.Get(); public LaunchScheme? SelectedScheme { get => selectedScheme; - set - { - SetProperty(ref selectedScheme, value, UpdateGameResourceAsync); - - async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme) - { - if (scheme is null) - { - return; - } - - await taskContext.SwitchToBackgroundAsync(); - Web.Response.Response response = await resourceClient - .GetResourceAsync(scheme) - .ConfigureAwait(false); - - if (response.IsOk()) - { - await taskContext.SwitchToMainThreadAsync(); - GameResource = response.Data; - } - } - } + set => SetSelectedSchemeAsync(value).SafeForget(); } - public ObservableCollection? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); } + public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); } public GameAccount? SelectedGameAccount { get => selectedGameAccount; set => SetProperty(ref selectedGameAccount, value); } @@ -114,7 +94,54 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel { if (SetProperty(ref gamePathSelectedAndValid, value) && value) { - InitializeUICoreAsync().SafeForget(); + RefreshUIAsync().SafeForget(); + } + + async ValueTask RefreshUIAsync() + { + try + { + using (await EnterCriticalExecutionAsync().ConfigureAwait(false)) + { + LaunchScheme? scheme = LaunchGameShared.GetCurrentLaunchSchemeFromConfigFile(gameService, infoBarService); + + await taskContext.SwitchToMainThreadAsync(); + await SetSelectedSchemeAsync(scheme).ConfigureAwait(true); + + // Sync uid, almost never hit, so we are not so care about performance + if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid) + { + ArgumentNullException.ThrowIfNull(GameAccountsView); + + // Exists in the source collection + if (GameAccountsView.SourceCollection.Cast().FirstOrDefault(g => g.AttachUid == uid) is { } sourceAccount) + { + SelectedGameAccount = GameAccountsView.Cast().FirstOrDefault(g => g.AttachUid == uid); + + // But not exists in the view for current scheme + if (SelectedGameAccount is null) + { + infoBarService.Warning(SH.FormatViewModelLaunchGameUnableToSwitchUidAttachedGameAccount(uid, sourceAccount.Name)); + } + } + } + + // Try set to the current account. + if (SelectedScheme is not null) + { + // The GameAccount is gaurenteed to be in the view, bacause the scheme is synced + SelectedGameAccount ??= gameService.DetectCurrentGameAccount(SelectedScheme); + } + else + { + infoBarService.Warning(SH.ViewModelLaunchGameSchemeNotSelected); + } + } + } + catch (UserdataCorruptedException ex) + { + infoBarService.Error(ex); + } } } } @@ -134,64 +161,6 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel return ValueTask.FromResult(true); } - private async ValueTask InitializeUICoreAsync() - { - try - { - using (await EnterCriticalExecutionAsync().ConfigureAwait(false)) - { - ChannelOptions options = gameService.GetChannelOptions(); - if (string.IsNullOrEmpty(options.ConfigFilePath)) - { - try - { - SelectedScheme = KnownSchemes.Single(scheme => scheme.Equals(options)); - } - catch (InvalidOperationException) - { - if (!IgnoredInvalidChannelOptions.Contains(options)) - { - // 后台收集 - throw ThrowHelper.NotSupported($"不支持的 MultiChannel: {options}"); - } - } - } - else - { - infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath)); - } - - ObservableCollection accounts = gameService.GameAccountCollection; - - await taskContext.SwitchToMainThreadAsync(); - GameAccounts = accounts; - - // Sync uid - if (memoryCache.TryRemove(DesiredUid, out object? value) && value is string uid) - { - SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid); - } - - // Try set to the current account. - if (SelectedScheme is not null) - { - SelectedGameAccount ??= gameService.DetectCurrentGameAccount(SelectedScheme); - } - else - { - infoBarService.Warning(SH.ViewModelLaunchGameSchemeNotSelected); - } - } - } - catch (UserdataCorruptedException ex) - { - infoBarService.Error(ex); - } - catch (OperationCanceledException) - { - } - } - private void UpdateSelectedGamePathEntry(GamePathEntry? value, bool setBack) { if (SetProperty(ref selectedGamePathEntry, value, nameof(SelectedGamePathEntry)) && setBack) @@ -369,4 +338,44 @@ internal sealed partial class LaunchGameViewModel : Abstraction.ViewModel await Windows.System.Launcher.LaunchFolderPathAsync(screenshot); } } + + private async ValueTask SetSelectedSchemeAsync(LaunchScheme? value) + { + if (SetProperty(ref selectedScheme, value, nameof(SelectedScheme))) + { + UpdateGameResourceAsync(value).SafeForget(); + await UpdateGameAccountsViewAsync().ConfigureAwait(false); + } + + async ValueTask UpdateGameResourceAsync(LaunchScheme? scheme) + { + if (scheme is null) + { + return; + } + + await taskContext.SwitchToBackgroundAsync(); + Web.Response.Response response = await resourceClient + .GetResourceAsync(scheme) + .ConfigureAwait(false); + + if (response.IsOk()) + { + await taskContext.SwitchToMainThreadAsync(); + GameResource = response.Data; + } + } + + async ValueTask UpdateGameAccountsViewAsync() + { + gameAccountFilter = new(SelectedScheme?.GetSchemeType()); + ObservableCollection accounts = gameService.GameAccountCollection; + + await taskContext.SwitchToMainThreadAsync(); + GameAccountsView = new(accounts, true) + { + Filter = gameAccountFilter.Filter, + }; + } + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs index 9ca0c82d..b1743441 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameViewModelSlim.cs @@ -1,8 +1,11 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using CommunityToolkit.WinUI.Collections; using Snap.Hutao.Core.ExceptionService; +using Snap.Hutao.Factory.Progress; using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Primitive; using Snap.Hutao.Service.Game; using Snap.Hutao.Service.Game.Configuration; using Snap.Hutao.Service.Game.Scheme; @@ -19,17 +22,17 @@ namespace Snap.Hutao.ViewModel.Game; [ConstructorGenerated(CallBaseConstructor = true)] internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSlim { + private readonly LaunchStatusOptions launchStatusOptions; + private readonly IProgressFactory progressFactory; + private readonly IInfoBarService infoBarService; private readonly IGameServiceFacade gameService; private readonly ITaskContext taskContext; - private readonly IInfoBarService infoBarService; - private ObservableCollection? gameAccounts; + private AdvancedCollectionView? gameAccountsView; private GameAccount? selectedGameAccount; + private GameAccountFilter? gameAccountFilter; - /// - /// 游戏账号集合 - /// - public ObservableCollection? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); } + public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); } /// /// 选中的账号 @@ -39,31 +42,7 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli /// protected override async Task OpenUIAsync() { - ObservableCollection accounts = gameService.GameAccountCollection; - await taskContext.SwitchToMainThreadAsync(); - GameAccounts = accounts; - - ChannelOptions options = gameService.GetChannelOptions(); - LaunchScheme? scheme = default; - if (string.IsNullOrEmpty(options.ConfigFilePath)) - { - try - { - scheme = KnownLaunchSchemes.Get().Single(scheme => scheme.Equals(options)); - } - catch (InvalidOperationException) - { - if (!IgnoredInvalidChannelOptions.Contains(options)) - { - // 后台收集 - throw ThrowHelper.NotSupported($"不支持的 MultiChannel: {options}"); - } - } - } - else - { - infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath)); - } + LaunchScheme? scheme = LaunchGameShared.GetCurrentLaunchSchemeFromConfigFile(gameService, infoBarService); try { @@ -77,6 +56,15 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli { infoBarService.Error(ex); } + + gameAccountFilter = new(scheme?.GetSchemeType()); + ObservableCollection accounts = gameService.GameAccountCollection; + + await taskContext.SwitchToMainThreadAsync(); + GameAccountsView = new(accounts, true) + { + Filter = gameAccountFilter.Filter, + }; } [Command("LaunchCommand")] @@ -95,7 +83,8 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli } } - await gameService.LaunchAsync(new Progress()).ConfigureAwait(false); + IProgress launchProgress = progressFactory.CreateForMainThread(status => launchStatusOptions.LaunchStatus = status); + await gameService.LaunchAsync(launchProgress).ConfigureAwait(false); } catch (Exception ex) { diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs index 6b570c00..9ff8767f 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingViewModel.cs @@ -47,7 +47,6 @@ internal sealed partial class SettingViewModel : Abstraction.ViewModel private readonly HutaoInfrastructureClient hutaoInfrastructureClient; private readonly HutaoPassportViewModel hutaoPassportViewModel; private readonly IContentDialogFactory contentDialogFactory; - private readonly IGameLocatorFactory gameLocatorFactory; private readonly INavigationService navigationService; private readonly IClipboardProvider clipboardInterop; private readonly IShellLinkInterop shellLinkInterop;