add package convert check

This commit is contained in:
DismissedLight
2024-01-06 15:21:51 +08:00
parent 88b8335e5b
commit 8b6f95c3d9
17 changed files with 194 additions and 145 deletions

View File

@@ -11,11 +11,14 @@ namespace Snap.Hutao.Core.IO.Ini;
[HighQuality]
internal static class IniSerializer
{
/// <summary>
/// 反序列化
/// </summary>
/// <param name="fileStream">文件流</param>
/// <returns>Ini 元素集合</returns>
public static List<IniElement> DeserializeFromFile(string filePath)
{
using (FileStream readStream = File.OpenRead(filePath))
{
return Deserialize(readStream);
}
}
public static List<IniElement> Deserialize(FileStream fileStream)
{
List<IniElement> results = [];
@@ -50,11 +53,14 @@ internal static class IniSerializer
return results;
}
/// <summary>
/// 序列化
/// </summary>
/// <param name="fileStream">写入的流</param>
/// <param name="elements">元素</param>
public static void SerializeToFile(string filePath, IEnumerable<IniElement> elements)
{
using (FileStream writeStream = File.Create(filePath))
{
Serialize(writeStream, elements);
}
}
public static void Serialize(FileStream fileStream, IEnumerable<IniElement> elements)
{
using (StreamWriter writer = new(fileStream))

View File

@@ -29,10 +29,9 @@ internal readonly struct ChannelOptions
/// </summary>
public readonly bool IsOversea;
/// <summary>
/// 配置文件路径 当不为 null 时则存在文件读写问题
/// </summary>
public readonly string? ConfigFilePath;
public readonly ChannelOptionsErrorKind ErrorKind;
public readonly string? FilePath;
public ChannelOptions(ChannelType channel, SubChannelType subChannel, bool isOversea)
{
@@ -48,15 +47,20 @@ internal readonly struct ChannelOptions
IsOversea = isOversea;
}
private ChannelOptions(bool isOversea, string? configFilePath)
private ChannelOptions(ChannelOptionsErrorKind errorKind,string? filePath)
{
IsOversea = isOversea;
ConfigFilePath = configFilePath;
ErrorKind = errorKind;
FilePath = filePath;
}
public static ChannelOptions FileNotFound(bool isOversea, string configFilePath)
public static ChannelOptions ConfigurationFileNotFound(string filePath)
{
return new(isOversea, configFilePath);
return new(ChannelOptionsErrorKind.ConfigurationFileNotFound, filePath);
}
public static ChannelOptions GamePathNullOrEmpty()
{
return new(ChannelOptionsErrorKind.GamePathNullOrEmpty, string.Empty);
}
/// <inheritdoc/>

View File

@@ -0,0 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game.Configuration;
internal enum ChannelOptionsErrorKind
{
None,
ConfigurationFileNotFound,
GamePathNullOrEmpty,
}

View File

@@ -17,25 +17,22 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
public ChannelOptions GetChannelOptions()
{
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath))
if (!launchOptions.TryGetGameFileSystem(out GameFileSystem? gameFileSystem))
{
throw ThrowHelper.InvalidOperation($"Invalid game path: {gamePath}");
return ChannelOptions.GamePathNullOrEmpty();
}
bool isOversea = LaunchScheme.ExecutableIsOversea(Path.GetFileName(gamePath));
bool isOversea = LaunchScheme.ExecutableIsOversea(gameFileSystem.GameFileName);
if (!File.Exists(configPath))
if (!File.Exists(gameFileSystem.GameConfigFilePath))
{
return ChannelOptions.FileNotFound(isOversea, configPath);
return ChannelOptions.ConfigurationFileNotFound(gameFileSystem.GameConfigFilePath);
}
using (FileStream stream = File.OpenRead(configPath))
{
List<IniParameter> parameters = IniSerializer.Deserialize(stream).OfType<IniParameter>().ToList();
string? channel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.ChannelName)?.Value;
string? subChannel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.SubChannelName)?.Value;
List<IniParameter> parameters = IniSerializer.DeserializeFromFile(gameFileSystem.GameConfigFilePath).OfType<IniParameter>().ToList();
string? channel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.ChannelName)?.Value;
string? subChannel = parameters.FirstOrDefault(p => p.Key == ChannelOptions.SubChannelName)?.Value;
return new(channel, subChannel, isOversea);
}
return new(channel, subChannel, isOversea);
}
}

View File

