mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
fix #811
This commit is contained in:
@@ -1500,6 +1500,15 @@ namespace Snap.Hutao.Resource.Localization {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找类似 下载客户端文件失败:{0} 的本地化字符串。
|
||||||
|
/// </summary>
|
||||||
|
internal static string ServiceGamePackageRequestScatteredFileFailed {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("ServiceGamePackageRequestScatteredFileFailed", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查找类似 无法找到游戏路径,请前往设置修改 的本地化字符串。
|
/// 查找类似 无法找到游戏路径,请前往设置修改 的本地化字符串。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -2078,4 +2078,7 @@
|
|||||||
<data name="ServiceGachaUIGFImportLanguageNotMatch" xml:space="preserve">
|
<data name="ServiceGachaUIGFImportLanguageNotMatch" xml:space="preserve">
|
||||||
<value>UIGF 文件的语言:{0} 与胡桃的语言:{1} 不匹配,请切换到对应语言重试</value>
|
<value>UIGF 文件的语言:{0} 与胡桃的语言:{1} 不匹配,请切换到对应语言重试</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ServiceGamePackageRequestScatteredFileFailed" xml:space="preserve">
|
||||||
|
<value>下载客户端文件失败:{0}</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
@@ -20,35 +20,23 @@ internal readonly struct ItemOperationInfo
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 目标文件
|
/// 目标文件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly string Target;
|
public readonly VersionItem Remote;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 移动至中时的名称
|
/// 移动至中时的名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly string MoveTo;
|
public readonly VersionItem Local;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 文件的目标Md5
|
|
||||||
/// </summary>
|
|
||||||
public readonly string Md5;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 文件的目标大小 Byte
|
|
||||||
/// </summary>
|
|
||||||
public readonly long TotalBytes;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造一个新的包操作
|
/// 构造一个新的包操作
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="type">操作类型</param>
|
/// <param name="type">操作类型</param>
|
||||||
/// <param name="target">目标</param>
|
/// <param name="remote">远程</param>
|
||||||
/// <param name="moveTo">缓存</param>
|
/// <param name="local">本地</param>
|
||||||
public ItemOperationInfo(ItemOperationType type, VersionItem target, VersionItem moveTo)
|
public ItemOperationInfo(ItemOperationType type, VersionItem remote, VersionItem local)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
Target = target.RemoteName;
|
Remote = remote;
|
||||||
MoveTo = moveTo.RemoteName;
|
Local = local;
|
||||||
Md5 = target.Md5;
|
|
||||||
TotalBytes = target.FileSize;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,9 @@ namespace Snap.Hutao.Service.Game.Package;
|
|||||||
internal enum ItemOperationType
|
internal enum ItemOperationType
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 添加
|
/// 需要备份
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Add = 0,
|
Backup = 0,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 替换
|
/// 替换
|
||||||
@@ -20,7 +20,7 @@ internal enum ItemOperationType
|
|||||||
Replace = 1,
|
Replace = 1,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 需要备份
|
/// 添加
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Backup = 2,
|
Add = 2,
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using static Snap.Hutao.Service.Game.GameConstants;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Game.Package;
|
||||||
|
|
||||||
|
internal readonly struct PackageConvertContext
|
||||||
|
{
|
||||||
|
public readonly string GameFolder;
|
||||||
|
public readonly string ServerCacheFolder;
|
||||||
|
|
||||||
|
public readonly string ServerCacheBackupFolder; // From
|
||||||
|
public readonly string ServerCacheTargetFolder; // To
|
||||||
|
|
||||||
|
public readonly string FromDataFolderName;
|
||||||
|
public readonly string ToDataFolderName;
|
||||||
|
public readonly string FromDataFolder;
|
||||||
|
public readonly string ToDataFolder;
|
||||||
|
|
||||||
|
public readonly string ScatteredFilesUrl;
|
||||||
|
public readonly string PkgVersionUrl;
|
||||||
|
|
||||||
|
public PackageConvertContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl)
|
||||||
|
{
|
||||||
|
GameFolder = gameFolder;
|
||||||
|
ServerCacheFolder = Path.Combine(dataFolder, "ServerCache");
|
||||||
|
|
||||||
|
string serverCacheOversea = Path.Combine(ServerCacheFolder, "Oversea");
|
||||||
|
string serverCacheChinese = Path.Combine(ServerCacheFolder, "Chinese");
|
||||||
|
(ServerCacheBackupFolder, ServerCacheTargetFolder) = isTargetOversea
|
||||||
|
? (serverCacheChinese, serverCacheOversea)
|
||||||
|
: (serverCacheOversea, serverCacheChinese);
|
||||||
|
|
||||||
|
(FromDataFolderName, ToDataFolderName) = isTargetOversea
|
||||||
|
? (YuanShenData, GenshinImpactData)
|
||||||
|
: (GenshinImpactData, YuanShenData);
|
||||||
|
|
||||||
|
(FromDataFolder, ToDataFolder) = (Path.Combine(GameFolder, FromDataFolderName), Path.Combine(GameFolder, ToDataFolderName));
|
||||||
|
|
||||||
|
ScatteredFilesUrl = scatteredFilesUrl;
|
||||||
|
PkgVersionUrl = $"{scatteredFilesUrl}/pkg_version";
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly string GetScatteredFilesUrl(string file)
|
||||||
|
{
|
||||||
|
return $"{ScatteredFilesUrl}/{file}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly string GetServerCacheBackupFilePath(string filePath)
|
||||||
|
{
|
||||||
|
return Path.Combine(ServerCacheBackupFolder, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly string GetServerCacheTargetFilePath(string filePath)
|
||||||
|
{
|
||||||
|
return Path.Combine(ServerCacheTargetFolder, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly string GetGameFolderFilePath(string filePath)
|
||||||
|
{
|
||||||
|
return Path.Combine(GameFolder, filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core;
|
||||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||||
using Snap.Hutao.Core.ExceptionService;
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Core.IO;
|
using Snap.Hutao.Core.IO;
|
||||||
@@ -10,6 +11,7 @@ using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using static Snap.Hutao.Service.Game.GameConstants;
|
using static Snap.Hutao.Service.Game.GameConstants;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Package;
|
namespace Snap.Hutao.Service.Game.Package;
|
||||||
@@ -23,9 +25,11 @@ namespace Snap.Hutao.Service.Game.Package;
|
|||||||
internal sealed partial class PackageConverter
|
internal sealed partial class PackageConverter
|
||||||
{
|
{
|
||||||
private const string PackageVersion = "pkg_version";
|
private const string PackageVersion = "pkg_version";
|
||||||
|
private const string OverseaFolder = "Oversea";
|
||||||
|
private const string ChineseFolder = "Chinese";
|
||||||
private readonly IServiceProvider serviceProvider;
|
private readonly IServiceProvider serviceProvider;
|
||||||
private readonly JsonSerializerOptions options;
|
private readonly JsonSerializerOptions options;
|
||||||
|
private readonly RuntimeOptions runtimeOptions;
|
||||||
private readonly HttpClient httpClient;
|
private readonly HttpClient httpClient;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -57,15 +61,26 @@ internal sealed partial class PackageConverter
|
|||||||
// 4. 全部资源下载完成后,根据操作信息项,进行文件替换
|
// 4. 全部资源下载完成后,根据操作信息项,进行文件替换
|
||||||
// 处理顺序:备份/替换/新增
|
// 处理顺序:备份/替换/新增
|
||||||
// 替换操作等于 先备份国服文件,随后新增国际服文件
|
// 替换操作等于 先备份国服文件,随后新增国际服文件
|
||||||
|
|
||||||
|
// 准备下载链接
|
||||||
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
|
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
|
||||||
Uri pkgVersionUri = $"{scatteredFilesUrl}/{PackageVersion}".ToUri();
|
string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}";
|
||||||
ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese;
|
|
||||||
|
|
||||||
|
PackageConvertContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
|
||||||
|
|
||||||
|
// Step 1
|
||||||
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
|
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
|
||||||
Dictionary<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUri).ConfigureAwait(false);
|
Dictionary<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
|
||||||
Dictionary<string, VersionItem> localItems = await GetLocalItemsAsync(gameFolder, direction).ConfigureAwait(false);
|
Dictionary<string, VersionItem> localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
|
||||||
|
|
||||||
IEnumerable<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).OrderBy(i => i.Type);
|
// Step 2
|
||||||
|
List<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
|
||||||
|
diffOperations.SortBy(i => i.Type);
|
||||||
|
|
||||||
|
// Step 3
|
||||||
|
await PrepareCacheFilesAsync(diffOperations, direction, scatteredFilesUrl, cacheFolder, progress).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Step 4
|
||||||
return await ReplaceGameResourceAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
|
return await ReplaceGameResourceAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,46 +173,39 @@ internal sealed partial class PackageConverter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TryRenameDataFolder(string gameFolder, ConvertDirection direction)
|
|
||||||
{
|
|
||||||
string yuanShenData = Path.Combine(gameFolder, YuanShenData);
|
|
||||||
string genshinImpactData = Path.Combine(gameFolder, GenshinImpactData);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_ = direction == ConvertDirection.ChineseToOversea
|
|
||||||
? DirectoryOperation.Move(yuanShenData, genshinImpactData)
|
|
||||||
: DirectoryOperation.Move(genshinImpactData, yuanShenData);
|
|
||||||
}
|
|
||||||
catch (IOException ex)
|
|
||||||
{
|
|
||||||
// Access to the path is denied.
|
|
||||||
// When user install the game in special folder like 'Program Files'
|
|
||||||
throw ThrowHelper.GameFileOperation(SH.ServiceGamePackageRenameDataFolderFailed, ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void MoveToCache(string cacheFilePath, string targetFullPath)
|
private static void MoveToCache(string cacheFilePath, string targetFullPath)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)!);
|
||||||
File.Move(targetFullPath, cacheFilePath, true);
|
File.Move(targetFullPath, cacheFilePath, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<Dictionary<string, VersionItem>> GetLocalItemsAsync(string gameFolder, ConvertDirection direction)
|
[GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")]
|
||||||
|
private static partial Regex DataFolderRegex();
|
||||||
|
|
||||||
|
private async ValueTask<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream)
|
||||||
{
|
{
|
||||||
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion)))
|
Dictionary<string, VersionItem> results = new();
|
||||||
|
using (StreamReader reader = new(stream))
|
||||||
{
|
{
|
||||||
return await GetLocalVersionItemsAsync(localSteam, direction).ConfigureAwait(false);
|
Regex dataFolderRegex = DataFolderRegex();
|
||||||
|
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } row && !string.IsNullOrEmpty(row))
|
||||||
|
{
|
||||||
|
VersionItem item = JsonSerializer.Deserialize<VersionItem>(row, options)!;
|
||||||
|
item.RelativePath = dataFolderRegex.Replace(item.RelativePath, "{0}");
|
||||||
|
results.Add(item.RelativePath, item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<Dictionary<string, VersionItem>> GetRemoteItemsAsync(Uri pkgVersionUri)
|
private async ValueTask<Dictionary<string, VersionItem>> GetRemoteItemsAsync(string pkgVersionUrl)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false))
|
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUrl).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
return await GetRemoteVersionItemsAsync(remoteSteam).ConfigureAwait(false);
|
return await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (IOException ex)
|
catch (IOException ex)
|
||||||
@@ -206,104 +214,136 @@ internal sealed partial class PackageConverter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<bool> ReplaceGameResourceAsync(IEnumerable<ItemOperationInfo> operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress<PackageReplaceStatus> progress)
|
private async ValueTask<Dictionary<string, VersionItem>> GetLocalItemsAsync(string gameFolder)
|
||||||
{
|
{
|
||||||
// 重命名 _Data 目录
|
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion)))
|
||||||
TryRenameDataFolder(gameFolder, direction);
|
{
|
||||||
|
return await GetVersionItemsAsync(localSteam).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cache folder
|
private async ValueTask PrepareCacheFilesAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
|
||||||
Core.RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<Core.RuntimeOptions>();
|
{
|
||||||
string cacheFolder = Path.Combine(runtimeOptions.DataFolder, "ServerCache");
|
|
||||||
|
|
||||||
// 执行下载与移动操作
|
|
||||||
foreach (ItemOperationInfo info in operations)
|
foreach (ItemOperationInfo info in operations)
|
||||||
{
|
{
|
||||||
progress.Report(new($"{info.Target}"));
|
|
||||||
|
|
||||||
string targetFilePath = Path.Combine(gameFolder, info.Target);
|
|
||||||
string cacheFilePath = Path.Combine(cacheFolder, info.Target);
|
|
||||||
string moveToFilePath = Path.Combine(cacheFolder, info.MoveTo);
|
|
||||||
|
|
||||||
switch (info.Type)
|
switch (info.Type)
|
||||||
{
|
{
|
||||||
case ItemOperationType.Backup:
|
case ItemOperationType.Backup:
|
||||||
MoveToCache(moveToFilePath, targetFilePath);
|
continue;
|
||||||
break;
|
|
||||||
case ItemOperationType.Replace:
|
case ItemOperationType.Replace:
|
||||||
MoveToCache(moveToFilePath, targetFilePath);
|
|
||||||
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info, progress).ConfigureAwait(false);
|
|
||||||
break;
|
|
||||||
case ItemOperationType.Add:
|
case ItemOperationType.Add:
|
||||||
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info, progress).ConfigureAwait(false);
|
await SkipOrDownloadAsync(info, context, progress).ConfigureAwait(false);
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新下载所有 *pkg_version 文件
|
|
||||||
await ReplacePackageVersionFilesAsync(scatteredFilesUrl, gameFolder).ConfigureAwait(false);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask ReplaceFromCacheOrWebAsync(string cacheFilePath, string targetFilePath, string scatteredFilesUrl, ItemOperationInfo info, IProgress<PackageReplaceStatus> progress)
|
private async ValueTask SkipOrDownloadAsync(ItemOperationInfo info, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
|
||||||
{
|
{
|
||||||
if (File.Exists(cacheFilePath))
|
// 还原正确的远程地址
|
||||||
|
string remoteName = string.Format(info.Remote.RelativePath, context.ToDataFolderName);
|
||||||
|
string cacheFile = context.GetServerCacheTargetFilePath(remoteName);
|
||||||
|
|
||||||
|
if (File.Exists(cacheFile))
|
||||||
{
|
{
|
||||||
string remoteMd5 = await MD5.HashFileAsync(cacheFilePath).ConfigureAwait(false);
|
if (info.Remote.FileSize == new FileInfo(cacheFile).Length)
|
||||||
if (info.Md5 == remoteMd5.ToLowerInvariant() && new FileInfo(cacheFilePath).Length == info.TotalBytes)
|
|
||||||
{
|
{
|
||||||
// Valid, move it to target path
|
string cacheMd5 = await MD5.HashFileAsync(cacheFile).ConfigureAwait(false);
|
||||||
// There shouldn't be any file in the path/name
|
if (info.Remote.Md5.Equals(cacheMd5, StringComparison.OrdinalIgnoreCase))
|
||||||
File.Move(cacheFilePath, targetFilePath, false);
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// Invalid file, delete it
|
|
||||||
File.Delete(cacheFilePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalid file, delete it
|
||||||
|
File.Delete(cacheFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache no item, download it anyway.
|
// Cache no matching item, download
|
||||||
while (true)
|
using (FileStream fileStream = File.Create(cacheFile))
|
||||||
{
|
{
|
||||||
using (FileStream fileStream = File.Create(targetFilePath))
|
string remoteUrl = context.GetScatteredFilesUrl(remoteName);
|
||||||
|
using (HttpResponseMessage response = await httpClient.GetAsync(remoteUrl, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
using (HttpResponseMessage response = await httpClient.GetAsync($"{scatteredFilesUrl}/{info.Target}", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
|
// This stream's length is incorrect,
|
||||||
|
// so we use length in the header
|
||||||
|
long totalBytes = response.Content.Headers.ContentLength ?? 0;
|
||||||
|
using (Stream webStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
long totalBytes = response.Content.Headers.ContentLength ?? 0;
|
try
|
||||||
using (Stream webStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
try
|
StreamCopyWorker<PackageReplaceStatus> streamCopyWorker = new(webStream, fileStream, bytesRead => new(remoteName, bytesRead, totalBytes));
|
||||||
|
await streamCopyWorker.CopyAsync(progress).ConfigureAwait(false);
|
||||||
|
fileStream.Position = 0;
|
||||||
|
string cacheMd5 = await MD5.HashAsync(fileStream).ConfigureAwait(false);
|
||||||
|
if (string.Equals(info.Remote.Md5, cacheMd5, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
StreamCopyWorker<PackageReplaceStatus> streamCopyWorker = new(webStream, fileStream, bytesRead => new(info.Target, bytesRead, totalBytes));
|
return;
|
||||||
await streamCopyWorker.CopyAsync(progress).ConfigureAwait(false);
|
|
||||||
fileStream.Position = 0;
|
|
||||||
string remoteMd5 = await MD5.HashAsync(fileStream).ConfigureAwait(false);
|
|
||||||
if (string.Equals(info.Md5, remoteMd5, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// System.IO.IOException: The response ended prematurely.
|
|
||||||
// System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream.
|
|
||||||
|
|
||||||
// We want to retry forever.
|
|
||||||
serviceProvider.GetRequiredService<IInfoBarService>().Error(ex);
|
|
||||||
await Delay.FromSeconds(2).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// System.IO.IOException: The response ended prematurely.
|
||||||
|
// System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream.
|
||||||
|
ThrowHelper.PackageConvert(string.Format(SH.ServiceGamePackageRequestScatteredFileFailed, remoteName), ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask ReplacePackageVersionFilesAsync(string scatteredFilesUrl, string gameFolder)
|
private async ValueTask<bool> ReplaceGameResourceAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
|
||||||
{
|
{
|
||||||
foreach (string versionFilePath in Directory.EnumerateFiles(gameFolder, "*pkg_version"))
|
// 重命名 _Data 目录
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DirectoryOperation.Move(context.FromDataFolder, context.ToDataFolder);
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
// Access to the path is denied.
|
||||||
|
// When user install the game in special folder like 'Program Files'
|
||||||
|
throw ThrowHelper.GameFileOperation(SH.ServiceGamePackageRenameDataFolderFailed, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行下载与移动操作
|
||||||
|
foreach (ItemOperationInfo info in operations)
|
||||||
|
{
|
||||||
|
progress.Report(new($"{info.Remote}"));
|
||||||
|
|
||||||
|
(bool backup, bool target) = info.Type switch
|
||||||
|
{
|
||||||
|
ItemOperationType.Backup => (true, false),
|
||||||
|
ItemOperationType.Replace => (true, true),
|
||||||
|
ItemOperationType.Add => (false, true),
|
||||||
|
_ => (false, false),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (backup)
|
||||||
|
{
|
||||||
|
string localFileName = string.Format(info.Local.RelativePath, context.FromDataFolder);
|
||||||
|
string localFilePath = context.GetGameFolderFilePath(localFileName);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(localFilePath)!);
|
||||||
|
File.Move(localFilePath, context.GetServerCacheBackupFilePath(localFileName), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target)
|
||||||
|
{
|
||||||
|
string targetFileName = string.Format(info.Remote.RelativePath, context.ToDataFolder);
|
||||||
|
string targetFilePath = context.GetGameFolderFilePath(targetFileName);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(targetFilePath)!);
|
||||||
|
File.Move(context.GetServerCacheTargetFilePath(targetFileName), targetFilePath, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新下载所有 *pkg_version 文件
|
||||||
|
await ReplacePackageVersionFilesAsync(context).ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask ReplacePackageVersionFilesAsync(PackageConvertContext context)
|
||||||
|
{
|
||||||
|
foreach (string versionFilePath in Directory.EnumerateFiles(context.GameFolder, "*pkg_version"))
|
||||||
{
|
{
|
||||||
string versionFileName = Path.GetFileName(versionFilePath);
|
string versionFileName = Path.GetFileName(versionFilePath);
|
||||||
|
|
||||||
@@ -316,66 +356,11 @@ internal sealed partial class PackageConverter
|
|||||||
|
|
||||||
using (FileStream versionFileStream = File.Create(versionFilePath))
|
using (FileStream versionFileStream = File.Create(versionFilePath))
|
||||||
{
|
{
|
||||||
using (Stream webStream = await httpClient.GetStreamAsync($"{scatteredFilesUrl}/{versionFileName}").ConfigureAwait(false))
|
using (Stream webStream = await httpClient.GetStreamAsync(context.GetScatteredFilesUrl(versionFileName)).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await webStream.CopyToAsync(versionFileStream).ConfigureAwait(false);
|
await webStream.CopyToAsync(versionFileStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<Dictionary<string, VersionItem>> GetRemoteVersionItemsAsync(Stream stream)
|
|
||||||
{
|
|
||||||
Dictionary<string, VersionItem> results = new();
|
|
||||||
using (StreamReader reader = new(stream))
|
|
||||||
{
|
|
||||||
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } raw)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(raw))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
VersionItem item = JsonSerializer.Deserialize<VersionItem>(raw, options)!;
|
|
||||||
results.Add(item.RemoteName, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask<Dictionary<string, VersionItem>> GetLocalVersionItemsAsync(Stream stream, ConvertDirection direction)
|
|
||||||
{
|
|
||||||
Dictionary<string, VersionItem> results = new();
|
|
||||||
|
|
||||||
using (StreamReader reader = new(stream))
|
|
||||||
{
|
|
||||||
while (await reader.ReadLineAsync().ConfigureAwait(false) is { } row)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(row))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
VersionItem item = JsonSerializer.Deserialize<VersionItem>(row, options)!;
|
|
||||||
|
|
||||||
string remoteName = item.RemoteName;
|
|
||||||
|
|
||||||
// 我们已经提前重命名了整个 Data 文件夹 所以需要将 RemoteName 中的 Data 同样替换
|
|
||||||
if (remoteName.StartsWith(YuanShenData) || remoteName.StartsWith(GenshinImpactData))
|
|
||||||
{
|
|
||||||
remoteName = direction switch
|
|
||||||
{
|
|
||||||
ConvertDirection.OverseaToChinese => $"{YuanShenData}{remoteName[GenshinImpactData.Length..]}",
|
|
||||||
ConvertDirection.ChineseToOversea => $"{GenshinImpactData}{remoteName[YuanShenData.Length..]}",
|
|
||||||
_ => remoteName,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
results.Add(remoteName, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ internal sealed class VersionItem
|
|||||||
/// 服务器上的名称
|
/// 服务器上的名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("remoteName")]
|
[JsonPropertyName("remoteName")]
|
||||||
public string RemoteName { get; set; } = default!;
|
public string RelativePath { get; set; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// MD5校验值
|
/// MD5校验值
|
||||||
|
|||||||
Reference in New Issue
Block a user