Skip to content

Commit fa0450c

Browse files
authored
[Hotfix] Stable 1.82.32 (#793)
# What's Changed? - **[Imp]** Update .NET components NuGet to 9.0.9, by @bagusnl @neon-nyan - **[Fix]** ``NotSupportedException`` error while performing Game Repair on Honkai Impact 3rd and Honkai: Star Rail, by @neon-nyan - **[Fix]** Legacy Sophon won't recognize already existed files while installing games from scratch, by @neon-nyan - **[Fix/Imp]** Ignore assets that marked as "unused/deleted" on ZZZ Game Installation/Update and Game Repair, by @neon-nyan
2 parents ee51346 + 09c6986 commit fa0450c

35 files changed

+697
-461
lines changed

CollapseLauncher/Classes/Helper/FileUtility.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,19 @@ public static bool RenameFileWithPrefix(string filePath, string prefix = "-old",
111111
}
112112
return false;
113113
}
114+
115+
public static int GetFileStreamBufferSize(this long fileSize)
116+
=> fileSize switch
117+
{
118+
// 128 KiB
119+
<= 128 << 10 => 4 << 10,
120+
// 1 MiB
121+
<= 1 << 20 => 64 << 10,
122+
// 32 MiB
123+
<= 32 << 20 => 128 << 10,
124+
// 100 MiB
125+
<= 100 << 20 => 512 << 10,
126+
_ => 1 << 20
127+
};
114128
}
115129
}

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

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -421,12 +421,13 @@ await Parallel.ForEachAsync(sophonAssetList, parallelOptions, DelegateAssetRenam
421421
// Remove sophon verified files
422422
CleanupTempSophonVerifiedFiles();
423423

424+
// ReSharper disable AccessToDisposedClosure
424425
// Declare the download delegate
425-
ValueTask DelegateAssetDownload(SophonAsset asset, CancellationToken _)
426-
// ReSharper disable once AccessToDisposedClosure
427-
{
428-
return RunSophonAssetDownloadThread(httpClient, asset, parallelChunksOptions);
429-
}
426+
ValueTask DelegateAssetDownload(SophonAsset asset, CancellationToken _) =>
427+
gameState == GameInstallStateEnum.NeedsUpdate
428+
? RunSophonAssetUpdateThread(httpClient, asset, parallelChunksOptions)
429+
: RunSophonAssetDownloadThread(httpClient, asset, parallelChunksOptions);
430+
// ReSharper enable AccessToDisposedClosure
430431

431432
// Declare the rename temp file delegate
432433
async ValueTask DelegateAssetRenameTempFile(SophonAsset asset, CancellationToken token)
@@ -791,6 +792,9 @@ await TryGetAdditionalPackageForSophonDiff(httpClient,
791792
downloadSpeedLimiter);
792793
}
793794

795+
// Filter asset list
796+
await FilterSophonPatchAssetList(sophonUpdateAssetList, Token.Token);
797+
794798
// Get the remote chunk size
795799
ProgressPerFileSizeTotal = sophonUpdateAssetList.GetCalculatedDiffSize(!isPreloadMode);
796800
ProgressPerFileSizeCurrent = 0;
@@ -926,7 +930,14 @@ await Parallel.ForEachAsync(sophonUpdateAssetList.Where(x => !x.IsDirectory),
926930
}
927931
}
928932