@@ -10,6 +10,7 @@ namespace Snap.Hutao.Service.Game;
internal static class GameConstants
{
public const string ConfigFileName = "config.ini";
public const string PCGameSDKFilePath = @"YuanShen_Data\Plugins\PCGameSDK.dll";
public const string YuanShenFileName = "YuanShen.exe";
public const string YuanShenFileNameUpper = "YUANSHEN.EXE";
public const string GenshinImpactFileName = "GenshinImpact.exe";

View File

@@ -0,0 +1,39 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Service.Game;
internal sealed class GameFileSystem
{
private readonly string gameFilePath;
private string? gameFileName;
private string? gameDirectory;
private string? gameConfigFilePath;
private string? pcGameSDKFilePath;
public GameFileSystem(string gameFilePath)
{
this.gameFilePath = gameFilePath;
}
public string GameFilePath { get => gameFilePath; }
public string GameFileName { get => gameFileName ??= Path.GetFileName(gameFilePath); }
public string GameDirectory
{
get
{
gameDirectory ??= Path.GetDirectoryName(gameFilePath);
ArgumentException.ThrowIfNullOrEmpty(gameDirectory);
return gameDirectory;
}
}
public string GameConfigFilePath { get => gameConfigFilePath ??= Path.Combine(GameDirectory, GameConstants.ConfigFileName); }
public string PCGameSDKFilePath { get => pcGameSDKFilePath ?? Path.Combine(GameDirectory, GameConstants.PCGameSDKFilePath); }
}

View File

@@ -9,64 +9,20 @@ namespace Snap.Hutao.Service.Game;
internal static class LaunchOptionsExtension
{
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)
public static bool TryGetGameFileSystem(this LaunchOptions options, [NotNullWhen(true)] out GameFileSystem? fileSystem)
{
string gamePath = options.GamePath;
gameDirectory = Path.GetDirectoryName(gamePath);
if (string.IsNullOrEmpty(gameDirectory))
{
gameFileName = default;
return false;
}
gameFileName = Path.GetFileName(gamePath);
if (string.IsNullOrEmpty(gameFileName))
if (string.IsNullOrEmpty(gamePath))
{
fileSystem = default;
return false;
}
fileSystem = new GameFileSystem(gamePath);
return true;
}
public static bool TryGetGamePathAndGameFileName(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameFileName)
{
gamePath = options.GamePath;
gameFileName = Path.GetFileName(gamePath);
if (string.IsNullOrEmpty(gameFileName))
{
return false;
}
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

@@ -5,6 +5,7 @@ using Microsoft.Win32.SafeHandles;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Progress;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.Service.Game.PathAbstraction;
using Snap.Hutao.View.Dialog;
@@ -19,38 +20,68 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
IServiceProvider serviceProvider = context.ServiceProvider;
IContentDialogFactory contentDialogFactory = serviceProvider.GetRequiredService<IContentDialogFactory>();
IProgressFactory progressFactory = serviceProvider.GetRequiredService<IProgressFactory>();
LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
IProgress<PackageConvertStatus> convertProgress = progressFactory.CreateForMainThread<PackageConvertStatus>(state => dialog.State = state);
using (await dialog.BlockAsync(context.TaskContext).ConfigureAwait(false))
if (!context.TryGetGameFileSystem(out GameFileSystem? gameFileSystem))
{
if (!await EnsureGameResourceAsync(context, convertProgress).ConfigureAwait(false))
{
// context.Result is set in EnsureGameResourceAsync
return;
}
return;
}
await context.TaskContext.SwitchToMainThreadAsync();
ImmutableList<GamePathEntry> gamePathEntries = context.Options.GetGamePathEntries(out GamePathEntry? selected);
context.ViewModel.SetGamePathEntriesAndSelectedGamePathEntry(gamePathEntries, selected);
if (ShouldConvert(context, gameFileSystem))
{
IServiceProvider serviceProvider = context.ServiceProvider;
IContentDialogFactory contentDialogFactory = serviceProvider.GetRequiredService<IContentDialogFactory>();
IProgressFactory progressFactory = serviceProvider.GetRequiredService<IProgressFactory>();
LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
IProgress<PackageConvertStatus> convertProgress = progressFactory.CreateForMainThread<PackageConvertStatus>(state => dialog.State = state);
using (await dialog.BlockAsync(context.TaskContext).ConfigureAwait(false))
{
if (!await EnsureGameResourceAsync(context, gameFileSystem, convertProgress).ConfigureAwait(false))
{
// context.Result is set in EnsureGameResourceAsync
return;
}
await context.TaskContext.SwitchToMainThreadAsync();
ImmutableList<GamePathEntry> gamePathEntries = context.Options.GetGamePathEntries(out GamePathEntry? selected);
context.ViewModel.SetGamePathEntriesAndSelectedGamePathEntry(gamePathEntries, selected);
}
}
await next().ConfigureAwait(false);
}
private static async ValueTask<bool> EnsureGameResourceAsync(LaunchExecutionContext context, IProgress<PackageConvertStatus> progress)
private static bool ShouldConvert(LaunchExecutionContext context, GameFileSystem gameFileSystem)
{
if (!context.Options.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName))
// Configuration file changed
if (context.ChannelOptionsChanged)
{
context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
return false;
return true;
}
// Executable name not match
if (!context.Scheme.ExecutableMatches(gameFileSystem.GameFileName))
{
return true;
}
if (!context.Scheme.IsOversea)
{
// [It's Bilibili channel xor PCGameSDK.dll exists] means we need to convert
if (context.Scheme.Channel is ChannelType.Bili ^ File.Exists(gameFileSystem.PCGameSDKFilePath))
{
return true;
}
}
return false;
}
private static async ValueTask<bool> EnsureGameResourceAsync(LaunchExecutionContext context, GameFileSystem gameFileSystem, IProgress<PackageConvertStatus> progress)
{
string gameFolder = gameFileSystem.GameDirectory;
string gameFileName = gameFileSystem.GameFileName;
context.Logger.LogInformation("Game folder: {GameFolder}", gameFolder);
if (!CheckDirectoryPermissions(gameFolder))

View File

@@ -3,7 +3,7 @@
namespace Snap.Hutao.Service.Game.Launching.Handler;
internal sealed class LaunchExecutionEnsureSchemeNotExistsHandler : ILaunchExecutionDelegateHandler
internal sealed class LaunchExecutionEnsureSchemeHandler : ILaunchExecutionDelegateHandler
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
@@ -14,7 +14,7 @@ internal sealed class LaunchExecutionEnsureSchemeNotExistsHandler : ILaunchExecu
return;
}
context.Logger.LogInformation("Scheme[{Scheme}] is selected", context.Scheme.DisplayName);
context.Logger.LogInformation("Scheme [{Scheme}] is selected", context.Scheme.DisplayName);
await next().ConfigureAwait(false);
}
}

