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>
|
||||
|
||||
@@ -2078,4 +2078,7 @@
|
||||
<data name="ServiceGachaUIGFImportLanguageNotMatch" xml:space="preserve">
|
||||
<value>UIGF 文件的语言:{0} 与胡桃的语言:{1} 不匹配,请切换到对应语言重试</value>
|
||||
</data>
|
||||
<data name="ServiceGamePackageRequestScatteredFileFailed" xml:space="preserve">
|
||||
<value>下载客户端文件失败:{0}</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -20,35 +20,23 @@ internal readonly struct ItemOperationInfo
|
||||
/// <summary>
|
||||
/// 目标文件
|
||||
/// </summary>
|
||||
public readonly string Target;
|
||||
public readonly VersionItem Remote;
|
||||
|
||||
/// <summary>
|
||||
/// 移动至中时的名称
|
||||
/// </summary>
|
||||
public readonly string MoveTo;
|
||||
|
||||
/// <summary>
|
||||
/// 文件的目标Md5
|
||||
/// </summary>
|
||||
public readonly string Md5;
|
||||
|
||||
/// <summary>
|
||||
/// 文件的目标大小 Byte
|
||||
/// </summary>
|
||||
public readonly long TotalBytes;
|
||||
public readonly VersionItem Local;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的包操作
|
||||
/// </summary>
|
||||
/// <param name="type">操作类型</param>
|
||||
/// <param name="target">目标</param>
|
||||
/// <param name="moveTo">缓存</param>
|
||||
public ItemOperationInfo(ItemOperationType type, VersionItem target, VersionItem moveTo)
|
||||
/// <param name="remote">远程</param>
|
||||
/// <param name="local">本地</param>
|
||||
public ItemOperationInfo(ItemOperationType type, VersionItem remote, VersionItem local)
|
||||
{
|
||||
Type = type;
|
||||
Target = target.RemoteName;
|
||||
MoveTo = moveTo.RemoteName;
|
||||
Md5 = target.Md5;
|
||||
TotalBytes = target.FileSize;
|
||||
Remote = remote;
|
||||
Local = local;
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ namespace Snap.Hutao.Service.Game.Package;
|
||||
internal enum ItemOperationType
|
||||
{
|
||||
/// <summary>
|
||||
/// 添加
|
||||
/// 需要备份
|
||||
/// </summary>
|
||||
Add = 0,
|
||||
Backup = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 替换
|
||||
@@ -20,7 +20,7 @@ internal enum ItemOperationType
|
||||
Replace = 1,
|
||||
|
||||
/// <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.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.IO;
|
||||
@@ -10,6 +11,7 @@ using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using static Snap.Hutao.Service.Game.GameConstants;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Package;
|
||||
@@ -23,9 +25,11 @@ namespace Snap.Hutao.Service.Game.Package;
|
||||
internal sealed partial class PackageConverter
|
||||
{
|
||||
private const string PackageVersion = "pkg_version";
|
||||
|
||||
private const string OverseaFolder = "Oversea";
|
||||
private const string ChineseFolder = "Chinese";
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
/// <summary>
|
||||
@@ -57,15 +61,26 @@ internal sealed partial class PackageConverter
|
||||
// 4. 全部资源下载完成后,根据操作信息项,进行文件替换
|
||||
// 处理顺序:备份/替换/新增
|
||||
// 替换操作等于 先备份国服文件,随后新增国际服文件
|
||||
|
||||
// 准备下载链接
|
||||
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
|
||||
Uri pkgVersionUri = $"{scatteredFilesUrl}/{PackageVersion}".ToUri();
|
||||
ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese;
|
||||
string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}";
|
||||
|
||||
PackageConvertContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
|
||||
|
||||
// Step 1
|
||||
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
|
||||
Dictionary<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUri).ConfigureAwait(false);
|
||||
Dictionary<string, VersionItem> localItems = await GetLocalItemsAsync(gameFolder, direction).ConfigureAwait(false);
|
||||
Dictionary<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)!);
|
||||
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
|
||||
{
|
||||
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)
|
||||
@@ -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 目录
|
||||
TryRenameDataFolder(gameFolder, direction);
|
||||
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion)))
|
||||
{
|
||||
return await GetVersionItemsAsync(localSteam).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache folder
|
||||
Core.RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<Core.RuntimeOptions>();
|
||||
string cacheFolder = Path.Combine(runtimeOptions.DataFolder, "ServerCache");
|
||||
|
||||
// 执行下载与移动操作
|
||||
private async ValueTask PrepareCacheFilesAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
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)
|
||||
{
|
||||
case ItemOperationType.Backup:
|
||||
MoveToCache(moveToFilePath, targetFilePath);
|
||||
break;
|
||||
continue;
|
||||
case ItemOperationType.Replace:
|
||||
MoveToCache(moveToFilePath, targetFilePath);
|
||||
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info, progress).ConfigureAwait(false);
|
||||
break;
|
||||
case ItemOperationType.Add:
|
||||
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info, progress).ConfigureAwait(false);
|
||||
break;
|
||||
default:
|
||||
await SkipOrDownloadAsync(info, context, progress).ConfigureAwait(false);
|
||||
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.Md5 == remoteMd5.ToLowerInvariant() && new FileInfo(cacheFilePath).Length == info.TotalBytes)
|
||||
if (info.Remote.FileSize == new FileInfo(cacheFile).Length)
|
||||
{
|
||||
// Valid, move it to target path
|
||||
// There shouldn't be any file in the path/name
|
||||
File.Move(cacheFilePath, targetFilePath, false);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invalid file, delete it
|
||||
File.Delete(cacheFilePath);
|
||||
string cacheMd5 = await MD5.HashFileAsync(cacheFile).ConfigureAwait(false);
|
||||
if (info.Remote.Md5.Equals(cacheMd5, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Invalid file, delete it
|
||||
File.Delete(cacheFile);
|
||||
}
|
||||
|
||||
// Cache no item, download it anyway.
|
||||
while (true)
|
||||
// Cache no matching item, download
|
||||
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;
|
||||
using (Stream webStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
|
||||
try
|
||||
{
|
||||
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));
|
||||
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);
|
||||
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.
|
||||
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);
|
||||
|
||||
@@ -316,66 +356,11 @@ internal sealed partial class PackageConverter
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
[JsonPropertyName("remoteName")]
|
||||
public string RemoteName { get; set; } = default!;
|
||||
public string RelativePath { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// MD5校验值
|
||||
|
||||
Reference in New Issue
Block a user