fix convert cache

This commit is contained in:
DismissedLight
2023-01-27 16:51:43 +08:00
parent 2518ae0b90
commit 01b7e58b3e
11 changed files with 209 additions and 87 deletions

View File

@@ -12,7 +12,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio&amp;comma DissmissedLight"
Version="1.3.13.0" />
Version="1.4.0.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -0,0 +1,35 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.Game;
/// <summary>
/// 游戏常量
/// </summary>
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 GenshinImpactFileName = "GenshinImpact.exe";
/// <summary>
/// 国服数据文件夹
/// </summary>
public const string YuanShenData = "YuanShen_Data";
/// <summary>
/// 国际服数据文件夹
/// </summary>
public const string GenshinImpactData = "GenshinImpact_Data";
}

View File

@@ -20,6 +20,7 @@ using Snap.Hutao.Web.Response;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game;
@@ -31,7 +32,6 @@ namespace Snap.Hutao.Service.Game;
internal class GameService : IGameService
{
private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}";
private const string ConfigFile = "config.ini";
private readonly IServiceScopeFactory scopeFactory;
private readonly IMemoryCache memoryCache;
@@ -148,7 +148,7 @@ internal class GameService : IGameService
public MultiChannel GetMultiChannel()
{
string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFile);
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
if (!File.Exists(configPath))
{
@@ -169,7 +169,7 @@ internal class GameService : IGameService
public bool SetMultiChannel(LaunchScheme scheme)
{
string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile);
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFileName);
List<IniElement> elements;
try
@@ -226,28 +226,39 @@ internal class GameService : IGameService
}
/// <inheritdoc/>
public async Task<bool> ReplaceGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
public async Task<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
{
string gamePath = GetGamePathSkipLocator();
string gameFolder = Path.GetDirectoryName(gamePath)!;
string gameFileName = Path.GetFileName(gamePath);
if (launchScheme.IsOversea && gameFileName == "GenshinImpact.exe")
progress.Report(new("查询游戏资源信息"));
Response<GameResource> response = await Ioc.Default
.GetRequiredService<ResourceClient>()
.GetResourceAsync(launchScheme)
.ConfigureAwait(false);
if (response.IsOk())
{
// Already that scheme, no need to replace files
return true;
}
else if (!launchScheme.IsOversea && gameFileName == "YuanShen.exe")
{
// Already that scheme, no need to replace files
GameResource resource = response.Data;
if (!LaunchSchemeMatchesExecutable(launchScheme, gameFileName))
{
await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false);
// We need to change the gamePath if we switched.
OverwriteGamePath(Path.Combine(gameFolder, launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName));
}
if (!launchScheme.IsOversea)
{
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
}
return true;
}
// TODO: we still need to handle the Bilibili scheme.
await packageConverter.ReplaceGameResourceAsync(launchScheme, gameFolder, progress).ConfigureAwait(false);
// We need to change the gamePath if we switch.
return true;
return false;
}
/// <inheritdoc/>
@@ -258,19 +269,19 @@ internal class GameService : IGameService
return true;
}
return Process.GetProcessesByName("YuanShen.exe").Any()
|| Process.GetProcessesByName("GenshinImpact.exe").Any();
return Process.GetProcessesByName(YuanShenFileName).Any()
|| Process.GetProcessesByName(GenshinImpactFileName).Any();
}
/// <inheritdoc/>
public async Task<ObservableCollection<GameAccount>> GetGameAccountCollectionAsync()
{
await ThreadHelper.SwitchToMainThreadAsync();
if (gameAccounts == null)
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await ThreadHelper.SwitchToMainThreadAsync();
gameAccounts = appDbContext.GameAccounts.AsNoTracking().ToObservableCollection();
}
}
@@ -431,4 +442,10 @@ internal class GameService : IGameService
await scope.ServiceProvider.GetRequiredService<AppDbContext>().GameAccounts.RemoveAndSaveAsync(gameAccount).ConfigureAwait(false);
}
}
private static bool LaunchSchemeMatchesExecutable(LaunchScheme launchScheme, string gameFileName)
{
return (launchScheme.IsOversea && gameFileName == GenshinImpactFileName)
|| (!launchScheme.IsOversea && gameFileName == YuanShenFileName);
}
}

View File

