Skip to content

Commit fddfd0e

Browse files
authored
[ZZZ Install] #850 Fix deleted assets getting redownloaded on update (#854)
# Main Goal This change contains a bug fix where the deleted assets inside the game get redownloaded during a game update. Previously, the filtering for the deleted assets were done by comparing the URL which contains the ``MatchingField`` string. However, the URL structure has changed and the ``MatchingField`` string got removed, therefore the launcher is unable to identify which assets can be ignored. We are adding a new class inside our Sophon submodule called ``SophonIdentifiableProperty``, which contains some necessary fields needed to compare the asset's category and where the asset came from. See related Issue: #850 ## PR Status : - Overall Status : Done - Commits : Done - Synced to base (Collapse:main) : Yes - Build status : OK - Crashing : No - Bug found caused by PR : N/A ### Templates <details> <summary>Changelog Prefixes</summary> ``` **[New]** **[Imp]** **[Fix]** **[Loc]** **[Doc]** ``` </details>
2 parents 4d2a248 + 1fd5c87 commit fddfd0e

File tree

7 files changed

+99
-107
lines changed

7 files changed

+99
-107
lines changed

CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,7 @@ internal static FileInfo EnsureCreationOfDirectory(this FileInfo filePath)
142142

143143
try
144144
{
145-
if (directoryInfo is { Exists: false })
146-
directoryInfo.Create();
147-
145+
directoryInfo?.Create();
148146
return filePath;
149147
}
150148
finally

CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.Sophon.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -555,7 +555,7 @@ private async Task ConfirmAdditionalInstallDataPackageFiles(
555555
.Where(x => matchingFieldsList.Contains(x.MatchingField, StringComparer.OrdinalIgnoreCase))
556556
.Sum(x =>
557557
{
558-
var firstTag = x.ChunkInfo;
558+
SophonManifestChunkInfo? firstTag = x.ChunkInfo;
559559
return firstTag?.CompressedSize ?? 0;
560560
});
561561

@@ -968,7 +968,9 @@ private ValueTask RunSophonAssetDownloadThread(HttpClient client,
968968
}
969969

970970
// Get the target and temp file info
971-
FileInfo existingFileInfo = new FileInfo(filePath).EnsureNoReadOnly();
971+
FileInfo existingFileInfo = new FileInfo(filePath)
972+
.EnsureCreationOfDirectory()
973+
.EnsureNoReadOnly();
972974

973975
return asset.WriteToStreamAsync(client,
974976
assetSize => existingFileInfo.Open(new FileStreamOptions
@@ -1001,9 +1003,13 @@ private ValueTask RunSophonAssetUpdateThread(HttpClient client,
10011003

10021004
// Get the target and temp file info
10031005
FileInfo existingFileInfo =
1004-
new FileInfo(filePath).EnsureNoReadOnly(out bool isExistingFileInfoExist);
1006+
new FileInfo(filePath)
1007+
.EnsureCreationOfDirectory()
1008+
.EnsureNoReadOnly(out bool isExistingFileInfoExist);
10051009
FileInfo sophonFileInfo =
1006-
new FileInfo(filePath + "_tempSophon").EnsureNoReadOnly(out bool isSophonFileInfoExist);
1010+
new FileInfo(filePath + "_tempSophon")
1011+
.EnsureCreationOfDirectory()
1012+
.EnsureNoReadOnly(out bool isSophonFileInfoExist);
10071013

10081014
// Use "_tempSophon" if file is new or if "_tempSophon" file exist. Otherwise use original file if exist
10091015
if (!isExistingFileInfoExist || isSophonFileInfoExist

CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.SophonPatch.cs

Lines changed: 72 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
using CollapseLauncher.Extension;
1212
using CollapseLauncher.Helper;
1313
using CollapseLauncher.Helper.Metadata;
14-
using CollapseLauncher.Interfaces;
14+
using CollapseLauncher.Helper.StreamUtility;
1515
using Hi3Helper;
1616
using Hi3Helper.Data;
1717
using Hi3Helper.Plugin.Core.Management;
@@ -25,8 +25,10 @@
2525
using System.Collections.Generic;
2626
using System.Diagnostics;
2727
using System.IO;
28+
using System.IO.Hashing;
2829
using System.Linq;
2930
using System.Net.Http;
31+
using System.Security.Cryptography;
3032
using System.Threading;
3133
using System.Threading.Tasks;
3234

@@ -51,15 +53,14 @@ protected virtual async Task<bool> AlterStartPatchUpdateSophon(HttpClient httpCl
5153
nameof(GameVersionManager.GamePreset.LauncherBizName));
5254

5355
// Get GameVersionManager and GamePreset
54-
IGameVersion gameVersion = GameVersionManager;
55-
PresetConfig gamePreset = gameVersion.GamePreset;
56+
PresetConfig gamePreset = GameVersionManager.GamePreset;
5657

5758
// Gt current and future version
58-
GameVersion? requestedVersionFrom = gameVersion.GetGameExistingVersion() ??
59+
GameVersion? requestedVersionFrom = GameVersionManager.GetGameExistingVersion() ??
5960
throw new NullReferenceException("Cannot get previous/current version of the game");
6061
GameVersion? requestedVersionTo = (isPreloadMode ?
61-
gameVersion.GetGameVersionApiPreload() :
62-
gameVersion.GetGameVersionApi()) ??
62+
GameVersionManager.GetGameVersionApiPreload() :
63+
GameVersionManager.GetGameVersionApi()) ??
6364
throw new NullReferenceException("Cannot get next/future version of the game");
6465

6566
// Assign branch properties
@@ -168,13 +169,13 @@ protected virtual async Task ConfirmAdditionalPatchDataPackageFiles(SophonChunkM
168169
.Where(x => matchingFieldsList.Contains(x.MatchingField, StringComparer.OrdinalIgnoreCase))
169170
.Sum(x =>
170171
{
171-
var firstTag = x.DiffTaggedInfo.FirstOrDefault(y => y.Key == currentVersion).Value;
172+
SophonManifestChunkInfo? firstTag = x.DiffTaggedInfo.FirstOrDefault(y => y.Key == currentVersion).Value;
172173
return firstTag?.CompressedSize ?? 0;
173174
});
174175
long sizeAdditionalToDownload = otherManifestIdentity
175176
.Sum(x =>
176177
{
177-
var firstTag = x.DiffTaggedInfo.FirstOrDefault(y => y.Key == currentVersion).Value;
178+
SophonManifestChunkInfo? firstTag = x.DiffTaggedInfo.FirstOrDefault(y => y.Key == currentVersion).Value;
178179
return firstTag?.CompressedSize ?? 0;
179180
});
180181

@@ -191,7 +192,7 @@ protected virtual async Task ConfirmAdditionalPatchDataPackageFiles(SophonChunkM
191192
}
192193
}
193194

194-
matchingFieldsList.AddRange(otherManifestIdentity.Select(identity => identity.MatchingField));
195+
matchingFieldsList.AddRange(otherManifestIdentity.Select(identity => identity.MatchingField ?? ""));
195196
return;
196197

197198
string GetFileDetails()
@@ -205,12 +206,12 @@ string GetFileDetails()
205206
long chunkCount = 0;
206207

207208
// ReSharper disable once ConvertToUsingDeclaration
208-
using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
209+
using (FileStream fileStream = new(filePath, FileMode.Create, FileAccess.Write))
209210
{
210-
using StreamWriter writer = new StreamWriter(fileStream);
211-
foreach (var field in otherManifestIdentity)
211+
using StreamWriter writer = new(fileStream);
212+
foreach (SophonManifestPatchIdentity field in otherManifestIdentity)
212213
{
213-
var fieldInfo = field.DiffTaggedInfo.FirstOrDefault(x => x.Key == currentVersion).Value;
214+
SophonManifestChunkInfo? fieldInfo = field.DiffTaggedInfo.FirstOrDefault(x => x.Key == currentVersion).Value;
214215
if (fieldInfo == null)
215216
{
216217
continue;
@@ -560,29 +561,67 @@ async ValueTask ImplDownload(Tuple<SophonPatchAsset, Dictionary<string, int>> ct
560561
SophonPatchAsset patchAsset = ctx.Item1;
561562
Dictionary<string, int> downloadedDict = ctx.Item2;
562563

563-
using (dictionaryLock.EnterScope())
564+
try
564565
{
565-
_ = downloadedDict.TryAdd(patchAsset.PatchNameSource, 0);
566-
downloadedDict[patchAsset.PatchNameSource]++;
566+
UpdateCurrentDownloadStatus();
567+
// Check if target file has already been patched so the launcher won't redownload everything.
568+
if (!isPreloadMode && patchAsset.PatchMethod != SophonPatchMethod.Remove)
569+
{
570+
FileInfo fileInfo = new FileInfo(Path.Combine(GamePath, patchAsset.TargetFilePath))
571+
.EnsureNoReadOnly()
572+
.StripAlternateDataStream();
573+
574+
if (fileInfo.Exists &&
575+
fileInfo.Length == patchAsset.TargetFileSize)
576+
{
577+
byte[] remoteHashBytes = HexTool.HexToBytesUnsafe(patchAsset.TargetFileHash);
578+
byte[] localHashBytes = remoteHashBytes.Length > 8
579+
? await GetCryptoHashAsync<MD5>(fileInfo, null, false, false, innerToken)
580+
: await GetHashAsync<XxHash64>(fileInfo, false, false, innerToken);
581+
582+
// Try to reverse hash bytes in case the returned bytes are going to be Big-endian.
583+
if (!localHashBytes.SequenceEqual(remoteHashBytes))
584+
{
585+
Array.Reverse(localHashBytes);
586+
}
587+
588+
// Now compare. If the hash is already equal (means the target file is already downloaded),
589+
// then skip from downloading the patch.
590+
if (localHashBytes.SequenceEqual(remoteHashBytes))
591+
{
592+
long patchSize = patchAsset.PatchSize;
593+
UpdateSophonFileTotalProgress(patchSize);
594+
UpdateSophonFileDownloadProgress(patchSize, patchSize);
595+
return;
596+
}
597+
}
598+
}
599+
600+
await patchAsset.DownloadPatchAsync(httpClient,
601+
GamePath,
602+
patchOutputDir,
603+
true,
604+
read =>
605+
{
606+
UpdateSophonFileTotalProgress(read);
607+
UpdateSophonFileDownloadProgress(read, read);
608+
},
609+
downloadLimiter,
610+
innerToken);
567611
}
612+
finally
613+
{
614+
using (dictionaryLock.EnterScope())
615+
{
616+
_ = downloadedDict.TryAdd(patchAsset.PatchNameSource, 0);
617+
downloadedDict[patchAsset.PatchNameSource]++;
618+
}
568619

569-
UpdateCurrentDownloadStatus();
570-
await patchAsset.DownloadPatchAsync(httpClient,
571-
GamePath,
572-
patchOutputDir,
573-
true,
574-
read =>
575-
{
576-
UpdateSophonFileTotalProgress(read);
577-
UpdateSophonFileDownloadProgress(read, read);
578-
},
579-
downloadLimiter,
580-
innerToken);
581-
582-
Logger.LogWriteLine($"Downloaded patch file for: {patchAsset.TargetFilePath}",
583-
LogType.Debug,
584-
true);
585-
Interlocked.Increment(ref ProgressAllCountCurrent);
620+
Logger.LogWriteLine($"Downloaded patch file for: {patchAsset.TargetFilePath}",
621+
LogType.Debug,
622+
true);
623+
Interlocked.Increment(ref ProgressAllCountCurrent);
624+
}
586625
}
587626

588627
async ValueTask ImplPatchUpdate(Tuple<SophonPatchAsset, Dictionary<string, int>> ctx, CancellationToken innerToken)

CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1926,6 +1926,7 @@ protected virtual string GetLanguageStringByID(int id)
19261926
) => langString switch
19271927
{
19281928
"Chinese" => "zh-cn",
1929+
"Chinese(PRC)" => "zh-cn",
19291930
"English" => "en-us",
19301931
"English(US)" => "en-us",
19311932
"Korean" => "ko-kr",

CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs

Lines changed: 12 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Hi3Helper.Sophon;
22
using Hi3Helper.Sophon.Infos;
3+
using Hi3Helper.Sophon.Structs;
34
using System;
45
using System.Buffers;
56
using System.Collections.Generic;
@@ -29,47 +30,16 @@ public override async Task FilterAssetList<T>(
2930
Func<T, string?> itemPathSelector,
3031
CancellationToken token)
3132
{
32-
HashSet<int> exceptMatchFieldHashSet = await GetExceptMatchFieldHashSet(token);
33+
HashSet<string> exceptMatchFieldHashSet = await GetExceptMatchFieldHashSet(token);
3334
if (exceptMatchFieldHashSet.Count == 0)
3435
{
3536
return;
3637
}
3738

38-
FilterSophonAsset(itemList, Selector, exceptMatchFieldHashSet);
39-
return;
40-
41-
string? Selector(T item)
42-
{
43-
// For Game Update
44-
if (item is SophonPatchAsset asset)
45-
{
46-
if (asset.MainAssetInfo is not { } assetSelected)
47-
{
48-
return null;
49-
}
50-
51-
token.ThrowIfCancellationRequested();
52-
ref SophonChunksInfo chunkInfo = ref Unsafe.IsNullRef(ref assetSelected)
53-
? ref Unsafe.NullRef<SophonChunksInfo>()
54-
: ref GetChunkAssetChunksInfo(assetSelected);
55-
56-
if (Unsafe.IsNullRef(ref chunkInfo))
57-
{
58-
chunkInfo = ref GetChunkAssetChunksInfoAlt(assetSelected);
59-
}
60-
61-
return Unsafe.IsNullRef(ref chunkInfo)
62-
? asset.MainAssetInfo.AssetName
63-
: chunkInfo.ChunksBaseUrl;
64-
}
65-
66-
// TODO: For Game Repair handle
67-
68-
return null;
69-
}
39+
FilterSophonAsset(itemList, exceptMatchFieldHashSet);
7040
}
7141

72-
private async Task<HashSet<int>> GetExceptMatchFieldHashSet(CancellationToken token)
42+
private async Task<HashSet<string>> GetExceptMatchFieldHashSet(CancellationToken token)
7343
{
7444
string gameExecDataName =
7545
Path.GetFileNameWithoutExtension(GameVersionManager.GamePreset.GameExecutableName) ?? "ZenlessZoneZero";
@@ -82,38 +52,20 @@ private async Task<HashSet<int>> GetExceptMatchFieldHashSet(CancellationToken to
8252
return [];
8353
}
8454

85-
string exceptMatchFieldContent = await File.ReadAllTextAsync(gameExceptMatchFieldFile, token);
86-
HashSet<int> exceptMatchFieldHashSet = CreateExceptMatchFieldHashSet<int>(exceptMatchFieldContent);
55+
string exceptMatchFieldContent = await File.ReadAllTextAsync(gameExceptMatchFieldFile, token);
56+
HashSet<string> exceptMatchFieldHashSet = CreateExceptMatchFieldHashSet(exceptMatchFieldContent);
8757

8858
return exceptMatchFieldHashSet;
8959
}
9060

9161
// ReSharper disable once IdentifierTypo
92-
private static void FilterSophonAsset<T>(List<T> itemList, Func<T, string?> assetSelector, HashSet<int> exceptMatchFieldHashSet)
62+
private static void FilterSophonAsset<T>(List<T> itemList, HashSet<string> exceptMatchFieldHashSet)
9363
{
94-
const string separators = "/\\";
95-
scoped Span<Range> urlPathRanges = stackalloc Range[32];
96-
9764
List<T> filteredList = [];
9865
foreach (T asset in itemList)
9966
{
100-
string? assetPath = assetSelector(asset);
101-
if (assetPath == null)
102-
{
103-
filteredList.Add(asset);
104-
continue;
105-
}
106-
107-
ReadOnlySpan<char> manifestUrl = assetPath;
108-
int rangeLen = manifestUrl.SplitAny(urlPathRanges, separators, SplitOptions);
109-
if (rangeLen <= 0)
110-
{
111-
continue;
112-
}
113-
114-
ReadOnlySpan<char> manifestStr = manifestUrl[urlPathRanges[rangeLen - 1]];
115-
if (int.TryParse(manifestStr, null, out int lookupNumber) &&
116-
exceptMatchFieldHashSet.Contains(lookupNumber))
67+
if (asset is SophonIdentifiableProperty { MatchingField: { } assetMatchingField } &&
68+
exceptMatchFieldHashSet.Contains(assetMatchingField))
11769
{
11870
continue;
11971
}
@@ -130,11 +82,10 @@ private static void FilterSophonAsset<T>(List<T> itemList, Func<T, string?> asse
13082
itemList.AddRange(filteredList);
13183
}
13284

133-
internal static HashSet<T> CreateExceptMatchFieldHashSet<T>(string exceptMatchFieldContent)
134-
where T : ISpanParsable<T>
85+
internal static HashSet<string> CreateExceptMatchFieldHashSet(string exceptMatchFieldContent)
13586
{
13687
const string lineFeedSeparators = "\r\n";
137-
HashSet<T> hashSetReturn = [];
88+
HashSet<string> hashSetReturn = new(StringComparer.OrdinalIgnoreCase);
13889
scoped Span<Range> contentLineRange = stackalloc Range[2];
13990

14091
ReadOnlySpan<char> contentSpan = exceptMatchFieldContent.AsSpan();
@@ -157,10 +108,7 @@ internal static HashSet<T> CreateExceptMatchFieldHashSet<T>(string exceptMatchFi
157108
}
158109

159110
ReadOnlySpan<char> contentMatch = contentSpan[contentMatchRange].Trim(separatorsChars);
160-
if (T.TryParse(contentMatch, null, out T? result))
161-
{
162-
hashSetReturn.Add(result);
163-
}
111+
hashSetReturn.Add(contentMatch.ToString());
164112
}
165113

166114
return hashSetReturn;

CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ private void FilterExcludedAssets(List<FilePropertiesRemote> assetList)
107107
}
108108

109109
string exceptMatchFieldContent = File.ReadAllText(gameExceptMatchFieldFile);
110-
HashSet<int> exceptMatchFieldHashSet = ZenlessInstall.CreateExceptMatchFieldHashSet<int>(exceptMatchFieldContent);
110+
HashSet<string> exceptMatchFieldHashSet = ZenlessInstall.CreateExceptMatchFieldHashSet(exceptMatchFieldContent);
111111

112112
List<FilePropertiesRemote> filteredList = [];
113113
foreach (FilePropertiesRemote asset in assetList)
@@ -120,7 +120,7 @@ private void FilterExcludedAssets(List<FilePropertiesRemote> assetList)
120120
continue;
121121
}
122122

123-
bool isExceptionFound = zenlessResAsset.PackageMatchingIds.Any(exceptMatchFieldHashSet.Contains);
123+
bool isExceptionFound = zenlessResAsset.PackageMatchingIds.Any(x => exceptMatchFieldHashSet.Contains($"{x}"));
124124
if (isExceptionFound)
125125
{
126126
continue;

0 commit comments

Comments
 (0)