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;