@@ -90,7 +90,7 @@ internal interface IGameService
/// <param name="launchScheme">目标启动方案</param>
/// <param name="progress">进度</param>
/// <returns>是否替换成功</returns>
Task<bool> ReplaceGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
Task<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
/// <summary>
/// 修改注册表中的账号信息

View File

@@ -21,7 +21,7 @@ internal class ItemOperationInfo
{
Type = type;
Target = target.RemoteName;
Cache = cache.RemoteName;
MoveTo = cache.RemoteName;
Md5 = target.Md5;
TotalBytes = target.FileSize;
}
@@ -39,7 +39,7 @@ internal class ItemOperationInfo
/// <summary>
/// 移动至中时的名称
/// </summary>
public string Cache { get; set; }
public string MoveTo { get; set; }
/// <summary>
/// 文件的目标Md5

View File

@@ -3,11 +3,14 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using Snap.Hutao.Web.Response;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Package;
@@ -17,9 +20,6 @@ namespace Snap.Hutao.Service.Game.Package;
[HttpClient(HttpClientConfigration.Default)]
internal class PackageConverter
{
private const string GenshinImpactData = "GenshinImpact_Data";
private const string YuanShenData = "YuanShen_Data";
private readonly ResourceClient resourceClient;
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
@@ -38,47 +38,102 @@ internal class PackageConverter
}
/// <summary>
/// 异步替换游戏资源
/// 异步检查替换游戏资源
/// 调用前需要确认本地文件与服务器上的不同
/// </summary>
/// <param name="targetScheme">目标启动方案</param>
/// <param name="gameResouce">游戏资源</param>
/// <param name="gameFolder">游戏目录</param>
/// <param name="progress">进度</param>
/// <returns>任务</returns>
public async Task<bool> ReplaceGameResourceAsync(LaunchScheme targetScheme, string gameFolder, IProgress<PackageReplaceStatus> progress)
/// <returns>替换结果与资源</returns>
public async Task EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResouce, string gameFolder, IProgress<PackageReplaceStatus> progress)
{
await ThreadHelper.SwitchToBackgroundAsync();
progress.Report(new("查询游戏资源信息"));
Response<GameResource> response = await resourceClient.GetResourceAsync(targetScheme).ConfigureAwait(false);
string scatteredFilesUrl = gameResouce.Game.Latest.DecompressedPath;
Uri pkgVersionUri = new($"{scatteredFilesUrl}/pkg_version");
ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese;
if (response.IsOk())
progress.Report(new("下载包版本信息"));
Dictionary<string, VersionItem> remoteItems;
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false))
{
GameResource remoteGameResouce = response.Data;
string scatteredFilesUrl = remoteGameResouce.Game.Latest.DecompressedPath;
Uri pkgVersionUri = new($"{scatteredFilesUrl}/pkg_version");
ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese;
progress.Report(new("下载包版本信息"));
Dictionary<string, VersionItem> remoteItems;
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false))
{
remoteItems = await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false);
}
Dictionary<string, VersionItem> localItems;
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, "pkg_version")))
{
localItems = await GetVersionItemsAsync(localSteam, direction, ConvertRemoteName).ConfigureAwait(false);
}
IEnumerable<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems);
var a = diffOperations.ToList();
await ReplaceGameResourceCoreAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
return true;
remoteItems = await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false);
}
return false;
Dictionary<string, VersionItem> localItems;
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, "pkg_version")))
{
localItems = await GetVersionItemsAsync(localSteam, direction, ConvertRemoteName).ConfigureAwait(false);
}
IEnumerable<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems);
await ReplaceGameResourceAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
}
/// <summary>
/// 检查过时文件与Sdk
/// </summary>
/// <param name="resource">游戏资源</param>
/// <param name="gameFolder">游戏文件夹</param>
/// <returns>任务</returns>
public async Task EnsureDeprecatedFilesAndSdkAsync(GameResource resource, string gameFolder)
{
if (resource.DeprecatedFiles != null)
{
foreach (NameMd5 file in resource.DeprecatedFiles)
{
string filePath = Path.Combine(gameFolder, file.Name);
if (File.Exists(filePath))
{
File.Move(filePath, $"{filePath}.backup");
}
}
}
string sdkDllBackup = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll.backup");
string sdkDll = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll");
string sdkVersionBackup = Path.Combine(gameFolder, YuanShenData, "sdk_pkg_version.backup");
string sdkVersion = Path.Combine(gameFolder, YuanShenData, "sdk_pkg_version");
// Only bilibili's sdk is not null
if (resource.Sdk != null)
{
if (File.Exists(sdkDllBackup) && File.Exists(sdkVersionBackup))
{
File.Move(sdkDllBackup, sdkDll, false);
File.Move(sdkVersionBackup, sdkVersion, false);
}
else
{
using (Stream sdkWebStream = await httpClient.GetStreamAsync(resource.Sdk.Path).ConfigureAwait(false))
{
using (ZipArchive zip = new(sdkWebStream))
{
foreach (ZipArchiveEntry entry in zip.Entries)
{
if (entry.CompressedLength != 0)
{
string targetPath = Path.Combine(gameFolder, entry.FullName);
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
entry.ExtractToFile(targetPath, true);
}
}
}
}
}
}
else
{
if (File.Exists(sdkDll))
{
File.Move(sdkDll, sdkDllBackup, true);
}
if (File.Exists(sdkVersion))
{
File.Move(sdkVersion, sdkVersionBackup, true);
}
}
}
private static string ConvertRemoteName(string remoteName, ConvertDirection direction)
@@ -143,14 +198,13 @@ internal class PackageConverter
}
}
private static void MoveToCache(string cacheFolder, string cacheName, string targetFullPath)
private static void MoveToCache(string cacheFilePath, string targetFullPath)
{
string cacheFilePath = Path.Combine(cacheFolder, cacheName);
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)!);
File.Move(targetFullPath, cacheFilePath, true);
}
private async Task ReplaceGameResourceCoreAsync(IEnumerable<ItemOperationInfo> operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress<PackageReplaceStatus> progress)
private async Task ReplaceGameResourceAsync(IEnumerable<ItemOperationInfo> operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress<PackageReplaceStatus> progress)
{
// 重命名 _Data 目录
RenameDataFolder(gameFolder, direction);
@@ -164,7 +218,8 @@ internal class PackageConverter
progress.Report(new($"{info.Target}"));
string targetFilePath = Path.Combine(gameFolder, info.Target);
string cacheFilePath = Path.Combine(cacheFolder, info.Cache);
string cacheFilePath = Path.Combine(cacheFolder, info.Target);
string moveToFilePath = Path.Combine(cacheFolder, info.MoveTo);
switch (info.Type)
{
@@ -173,14 +228,14 @@ internal class PackageConverter
break;
case ItemOperationType.Replace:
{
MoveToCache(cacheFolder, info.Cache, targetFilePath);
MoveToCache(moveToFilePath, targetFilePath);
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info).ConfigureAwait(false);
break;
}
case ItemOperationType.Remove:
MoveToCache(cacheFolder, info.Cache, targetFilePath);
MoveToCache(moveToFilePath, targetFilePath);
break;
default:
@@ -223,14 +278,22 @@ internal class PackageConverter
private async Task ReplacePackageVersionsAsync(string scatteredFilesUrl, string gameFolder)
{
foreach (string audioPkgVersionFilePath in Directory.EnumerateFiles(gameFolder, "*pkg_version"))
foreach (string versionFilePath in Directory.EnumerateFiles(gameFolder, "*pkg_version"))
{
string audioPkgVersionFileName = Path.GetFileName(audioPkgVersionFilePath);
using (FileStream audioPkgVersionFileStream = File.Create(audioPkgVersionFilePath))
string versionFileName = Path.GetFileName(versionFilePath);
if (versionFileName == "sdk_pkg_version")
{
using (Stream webStream = await httpClient.GetStreamAsync($"{scatteredFilesUrl}/{audioPkgVersionFileName}").ConfigureAwait(false))
// Skiping the sdk_pkg_version file,
// it can't be claimed from remote.
continue;
}
using (FileStream versionFileStream = File.Create(versionFilePath))
{
using (Stream webStream = await httpClient.GetStreamAsync($"{scatteredFilesUrl}/{versionFileName}").ConfigureAwait(false))
{
await webStream.CopyToAsync(audioPkgVersionFileStream).ConfigureAwait(false);
await webStream.CopyToAsync(versionFileStream).ConfigureAwait(false);
}
}
}

View File

@@ -22,7 +22,7 @@
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<PackageCertificateThumbprint>F8C2255969BEA4A681CED102771BF807856AEC02</PackageCertificateThumbprint>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<AppxAutoIncrementPackageRevision>True</AppxAutoIncrementPackageRevision>
<AppxAutoIncrementPackageRevision>False</AppxAutoIncrementPackageRevision>
<AppxSymbolPackageEnabled>True</AppxSymbolPackageEnabled>
<GenerateTestArtifacts>True</GenerateTestArtifacts>
<AppxBundle>Never</AppxBundle>

View File

@@ -1,4 +1,4 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<ContentDialog
@@ -8,16 +8,16 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shvd="using:Snap.Hutao.View.Dialog"
Title="转换客户端"
Title="转换客户端"
d:DataContext="{d:DesignInstance shvd:LaunchGamePackageConvertDialog}"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<StackPanel>
<TextBlock Text="转换可能需要花费一段时间"/>
<TextBlock Text="转换可能需要花费一段时间,请勿关闭胡桃"/>
<TextBlock
MinWidth="480"
Margin="0,8,0,2"
MinWidth="360"
Margin="0,8,0,16"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Description}"/>
<ProgressBar IsIndeterminate="True"/>

View File

@@ -1,4 +1,4 @@
// Copyright (c) DGP Studio. All rights reserved.
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
@@ -8,14 +8,14 @@ using Snap.Hutao.Control;
namespace Snap.Hutao.View.Dialog;
/// <summary>
/// 启动游戏客户端转换对话框
/// 启动游戏客户端转换对话框
/// </summary>
public sealed partial class LaunchGamePackageConvertDialog : ContentDialog
{
private static readonly DependencyProperty DescriptionProperty = Property<LaunchGamePackageConvertDialog>.Depend(nameof(Description), "请稍候");
private static readonly DependencyProperty DescriptionProperty = Property<LaunchGamePackageConvertDialog>.Depend(nameof(Description), "请稍候");
/// <summary>
/// 构造一个新的启动游戏客户端转换对话框
/// 构造一个新的启动游戏客户端转换对话框
/// </summary>
public LaunchGamePackageConvertDialog()
{
@@ -25,7 +25,7 @@ public sealed partial class LaunchGamePackageConvertDialog : ContentDialog
}
/// <summary>
/// 描述
/// 描述
/// </summary>
public string Description
{

View File

@@ -52,11 +52,23 @@
IsOpen="True"
Message="所有选项仅会在启动游戏成功后保存"
Severity="Informational"/>
<InfoBar
Margin="0,2,0,0"
IsClosable="False"
IsOpen="{Binding IsElevated, Converter={StaticResource BoolNegationConverter}}"
Message="某些选项处于禁用状态,它们只在管理员模式下生效!"
Severity="Warning"/>
<wsc:SettingsGroup Margin="0,0,0,0" Header="常规">
<InfoBar
IsClosable="False"
IsOpen="{Binding IsElevated}"
Message="切换国际服功能尚处于测试阶段,可能出现未预料的问题,请注意备份游戏资源!"
Severity="Error"/>
<wsc:Setting
Description="切换游戏服务器B服用户需要自备额外的 PCGameSDK.dll 文件"
Description="切换游戏服务器(国服/渠道服/国际服)"
Header="服务器"
Icon="&#xE8AB;">
Icon="&#xE8AB;"
IsEnabled="{Binding IsElevated}">
<wsc:Setting.ActionContent>
<ComboBox
DisplayMemberPath="DisplayName"
@@ -217,11 +229,6 @@
</wsc:SettingsGroup>
<wsc:SettingsGroup Header="高级功能" IsEnabled="{Binding IsElevated}">
<InfoBar
IsClosable="False"
IsOpen="True"
Message="需要读写游戏进程或与游戏窗体交互,因此只在管理员模式下生效!"
Severity="Warning"/>
<wsc:Setting
Description="在启动游戏前尝试终止运行中的游戏进程"
Header="快速切换账号"
@@ -236,7 +243,7 @@
</wsc:Setting>
<InfoBar
IsClosable="False"
IsOpen="True"
IsOpen="{Binding IsElevated}"
Message="下面的功能十分危险,如果您不愿承担因此可能带来的后果,请勿启用!"
Severity="Error"/>
<wsc:Setting

View File

@@ -308,7 +308,7 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
await using (await dialog.BlockAsync().ConfigureAwait(false))
{
Progress<Service.Game.Package.PackageReplaceStatus> progress = new(s => dialog.Description = s.Description);
await gameService.ReplaceGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false);
await gameService.EnsureGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false);
}
}