mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
refactor Launch Game Pipeline
This commit is contained in:
@@ -1553,6 +1553,9 @@
|
||||
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
|
||||
<value>切换账号失败</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
|
||||
<value>无法选择UID [{0}] 对应的账号 [{1}],该账号不属于当前服务器</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingActionComplete" xml:space="preserve">
|
||||
<value>操作完成</value>
|
||||
</data>
|
||||
|
||||
@@ -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<GameAccount?> DetectGameAccountAsync(LaunchScheme scheme)
|
||||
public async ValueTask<GameAccount?> 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<LaunchGameAccountNameDialog>().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<LaunchGameAccountNameDialog>().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<LaunchGameAccountNameDialog>().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<GameAccount> gameAccounts, string registrySdk)
|
||||
{
|
||||
try
|
||||
{
|
||||
return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
throw ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GameAccount?> DetectGameAccountAsync(LaunchScheme scheme);
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType);
|
||||
|
||||
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
|
||||
@@ -43,17 +43,17 @@ internal static class RegistryInterop
|
||||
(string keyName, string valueName) = GetKeyValueName(scheme);
|
||||
object? sdk = Registry.GetValue(keyName, valueName, Array.Empty<byte>());
|
||||
|
||||
if (sdk is byte[] bytes)
|
||||
if (sdk is not byte[] bytes)
|
||||
{
|
||||
fixed (byte* pByte = bytes)
|
||||
{
|
||||
// 从注册表获取的字节数组带有 '\0' 结尾,需要舍去
|
||||
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
|
||||
return Encoding.UTF8.GetString(span);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
fixed (byte* pByte = bytes)
|
||||
{
|
||||
// 从注册表获取的字节数组带有 '\0' 结尾,需要舍去
|
||||
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
|
||||
return Encoding.UTF8.GetString(span);
|
||||
}
|
||||
}
|
||||
|
||||
private static (string KeyName, string ValueName) GetKeyValueName(SchemeType scheme)
|
||||
|
||||
@@ -34,21 +34,6 @@ internal readonly struct ChannelOptions
|
||||
/// </summary>
|
||||
public readonly string? ConfigFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的多通道
|
||||
/// </summary>
|
||||
/// <param name="channel">通道</param>
|
||||
/// <param name="subChannel">子通道</param>
|
||||
/// <param name="isOversea">是否为国际服</param>
|
||||
/// <param name="configFilePath">配置文件路径</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件未找到
|
||||
/// </summary>
|
||||
/// <param name="isOversea">是否为国际服</param>
|
||||
/// <param name="configFilePath">配置文件期望路径</param>
|
||||
/// <returns>选项</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
|
||||
@@ -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<IniElement> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,38 +9,13 @@ namespace Snap.Hutao.Service.Game;
|
||||
[HighQuality]
|
||||
internal static class GameConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// 设置文件
|
||||
/// </summary>
|
||||
public const string ConfigFileName = "config.ini";
|
||||
|
||||
/// <summary>
|
||||
/// 国服文件名
|
||||
/// </summary>
|
||||
public const string YuanShenFileName = "YuanShen.exe";
|
||||
|
||||
/// <summary>
|
||||
/// 外服文件名
|
||||
/// </summary>
|
||||
public const string YuanShenFileNameUpper = "YUANSHEN.EXE";
|
||||
public const string GenshinImpactFileName = "GenshinImpact.exe";
|
||||
|
||||
/// <summary>
|
||||
/// 国服数据文件夹
|
||||
/// </summary>
|
||||
public const string GenshinImpactFileNameUpper = "GENSHINIMPACT.EXE";
|
||||
public const string YuanShenData = "YuanShen_Data";
|
||||
|
||||
/// <summary>
|
||||
/// 国际服数据文件夹
|
||||
/// </summary>
|
||||
public const string GenshinImpactData = "GenshinImpact_Data";
|
||||
|
||||
/// <summary>
|
||||
/// 国服进程名
|
||||
/// </summary>
|
||||
public const string YuanShenProcessName = "YuanShen";
|
||||
|
||||
/// <summary>
|
||||
/// 外服进程名
|
||||
/// </summary>
|
||||
public const string GenshinImpactProcessName = "GenshinImpact";
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<GameAccount?> DetectGameAccountAsync(LaunchScheme scheme)
|
||||
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
|
||||
{
|
||||
return gameAccountService.DetectGameAccountAsync(scheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameAccount? DetectCurrentGameAccount(LaunchScheme scheme)
|
||||
public GameAccount? DetectCurrentGameAccount(SchemeType scheme)
|
||||
{
|
||||
return gameAccountService.DetectCurrentGameAccount(scheme);
|
||||
}
|
||||
|
||||
@@ -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<GameAccount?> DetectGameAccountAsync(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme)
|
||||
{
|
||||
return gameServiceFacade.DetectGameAccountAsync(scheme.GetSchemeType());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <param name="uid">uid</param>
|
||||
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
|
||||
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync(LaunchScheme scheme);
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取游戏路径
|
||||
@@ -86,5 +87,5 @@ internal interface IGameServiceFacade
|
||||
/// <returns>是否更改了ini文件</returns>
|
||||
bool SetChannelOptions(LaunchScheme scheme);
|
||||
|
||||
GameAccount? DetectCurrentGameAccount(LaunchScheme scheme);
|
||||
GameAccount? DetectCurrentGameAccount(SchemeType scheme);
|
||||
}
|
||||
@@ -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<GamePathEntry> GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry)
|
||||
{
|
||||
string gamePath = options.GamePath;
|
||||
|
||||
@@ -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<RegistryLauncherLocator>(),
|
||||
GameLocationSource.UnityLog => serviceProvider.GetRequiredService<UnityLogGameLocator>(),
|
||||
GameLocationSource.Manual => serviceProvider.GetRequiredService<ManualGameLocator>(),
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
return serviceProvider.GetRequiredKeyedService<IGameLocator>(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ValueResult<bool, string>> LocateAsync(this IGameLocatorFactory factory, GameLocationSource source)
|
||||
{
|
||||
return factory.Create(source).LocateGamePathAsync();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Service.Game.Locator;
|
||||
/// </summary>
|
||||
[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<ValueResult<bool, string>>(new(true, file));
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ namespace Snap.Hutao.Service.Game.Locator;
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -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<IniElement> elements = IniSerializer.Deserialize(stream);
|
||||
escapedPath = elements
|
||||
.OfType<IniParameter>()
|
||||
.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<IniElement> elements = IniSerializer.Deserialize(stream);
|
||||
escapedPath = elements
|
||||
.OfType<IniParameter>()
|
||||
.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<bool, string> LocateInternal(string key)
|
||||
private static ValueResult<bool, string> 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)
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Service.Game.Locator;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Transient)]
|
||||
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.UnityLog)]
|
||||
internal sealed partial class UnityLogGameLocator : IGameLocator
|
||||
{
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
@@ -21,7 +21,7 @@ internal sealed partial class GamePackageService : IGamePackageService
|
||||
|
||||
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> 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;
|
||||
|
||||
@@ -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<string, Snap.Hutao.Service.Game.Package.VersionItem>;
|
||||
|
||||
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<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
|
||||
Dictionary<string, VersionItem> localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
|
||||
RelativePathVersionItemDictionary remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
|
||||
RelativePathVersionItemDictionary localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
|
||||
|
||||
// Step 2
|
||||
List<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
|
||||
List<PackageItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
|
||||
diffOperations.SortBy(i => i.Type);
|
||||
|
||||
// Step 3
|
||||
@@ -116,16 +117,16 @@ internal sealed partial class PackageConverter
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ItemOperationInfo> GetItemOperationInfos(Dictionary<string, VersionItem> remote, Dictionary<string, VersionItem> local)
|
||||
private static IEnumerable<PackageItemOperationInfo> 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<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream)
|
||||
private async ValueTask<RelativePathVersionItemDictionary> GetVersionItemsAsync(Stream stream)
|
||||
{
|
||||
Dictionary<string, VersionItem> 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<Dictionary<string, VersionItem>> GetRemoteItemsAsync(string pkgVersionUrl)
|
||||
private async ValueTask<RelativePathVersionItemDictionary> GetRemoteItemsAsync(string pkgVersionUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -179,7 +180,7 @@ internal sealed partial class PackageConverter
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<Dictionary<string, VersionItem>> GetLocalItemsAsync(string gameFolder)
|
||||
private async ValueTask<RelativePathVersionItemDictionary> 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<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
|
||||
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> 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<PackageReplaceStatus> progress)
|
||||
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
// 还原正确的远程地址
|
||||
string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName);
|
||||
@@ -257,16 +258,16 @@ internal sealed partial class PackageConverter
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<bool> ReplaceGameResourceAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
|
||||
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageReplaceStatus> 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"))
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
@@ -10,12 +10,12 @@ namespace Snap.Hutao.Service.Game.Package;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")]
|
||||
internal readonly struct ItemOperationInfo
|
||||
internal readonly struct PackageItemOperationInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 操作的类型
|
||||
/// </summary>
|
||||
public readonly ItemOperationType Type;
|
||||
public readonly PackageItemOperationType Type;
|
||||
|
||||
/// <summary>
|
||||
/// 目标文件
|
||||
@@ -33,7 +33,7 @@ internal readonly struct ItemOperationInfo
|
||||
/// <param name="type">操作类型</param>
|
||||
/// <param name="remote">远程</param>
|
||||
/// <param name="local">本地</param>
|
||||
public ItemOperationInfo(ItemOperationType type, VersionItem remote, VersionItem local)
|
||||
public PackageItemOperationInfo(PackageItemOperationType type, VersionItem remote, VersionItem local)
|
||||
{
|
||||
Type = type;
|
||||
Remote = remote;
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Game.Package;
|
||||
/// 包文件操作的类型
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal enum ItemOperationType
|
||||
internal enum PackageItemOperationType
|
||||
{
|
||||
/// <summary>
|
||||
/// 需要备份
|
||||
@@ -2,14 +2,13 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Common;
|
||||
using Snap.Hutao.Core.Abstraction;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Package;
|
||||
|
||||
/// <summary>
|
||||
/// 包更新状态
|
||||
/// </summary>
|
||||
internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
|
||||
internal sealed class PackageReplaceStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造一个新的包更新状态
|
||||
@@ -34,10 +33,6 @@ internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
|
||||
Description = $"{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}";
|
||||
}
|
||||
|
||||
private PackageReplaceStatus()
|
||||
{
|
||||
}
|
||||
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
@@ -54,19 +49,4 @@ internal sealed class PackageReplaceStatus : ICloneable<PackageReplaceStatus>
|
||||
/// 是否有进度
|
||||
/// </summary>
|
||||
public bool IsIndeterminate { get => Percent < 0; }
|
||||
|
||||
/// <summary>
|
||||
/// 克隆
|
||||
/// </summary>
|
||||
/// <returns>克隆的实例</returns>
|
||||
public PackageReplaceStatus Clone()
|
||||
{
|
||||
// 进度需要在主线程上创建
|
||||
return new()
|
||||
{
|
||||
Name = Name,
|
||||
Description = Description,
|
||||
Percent = Percent,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<ValueResult<bool, string>> SilentGetGamePathAsync()
|
||||
@@ -17,24 +17,16 @@ internal sealed partial class GamePathService : IGamePathService
|
||||
// Cannot find in setting
|
||||
if (string.IsNullOrEmpty(launchOptions.GamePath))
|
||||
{
|
||||
IGameLocatorFactory locatorFactory = serviceProvider.GetRequiredService<IGameLocatorFactory>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<GameFpsUnlocker>(game);
|
||||
#pragma warning restore CA1859
|
||||
UnlockTimingOptions options = new(100, 20000, 3000);
|
||||
Progress<UnlockerStatus> lockerProgress = new(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus)));
|
||||
IProgress<UnlockerStatus> lockerProgress = progressFactory.CreateForMainThread<UnlockerStatus>(unlockStatus => progress.Report(LaunchStatus.FromUnlockStatus(unlockStatus)));
|
||||
return unlocker.UnlockAsync(options, lockerProgress, token);
|
||||
}
|
||||
|
||||
|
||||
@@ -59,11 +59,11 @@ internal class LaunchScheme : IEquatable<ChannelOptions>
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
VerticalAlignment="Bottom">
|
||||
<ComboBox
|
||||
DisplayMemberPath="Name"
|
||||
ItemsSource="{Binding GameAccounts}"
|
||||
ItemsSource="{Binding GameAccountsView}"
|
||||
PlaceholderText="{shcm:ResourceString Name=ViewCardLaunchGameSelectAccountPlaceholder}"
|
||||
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}"/>
|
||||
</shc:SizeRestrictedContentControl>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
39
src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameShared.cs
Normal file
39
src/Snap.Hutao/Snap.Hutao/ViewModel/Game/LaunchGameShared.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<GameAccount>? gameAccounts;
|
||||
private AdvancedCollectionView? gameAccountsView;
|
||||
private GameAccount? selectedGameAccount;
|
||||
private GameResource? gameResource;
|
||||
private bool gamePathSelectedAndValid;
|
||||
private ImmutableList<GamePathEntry> gamePathEntries;
|
||||
private GamePathEntry? selectedGamePathEntry;
|
||||
private GameAccountFilter? gameAccountFilter;
|
||||
|
||||
public List<LaunchScheme> 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<GameResource> response = await resourceClient
|
||||
.GetResourceAsync(scheme)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.IsOk())
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
GameResource = response.Data;
|
||||
}
|
||||
}
|
||||
}
|
||||
set => SetSelectedSchemeAsync(value).SafeForget();
|
||||
}
|
||||
|
||||
public ObservableCollection<GameAccount>? 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<GameAccount>().FirstOrDefault(g => g.AttachUid == uid) is { } sourceAccount)
|
||||
{
|
||||
SelectedGameAccount = GameAccountsView.Cast<GameAccount>().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<GameAccount> 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<GameResource> response = await resourceClient
|
||||
.GetResourceAsync(scheme)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (response.IsOk())
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
GameResource = response.Data;
|
||||
}
|
||||
}
|
||||
|
||||
async ValueTask UpdateGameAccountsViewAsync()
|
||||
{
|
||||
gameAccountFilter = new(SelectedScheme?.GetSchemeType());
|
||||
ObservableCollection<GameAccount> accounts = gameService.GameAccountCollection;
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
GameAccountsView = new(accounts, true)
|
||||
{
|
||||
Filter = gameAccountFilter.Filter,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<View.Page.LaunchGamePage>
|
||||
{
|
||||
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<GameAccount>? gameAccounts;
|
||||
private AdvancedCollectionView? gameAccountsView;
|
||||
private GameAccount? selectedGameAccount;
|
||||
private GameAccountFilter? gameAccountFilter;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏账号集合
|
||||
/// </summary>
|
||||
public ObservableCollection<GameAccount>? GameAccounts { get => gameAccounts; set => SetProperty(ref gameAccounts, value); }
|
||||
public AdvancedCollectionView? GameAccountsView { get => gameAccountsView; set => SetProperty(ref gameAccountsView, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 选中的账号
|
||||
@@ -39,31 +42,7 @@ internal sealed partial class LaunchGameViewModelSlim : Abstraction.ViewModelSli
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OpenUIAsync()
|
||||
{
|
||||
ObservableCollection<GameAccount> 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<GameAccount> 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<LaunchStatus>()).ConfigureAwait(false);
|
||||
IProgress<LaunchStatus> launchProgress = progressFactory.CreateForMainThread<LaunchStatus>(status => launchStatusOptions.LaunchStatus = status);
|
||||
await gameService.LaunchAsync(launchProgress).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user