View File

@@ -10,21 +10,19 @@ internal sealed class LaunchExecutionGameProcessInitializationHandler : ILaunchE
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
if (!context.Options.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
if (!context.TryGetGameFileSystem(out GameFileSystem? gameFileSystem))
{
context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
return;
}
context.Progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
using (context.Process = InitializeGameProcess(context, gamePath))
using (context.Process = InitializeGameProcess(context, gameFileSystem))
{
await next().ConfigureAwait(false);
}
}
private static System.Diagnostics.Process InitializeGameProcess(LaunchExecutionContext context, string gamePath)
private static System.Diagnostics.Process InitializeGameProcess(LaunchExecutionContext context, GameFileSystem gameFileSystem)
{
LaunchOptions launchOptions = context.Options;
@@ -51,10 +49,10 @@ internal sealed class LaunchExecutionGameProcessInitializationHandler : ILaunchE
StartInfo = new()
{
Arguments = commandLine,
FileName = gamePath,
FileName = gameFileSystem.GameFilePath,
UseShellExecute = true,
Verb = "runas",
WorkingDirectory = Path.GetDirectoryName(gamePath),
WorkingDirectory = gameFileSystem.GameDirectory,
},
};
}

View File

@@ -11,22 +11,19 @@ internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecution
{
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
{
if (!context.Options.TryGetGamePathAndFilePathByName(GameConstants.ConfigFileName, out string gamePath, out string? configPath))
if (!context.TryGetGameFileSystem(out GameFileSystem? gameFileSystem))
{
context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
// context.Result is set in TryGetGameFileSystem
return;
}
string configPath = gameFileSystem.GameConfigFilePath;
context.Logger.LogInformation("Game config file path: {ConfigPath}", configPath);
List<IniElement> elements = default!;
try
{
using (FileStream readStream = File.OpenRead(configPath))
{
elements = [.. IniSerializer.Deserialize(readStream)];
}
elements = [.. IniSerializer.DeserializeFromFile(configPath)];
}
catch (FileNotFoundException)
{
@@ -47,32 +44,27 @@ internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecution
return;
}
bool changed = false;
foreach (IniElement element in elements)
{
if (element is IniParameter parameter)
{
if (parameter.Key is ChannelOptions.ChannelName)
{
changed = parameter.Set(context.Scheme.Channel.ToString("D")) || changed;
context.ChannelOptionsChanged = parameter.Set(context.Scheme.Channel.ToString("D")) || context.ChannelOptionsChanged;
continue;
}
if (parameter.Key is ChannelOptions.SubChannelName)
{
changed = parameter.Set(context.Scheme.SubChannel.ToString("D")) || changed;
context.ChannelOptionsChanged = parameter.Set(context.Scheme.SubChannel.ToString("D")) || context.ChannelOptionsChanged;
continue;
}
}
}
if (changed)
if (context.ChannelOptionsChanged)
{
using (FileStream writeStream = File.Create(configPath))
{
IniSerializer.Serialize(writeStream, elements);
}
IniSerializer.SerializeToFile(configPath, elements);
}
await next().ConfigureAwait(false);

View File

@@ -15,6 +15,8 @@ internal sealed partial class LaunchExecutionContext
private readonly ITaskContext taskContext;
private readonly LaunchOptions options;
private GameFileSystem? gameFileSystem;
[SuppressMessage("", "SH007")]
public LaunchExecutionContext(IServiceProvider serviceProvider, IViewModelSupportLaunchExecution viewModel, LaunchScheme? scheme, GameAccount? account)
: this(serviceProvider)
@@ -36,13 +38,34 @@ internal sealed partial class LaunchExecutionContext
public LaunchOptions Options { get => options; }
public IViewModelSupportLaunchExecution ViewModel { get; set; } = default!;
public IViewModelSupportLaunchExecution ViewModel { get; private set; } = default!;
public LaunchScheme Scheme { get; set; } = default!;
public LaunchScheme Scheme { get; private set; } = default!;
public GameAccount? Account { get; set; }
public GameAccount? Account { get; private set; }
public bool ChannelOptionsChanged { get; set; }
public IProgress<LaunchStatus> Progress { get; set; } = default!;
public System.Diagnostics.Process Process { get; set; } = default!;
public bool TryGetGameFileSystem([NotNullWhen(true)] out GameFileSystem? gameFileSystem)
{
if (this.gameFileSystem is not null)
{
gameFileSystem = this.gameFileSystem;
return true;
}
if (!Options.TryGetGameFileSystem(out gameFileSystem))
{
Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
return false;
}
this.gameFileSystem = gameFileSystem;
return true;
}
}

View File

@@ -15,7 +15,7 @@ internal sealed class LaunchExecutionInvoker
{
handlers = [];
handlers.Enqueue(new LaunchExecutionEnsureGameNotRunningHandler());
handlers.Enqueue(new LaunchExecutionEnsureSchemeNotExistsHandler());
handlers.Enqueue(new LaunchExecutionEnsureSchemeHandler());
handlers.Enqueue(new LaunchExecutionSetChannelOptionsHandler());
handlers.Enqueue(new LaunchExecutionEnsureGameResourceHandler());
handlers.Enqueue(new LaunchExecutionSetGameAccountHandler());

View File

@@ -93,7 +93,6 @@ internal sealed partial class PackageConverter
ZipFile.ExtractToDirectory(sdkWebStream, gameFolder, true);
}
// TODO: verify sdk md5
if (File.Exists(sdkDllBackup) && File.Exists(sdkVersionBackup))
{
File.Delete(sdkDllBackup);

View File

@@ -9,7 +9,7 @@ internal sealed class GamePathEntry
public string Path { get; set; } = default!;
[JsonIgnore]
public GamePathKind Kind { get => GetKind(Path); }
public GamePathEntryKind Kind { get => GetKind(Path); }
public static GamePathEntry Create(string path)
{
@@ -19,8 +19,8 @@ internal sealed class GamePathEntry
};
}
private static GamePathKind GetKind(string path)
private static GamePathEntryKind GetKind(string path)
{
return GamePathKind.None;
return GamePathEntryKind.None;
}
}

View File

@@ -3,7 +3,7 @@
namespace Snap.Hutao.Service.Game.PathAbstraction;
internal enum GamePathKind
internal enum GamePathEntryKind
{
None,
ChineseClient,

View File

@@ -13,17 +13,9 @@ internal static class LaunchGameShared
{
public static LaunchScheme? GetCurrentLaunchSchemeFromConfigFile(IGameServiceFacade gameService, IInfoBarService infoBarService)
{
ChannelOptions options;
try
{
options = gameService.GetChannelOptions();
}
catch (InvalidOperationException)
{
return default;
}
ChannelOptions options = gameService.GetChannelOptions();
if (string.IsNullOrEmpty(options.ConfigFilePath))
if (options.ErrorKind is ChannelOptionsErrorKind.None)
{
try
{
@@ -40,7 +32,7 @@ internal static class LaunchGameShared
}
else
{
infoBarService.Warning(SH.FormatViewModelLaunchGameMultiChannelReadFail(options.ConfigFilePath));
infoBarService.Warning($"{options.ErrorKind}", SH.FormatViewModelLaunchGameMultiChannelReadFail(options.FilePath));
}
return default;