refactor Launch Game Pipeline

This commit is contained in:
Lightczx
2023-12-28 17:06:45 +08:00
parent 58e4d1b90e
commit 9fdedd78d0
33 changed files with 423 additions and 374 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Service.Game.Package;
/// 包文件操作的类型
/// </summary>
[HighQuality]
internal enum ItemOperationType
internal enum PackageItemOperationType
{
/// <summary>
/// 需要备份

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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