929-
private ValueTask RunSophonAssetDownloadThread(HttpClient client, SophonAsset asset,
933+
protected virtual Task FilterSophonPatchAssetList(List<SophonAsset> itemList, CancellationToken token)
934+
{
935+
// NOP
936+
return Task.CompletedTask;
937+
}
938+
939+
private ValueTask RunSophonAssetDownloadThread(HttpClient client,
940+
SophonAsset asset,
930941
ParallelOptions parallelOptions)
931942
{
932943
// If the asset is a dictionary, then return
@@ -936,11 +947,49 @@ private ValueTask RunSophonAssetDownloadThread(HttpClient client, SophonAss
936947
}
937948

938949
// Get the file path and start the write process
939-
var assetName = asset.AssetName;
940-
var filePath = EnsureCreationOfDirectory(Path.Combine(GamePath, assetName));
950+
string? assetName = asset.AssetName;
951+
string filePath = EnsureCreationOfDirectory(Path.Combine(GamePath, assetName));
952+
953+
if (File.Exists(filePath + "_tempSophon")) // Fallback to legacy behaviour
954+
{
955+
return RunSophonAssetUpdateThread(client, asset, parallelOptions);
956+
}
957+
958+
// Get the target and temp file info
959+
FileInfo existingFileInfo = new FileInfo(filePath).EnsureNoReadOnly();
960+
961+
return asset.WriteToStreamAsync(client,
962+
assetSize => existingFileInfo.Open(new FileStreamOptions
963+
{
964+
Mode = FileMode.OpenOrCreate,
965+
Access = FileAccess.ReadWrite,
966+
Share = FileShare.ReadWrite,
967+
BufferSize = assetSize.GetFileStreamBufferSize()
968+
}),
969+
parallelOptions,
970+
UpdateSophonFileTotalProgress,
971+
UpdateSophonFileDownloadProgress,
972+
UpdateSophonDownloadStatus
973+
);
974+
}
975+
976+
private ValueTask RunSophonAssetUpdateThread(HttpClient client,
977+
SophonAsset asset,
978+
ParallelOptions parallelOptions)
979+
{
980+
// If the asset is a dictionary, then return
981+
if (asset.IsDirectory)
982+
{
983+
return ValueTask.CompletedTask;
984+
}
985+
986+
// Get the file path and start the write process
987+
string? assetName = asset.AssetName;
988+
string filePath = EnsureCreationOfDirectory(Path.Combine(GamePath, assetName));
941989

942990
// Get the target and temp file info
943-
FileInfo existingFileInfo = new FileInfo(filePath).EnsureNoReadOnly(out bool isExistingFileInfoExist);
991+
FileInfo existingFileInfo =
992+
new FileInfo(filePath).EnsureNoReadOnly(out bool isExistingFileInfoExist);
944993
FileInfo sophonFileInfo =
945994
new FileInfo(filePath + "_tempSophon").EnsureNoReadOnly(out bool isSophonFileInfoExist);
946995

@@ -958,10 +1007,12 @@ private ValueTask RunSophonAssetDownloadThread(HttpClient client, SophonAss
9581007
sophonFileInfo.Delete();
9591008
}
9601009

961-
return asset.WriteToStreamAsync(
962-
client,
963-
() => new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite,
964-
FileShare.ReadWrite),
1010+
return asset.WriteToStreamAsync(client,
1011+
assetSize => new FileStream(filePath,
1012+
FileMode.OpenOrCreate,
1013+
FileAccess.ReadWrite,
1014+
FileShare.ReadWrite,
1015+
assetSize.GetFileStreamBufferSize()),
9651016
parallelOptions,
9661017
UpdateSophonFileTotalProgress,
9671018
UpdateSophonFileDownloadProgress,

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ await GetAlterSophonPatchAssets(httpClient,
117117
downloadSpeedLimiter,
118118
Token.Token);
119119

120+
// Filter asset list
121+
await FilterSophonPatchAssetList(patchAssets.AssetList, Token.Token);
122+
120123
// Start the patch pipeline
121124
await StartAlterSophonPatch(httpClient,
122125
isPreloadMode,
@@ -130,6 +133,12 @@ await StartAlterSophonPatch(httpClient,
130133
return true;
131134
}
132135

136+
protected virtual Task FilterSophonPatchAssetList(List<SophonPatchAsset> itemList, CancellationToken token)
137+
{
138+
// NOP
139+
return Task.CompletedTask;
140+
}
141+
133142
protected virtual async Task ConfirmAdditionalPatchDataPackageFiles(SophonChunkManifestInfoPair patchManifest,
134143
List<string> matchingFieldsList,
135144
CancellationToken token)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using Hi3Helper.Sophon;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
// ReSharper disable CheckNamespace
6+
7+
#nullable enable
8+
namespace CollapseLauncher.InstallManager.Zenless
9+
{
10+
internal partial class ZenlessInstall
11+
{
12+
protected override async Task FilterSophonPatchAssetList(List<SophonAsset> itemList, CancellationToken token)
13+
{
14+
HashSet<int> exceptMatchFieldHashSet = await GetExceptMatchFieldHashSet(token);
15+
if (exceptMatchFieldHashSet.Count == 0)
16+
{
17+
return;
18+
}
19+
20+
FilterSophonAsset(itemList, x => x, exceptMatchFieldHashSet, token);
21+
}
22+
}
23+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
using Hi3Helper.Sophon;
2+
using Hi3Helper.Sophon.Infos;
3+
using System;
4+
using System.Buffers;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Runtime.CompilerServices;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
// ReSharper disable CheckNamespace
11+
12+
#nullable enable
13+
namespace CollapseLauncher.InstallManager.Zenless
14+
{
15+
internal partial class ZenlessInstall
16+
{
17+
internal const StringSplitOptions SplitOptions = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries;
18+
19+
// ReSharper disable once StringLiteralTypo
20+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<SophonChunksInfo>k__BackingField")]
21+
private static extern ref SophonChunksInfo GetChunkAssetChunksInfo(SophonAsset element);
22+
23+
// ReSharper disable once StringLiteralTypo
24+
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<SophonChunksInfoAlt>k__BackingField")]
25+
private static extern ref SophonChunksInfo GetChunkAssetChunksInfoAlt(SophonAsset element);
26+
27+
protected override async Task FilterSophonPatchAssetList(List<SophonPatchAsset> itemList, CancellationToken token)
28+
{
29+
HashSet<int> exceptMatchFieldHashSet = await GetExceptMatchFieldHashSet(token);
30+
if (exceptMatchFieldHashSet.Count == 0)
31+
{
32+
return;
33+
}
34+
35+
FilterSophonAsset(itemList, x => x.MainAssetInfo, exceptMatchFieldHashSet, token);
36+
}
37+
38+
private async Task<HashSet<int>> GetExceptMatchFieldHashSet(CancellationToken token)
39+
{
40+
string gameExecDataName =
41+
Path.GetFileNameWithoutExtension(GameVersionManager.GamePreset.GameExecutableName) ?? "ZenlessZoneZero";
42+
string gameExecDataPath = $"{gameExecDataName}_Data";
43+
string gamePersistentDataPath = Path.Combine(GamePath, gameExecDataPath, "Persistent");
44+
string gameExceptMatchFieldFile = Path.Combine(gamePersistentDataPath, "KDelResource");
45+
46+
if (!File.Exists(gameExceptMatchFieldFile))
47+
{
48+
return [];
49+
}
50+
51+
string exceptMatchFieldContent = await File.ReadAllTextAsync(gameExceptMatchFieldFile, token);
52+
HashSet<int> exceptMatchFieldHashSet = CreateExceptMatchFieldHashSet<int>(exceptMatchFieldContent);
53+
54+
return exceptMatchFieldHashSet;
55+
}
56+
57+
// ReSharper disable once IdentifierTypo
58+
private static void FilterSophonAsset<T>(List<T> itemList, Func<T, SophonAsset?> assetSelector, HashSet<int> exceptMatchFieldHashSet, CancellationToken token)
59+
{
60+
const string separators = "/\\";
61+
scoped Span<Range> urlPathRanges = stackalloc Range[32];
62+
63+
List<T> filteredList = [];
64+
foreach (T asset in itemList)
65+
{
66+
SophonAsset? assetSelected = assetSelector(asset);
67+
68+
token.ThrowIfCancellationRequested();
69+
ref SophonChunksInfo chunkInfo = ref assetSelected == null
70+
? ref Unsafe.NullRef<SophonChunksInfo>()
71+
: ref GetChunkAssetChunksInfo(assetSelected);
72+
73+
if (assetSelected != null && Unsafe.IsNullRef(ref chunkInfo))
74+
{
75+
chunkInfo = ref GetChunkAssetChunksInfoAlt(assetSelected);
76+
}
77+
78+
if (Unsafe.IsNullRef(ref chunkInfo))
79+
{
80+
filteredList.Add(asset);
81+
continue;
82+
}
83+
84+
ReadOnlySpan<char> manifestUrl = chunkInfo.ChunksBaseUrl;
85+
int rangeLen = manifestUrl.SplitAny(urlPathRanges, separators, SplitOptions);
86+
87+
if (rangeLen <= 0)
88+
{
89+
continue;
90+
}
91+
92+
ReadOnlySpan<char> manifestStr = manifestUrl[urlPathRanges[rangeLen - 1]];
93+
if (int.TryParse(manifestStr, null, out int lookupNumber) &&
94+
exceptMatchFieldHashSet.Contains(lookupNumber))
95+
{
96+
continue;
97+
}
98+
99+
filteredList.Add(asset);
100+
}
101+
102+
if (filteredList.Count == 0)
103+
{
104+
return;
105+
}
106+
107+
itemList.Clear();
108+
itemList.AddRange(filteredList);
109+
}
110+
111+
internal static HashSet<T> CreateExceptMatchFieldHashSet<T>(string exceptMatchFieldContent)
112+
where T : ISpanParsable<T>
113+
{
114+
const string lineFeedSeparators = "\r\n";
115+
HashSet<T> hashSetReturn = [];
116+
scoped Span<Range> contentLineRange = stackalloc Range[2];
117+
118+
ReadOnlySpan<char> contentSpan = exceptMatchFieldContent.AsSpan();
119+
int contentLineLen = contentSpan.SplitAny(contentLineRange, lineFeedSeparators, SplitOptions);
120+
121+
if (contentLineLen == 0)
122+
{
123+
return hashSetReturn;
124+
}
125+
126+
contentSpan = contentSpan[contentLineRange[0]];
127+
const string separatorsChars = "|;,$#@+ ";
128+
SearchValues<char> separators = SearchValues.Create(separatorsChars);
129+
130+
foreach (Range contentMatchRange in contentSpan.SplitAny(separators))
131+
{
132+
if (contentMatchRange.End.Value - contentMatchRange.Start.Value <= 0)
133+
{
134+
continue;
135+
}
136+
137+
ReadOnlySpan<char> contentMatch = contentSpan[contentMatchRange].Trim(separatorsChars);
138+
if (T.TryParse(contentMatch, null, out T? result))
139+
{
140+
hashSetReturn.Add(result);
141+
}
142+
}
143+
144+
return hashSetReturn;
145+
}
146+
}
147+
}

CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
#nullable enable
2222
namespace CollapseLauncher.InstallManager.Zenless
2323
{
24-
internal sealed class ZenlessInstall : InstallManagerBase
24+
internal sealed partial class ZenlessInstall : InstallManagerBase
2525
{
2626
#region Private Properties
2727
private ZenlessSettings? ZenlessSettings { get; }

CollapseLauncher/Classes/RepairManagement/Honkai/Fetch.cs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -884,14 +884,11 @@ private async ValueTask<bool> IsAudioFileAvailable(ManifestAssetInfo audioInfo,
884884
private async Task<KianaAudioManifest> TryGetAudioManifest(HttpClient client, SenadinaFileIdentifier senadinaFileIdentifier, string manifestLocal, string manifestRemote, CancellationToken token)
885885
{
886886
string originalUrl = senadinaFileIdentifier.GetOriginalFileUrl();
887-
await using Stream? originalFile = await client.GetStreamAsync(originalUrl, token);
887+
await using Stream originalFile = await BridgedNetworkStream.CreateStream(client, originalUrl, null, token);
888888
await using FileStream localFile = new FileStream(EnsureCreationOfDirectory(manifestLocal), FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
889889

890890
// Start downloading manifest.m
891-
if (originalFile != null)
892-
{
893-
await DoCopyStreamProgress(originalFile, localFile, token: token);
894-
}
891+
await DoCopyStreamProgress(originalFile, localFile, token: token);
895892

896893
Stream? networkStream = senadinaFileIdentifier.fileStream;
897894

@@ -988,7 +985,7 @@ private async Task FetchXMFFile(HttpClient _httpClient,
988985
using MemoryStream tempXMFMetaStream = new();
989986

990987
await using Stream? metaBaseXMFStream = !IsOnlyRecoverMain && isPlatformXMFStreamExist ?
991-
await _httpClient.GetStreamAsync(xmfPlatformIdentifier!.GetOriginalFileUrl(), token) :
988+
await BridgedNetworkStream.CreateStream(_httpClient, xmfPlatformIdentifier!.GetOriginalFileUrl(), null, token) :
992989
null;
993990
if (xmfPlatformIdentifier != null)
994991
{
@@ -998,9 +995,10 @@ await _httpClient.GetStreamAsync(xmfPlatformIdentifier!.GetOriginalFileUrl(), to
998995

999996
if (isEitherXMFExist)
1000997
{
1001-
await using Stream? baseXMFStream = !IsOnlyRecoverMain && isSecondaryXMFStreamExist ?
1002-
await _httpClient.GetStreamAsync(xmfCurrentIdentifier!.GetOriginalFileUrl(), token) :
1003-
await _httpClient.GetStreamAsync(xmfBaseIdentifier!.GetOriginalFileUrl(), token);
998+
string baseXMFUrlStream = !IsOnlyRecoverMain && isSecondaryXMFStreamExist
999+
? xmfCurrentIdentifier!.GetOriginalFileUrl()
1000+
: xmfBaseIdentifier!.GetOriginalFileUrl();
1001+
await using Stream baseXMFStream = await BridgedNetworkStream.CreateStream(_httpClient, baseXMFUrlStream, null, token);
10041002
if (xmfCurrentIdentifier != null)
10051003
{
10061004
await using Stream? dataXMFStream = !IsOnlyRecoverMain ? xmfCurrentIdentifier.fileStream : xmfBaseIdentifier?.fileStream;
@@ -1009,10 +1007,7 @@ await _httpClient.GetStreamAsync(xmfCurrentIdentifier!.GetOriginalFileUrl(), tok
10091007
await using (FileStream fs1 = new FileStream(EnsureCreationOfDirectory(!IsOnlyRecoverMain ? xmfSecPath : xmfPriPath), FileMode.Create, FileAccess.ReadWrite))
10101008
{
10111009
// Download the secondary XMF into MemoryStream
1012-
if (baseXMFStream != null)
1013-
{
1014-
await DoCopyStreamProgress(baseXMFStream, fs1, token: token);
1015-
}
1010+
await DoCopyStreamProgress(baseXMFStream, fs1, token: token);
10161011

10171012
// Copy the secondary XMF into primary XMF if _isOnlyRecoverMain == false
10181013
if (!IsOnlyRecoverMain)

0 commit comments

Comments
 (0)