From 88d9a3a5bdcab64b18976b8ee16b66552cb8d17a Mon Sep 17 00:00:00 2001
From: Ron Friedman <9833218+Cryotechnic@users.noreply.github.com>
Date: Fri, 19 Dec 2025 16:47:35 -0500
Subject: [PATCH 01/47] [skip ci] Update README
---
README.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 74c422aff..1651c3be6 100644
--- a/README.md
+++ b/README.md
@@ -64,11 +64,11 @@ Not only that, this launcher also has some advanced features for **Genshin Impac
> ### You can find the list of features on our [new website](https://collapselauncher.com/features.html)!
# Download Ready-To-Use Builds
-[
](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.82.36/CollapseLauncher-stable-Setup.exe)
-> **Note**: The version for this build is `1.82.36` (Released on: October 26th, 2025).
+[
](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.83.13/CollapseLauncher-stable-Setup.exe)
+> **Note**: The version for this build is `1.82.36` (Released on: December 19th, 2025).
-[
](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.83.12-pre/CollapseLauncher-preview-Setup.exe)
-> **Note**: The version for this build is `1.83.12` (Released on: October 26th, 2025).
+[
](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.83.13-pre/CollapseLauncher-preview-Setup.exe)
+> **Note**: The version for this build is `1.83.12` (Released on: December 19th, 2025).
To view all releases, [**click here**](https://github.com/neon-nyan/CollapseLauncher/releases).
From ed60afc4fb08880145532731f04af87f9c23f948 Mon Sep 17 00:00:00 2001
From: Gabriel Lima <44784408+gablm@users.noreply.github.com>
Date: Sat, 20 Dec 2025 17:00:23 +0000
Subject: [PATCH 02/47] Fix misc settings not loading
---
.../GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs
index 943dc16b6..39343c98f 100644
--- a/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs
+++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Universal/RegistryClass/CollapseMiscSetting.cs
@@ -154,6 +154,8 @@ public static CollapseMiscSetting Load(IGameSettings gameSettings)
#endif
CollapseMiscSetting result = byteStr.Deserialize(UniversalSettingsJsonContext.Default.CollapseMiscSetting) ?? new CollapseMiscSetting();
result.ParentGameSettings = gameSettings;
+
+ return result;
}
}
catch ( Exception ex )
From 6f31f55e069293add7dd1bb4a42e19c5d334b201 Mon Sep 17 00:00:00 2001
From: Gabriel Lima <44784408+gablm@users.noreply.github.com>
Date: Sat, 20 Dec 2025 17:02:52 +0000
Subject: [PATCH 03/47] [skip ci] Update README
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 1651c3be6..741f66e33 100644
--- a/README.md
+++ b/README.md
@@ -65,10 +65,10 @@ Not only that, this launcher also has some advanced features for **Genshin Impac
# Download Ready-To-Use Builds
[
](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.83.13/CollapseLauncher-stable-Setup.exe)
-> **Note**: The version for this build is `1.82.36` (Released on: December 19th, 2025).
+> **Note**: The version for this build is `1.83.13` (Released on: December 19th, 2025).
[
](https://github.com/CollapseLauncher/Collapse/releases/download/CL-v1.83.13-pre/CollapseLauncher-preview-Setup.exe)
-> **Note**: The version for this build is `1.83.12` (Released on: December 19th, 2025).
+> **Note**: The version for this build is `1.83.13` (Released on: December 19th, 2025).
To view all releases, [**click here**](https://github.com/neon-nyan/CollapseLauncher/releases).
From bc92ac966e3f93d12959e66db41912684f3befc6 Mon Sep 17 00:00:00 2001
From: Kemal Setya Adhi
Date: Sun, 21 Dec 2025 18:21:54 +0700
Subject: [PATCH 04/47] Update Hi3Helper.SharpDiscordRPC
---
Hi3Helper.SharpDiscordRPC | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Hi3Helper.SharpDiscordRPC b/Hi3Helper.SharpDiscordRPC
index a6ba8bac8..73fc6cbfa 160000
--- a/Hi3Helper.SharpDiscordRPC
+++ b/Hi3Helper.SharpDiscordRPC
@@ -1 +1 @@
-Subproject commit a6ba8bac8d7795fb3059100a2a8b4905a3738636
+Subproject commit 73fc6cbfa862ca00f2f48356cfa0dbc03efe76ae
From 5c945280244e66d3dfba4cd91c70f2535979058e Mon Sep 17 00:00:00 2001
From: Kemal Setya Adhi
Date: Thu, 25 Dec 2025 01:26:40 +0700
Subject: [PATCH 05/47] [PR Fix] Fix wait event
---
Hi3Helper.SharpDiscordRPC | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Hi3Helper.SharpDiscordRPC b/Hi3Helper.SharpDiscordRPC
index 73fc6cbfa..035995e78 160000
--- a/Hi3Helper.SharpDiscordRPC
+++ b/Hi3Helper.SharpDiscordRPC
@@ -1 +1 @@
-Subproject commit 73fc6cbfa862ca00f2f48356cfa0dbc03efe76ae
+Subproject commit 035995e786f7f7a90d1dd002a74d9a434b030465
From 1e087ff5b6c42eb9fc73f57d756972c0916da928 Mon Sep 17 00:00:00 2001
From: Kemal Setya Adhi
Date: Thu, 25 Dec 2025 01:33:09 +0700
Subject: [PATCH 06/47] Update Hi3Helper.SharpDiscordRPC
---
Hi3Helper.SharpDiscordRPC | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Hi3Helper.SharpDiscordRPC b/Hi3Helper.SharpDiscordRPC
index 035995e78..fb14695a0 160000
--- a/Hi3Helper.SharpDiscordRPC
+++ b/Hi3Helper.SharpDiscordRPC
@@ -1 +1 @@
-Subproject commit 035995e786f7f7a90d1dd002a74d9a434b030465
+Subproject commit fb14695a08aa686b0b6d09831923c30097d9daef
From 1452e4efb2d3636f01a836d659273db5a523e6c6 Mon Sep 17 00:00:00 2001
From: "transifex-integration[bot]"
<43880903+transifex-integration[bot]@users.noreply.github.com>
Date: Fri, 26 Dec 2025 07:48:05 +0000
Subject: [PATCH 07/47] [skip ci] Sync translation Translate en_US.json in
zh_CN
100% reviewed source file: 'en_US.json'
on 'zh_CN'.
---
Hi3Helper.Core/Lang/zh_CN.json | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Hi3Helper.Core/Lang/zh_CN.json b/Hi3Helper.Core/Lang/zh_CN.json
index 543083447..136d33df3 100644
--- a/Hi3Helper.Core/Lang/zh_CN.json
+++ b/Hi3Helper.Core/Lang/zh_CN.json
@@ -612,9 +612,9 @@
"NetworkSettings_Proxy_PasswordHelp2": "[空]",
"NetworkSettings_Proxy_PasswordHelp3": "如果您的代理不需要身份验证,请将此字段留空。",
- "NetworkSettings_ProxyTest_Button": "测试代理可联通性",
- "NetworkSettings_ProxyTest_ButtonChecking": "检查可联通性中……",
- "NetworkSettings_ProxyTest_ButtonSuccess": "代理可联通性测试成功!",
+ "NetworkSettings_ProxyTest_Button": "测试代理可连通性",
+ "NetworkSettings_ProxyTest_ButtonChecking": "检查可连通性中……",
+ "NetworkSettings_ProxyTest_ButtonSuccess": "代理可连通性测试成功!",
"NetworkSettings_ProxyTest_ButtonFailed": "测试失败!代理无法访问",
"NetworkSettings_Dns_Title": "自定义 DNS 设置",
From 78728d044c5cb92644b276144947c5a2d3087219 Mon Sep 17 00:00:00 2001
From: Gabriel Lima <44784408+gablm@users.noreply.github.com>
Date: Fri, 26 Dec 2025 12:50:04 +0000
Subject: [PATCH 08/47] Fix plugin news not updating without restarting the
launcher
---
.../HoYoPlay/HypLauncherContentApi.cs | 10 ++++++++++
.../Classes/Plugins/PluginLauncherApiWrapper.News.cs | 1 +
2 files changed, 11 insertions(+)
diff --git a/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypLauncherContentApi.cs b/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypLauncherContentApi.cs
index 0e3d06438..159893945 100644
--- a/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypLauncherContentApi.cs
+++ b/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypLauncherContentApi.cs
@@ -43,6 +43,7 @@ internal List NewsEventKind
field ??= News
.Where(x => x.ContentType == LauncherGameNewsPostType.POST_TYPE_ACTIVITY)
.ToList();
+ private set;
}
[JsonIgnore]
@@ -53,6 +54,7 @@ internal List NewsAnnouncementKind
field ??= News
.Where(x => x.ContentType == LauncherGameNewsPostType.POST_TYPE_ANNOUNCE)
.ToList();
+ private set;
}
[JsonIgnore]
@@ -63,6 +65,14 @@ internal List NewsInformationKind
field ??= News
.Where(x => x.ContentType == LauncherGameNewsPostType.POST_TYPE_INFO)
.ToList();
+ private set;
+ }
+
+ public void ResetCachedNews()
+ {
+ NewsEventKind = null;
+ NewsAnnouncementKind = null;
+ NewsInformationKind = null;
}
}
diff --git a/CollapseLauncher/Classes/Plugins/PluginLauncherApiWrapper.News.cs b/CollapseLauncher/Classes/Plugins/PluginLauncherApiWrapper.News.cs
index e17b28e93..6ac378f60 100644
--- a/CollapseLauncher/Classes/Plugins/PluginLauncherApiWrapper.News.cs
+++ b/CollapseLauncher/Classes/Plugins/PluginLauncherApiWrapper.News.cs
@@ -26,6 +26,7 @@ private async Task ConvertNewsAndCarouselEntries(HypLauncherContentApi contentAp
var carouselList = contentApi.Data.Content.Carousel;
newsList.Clear();
+ contentApi.Data.Content.ResetCachedNews();
carouselList.Clear();
using PluginDisposableMemory newsEntry = PluginDisposableMemoryExtension.ToManagedSpan(_pluginNewsApi.GetNewsEntries);
From ce17af2cfa6a6748c63976020c0296ba1ea30583 Mon Sep 17 00:00:00 2001
From: Kemal Setya Adhi
Date: Fri, 26 Dec 2025 22:17:47 +0700
Subject: [PATCH 09/47] Fix HSR Install/Update still downloads blacklisted
files
---
.../StarRail/StarRailInstall.SophonPatch.cs | 36 +++++++++++++++++--
Hi3Helper.Sophon | 2 +-
2 files changed, 34 insertions(+), 4 deletions(-)
diff --git a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs
index 785573400..881509da4 100644
--- a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs
+++ b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs
@@ -6,6 +6,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+// ReSharper disable InvertIf
#pragma warning disable IDE0130
namespace CollapseLauncher.InstallManager.StarRail
@@ -35,7 +36,8 @@ protected override async Task FilterSophonPatchAssetList(List
continue;
}
- blackListAlt.Add(span);
+ // Normalize path
+ AddBothPersistentOrStreamingAssets(blackListAlt, span);
}
if (blackList.Count == 0)
@@ -55,8 +57,8 @@ protected override async Task FilterSophonPatchAssetList(List
continue;
}
- string assetPath = Path.Combine(GamePath, patchAsset.TargetFilePath);
- if (assetPath.AsSpan().ContainsAny(searchValues))
+ int indexOfAny = patchAsset.TargetFilePath.IndexOfAny(searchValues);
+ if (indexOfAny >= 0)
{
continue;
}
@@ -91,5 +93,33 @@ static ReadOnlySpan GetFilePathFromJson(ReadOnlySpan line)
return line[..endIndexOf];
}
}
+
+ private static void AddBothPersistentOrStreamingAssets(
+ HashSet.AlternateLookup> hashList,
+ ReadOnlySpan filePath)
+ {
+ const string streamingAssetsSegment = "StarRail_Data/StreamingAssets/";
+ const string persistentSegment = "StarRail_Data/Persistent/";
+
+ bool isContainStreamingAssets = filePath.Contains(streamingAssetsSegment, StringComparison.OrdinalIgnoreCase);
+ bool isContainPersistent = filePath.Contains(persistentSegment, StringComparison.OrdinalIgnoreCase);
+
+ // Add original path
+ hashList.Add(filePath);
+
+ if (isContainStreamingAssets)
+ {
+ string persistentPath = persistentSegment
+ + filePath[streamingAssetsSegment.Length..].ToString();
+ hashList.Add(persistentPath);
+ }
+
+ if (isContainPersistent)
+ {
+ string streamingAssetsPath = streamingAssetsSegment
+ + filePath[persistentSegment.Length..].ToString();
+ hashList.Add(streamingAssetsPath);
+ }
+ }
}
}
diff --git a/Hi3Helper.Sophon b/Hi3Helper.Sophon
index a672778c9..84c5119f5 160000
--- a/Hi3Helper.Sophon
+++ b/Hi3Helper.Sophon
@@ -1 +1 @@
-Subproject commit a672778c9ed2e4b748b27a43c1bd616946cf6e51
+Subproject commit 84c5119f51af3224ef41e5e08067f8c6eded3ef1
From 523a652c540c328d71a83b0c044eb076a588bb18 Mon Sep 17 00:00:00 2001
From: Kemal Setya Adhi
Date: Fri, 26 Dec 2025 22:49:22 +0700
Subject: [PATCH 10/47] Always normalize blacklist path
---
.../StarRail/StarRailInstall.SophonPatch.cs | 21 ++++++++++++-------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs
index 881509da4..156db6930 100644
--- a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs
+++ b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs
@@ -1,4 +1,4 @@
-using Hi3Helper.Sophon;
+using Hi3Helper.Data;
using System;
using System.Buffers;
using System.Collections.Generic;
@@ -9,11 +9,15 @@
// ReSharper disable InvertIf
#pragma warning disable IDE0130
+#nullable enable
namespace CollapseLauncher.InstallManager.StarRail
{
internal sealed partial class StarRailInstall
{
- protected override async Task FilterSophonPatchAssetList(List itemList, CancellationToken token)
+ public override async Task FilterAssetList(
+ List itemList,
+ Func itemPathSelector,
+ CancellationToken token)
{
string blackListFilePath = Path.Combine(GamePath, @"StarRail_Data\Persistent\DownloadBlacklist.json");
FileInfo fileInfo = new(blackListFilePath);
@@ -37,6 +41,7 @@ protected override async Task FilterSophonPatchAssetList(List
}
// Normalize path
+ ConverterTool.NormalizePathInplaceNoTrim(span);
AddBothPersistentOrStreamingAssets(blackListAlt, span);
}
@@ -48,16 +53,16 @@ protected override async Task FilterSophonPatchAssetList(List
SearchValues searchValues =
SearchValues.Create(blackList.ToArray(), StringComparison.OrdinalIgnoreCase);
- List listFiltered = [];
- foreach (SophonPatchAsset patchAsset in itemList)
+ List listFiltered = [];
+ foreach (T patchAsset in itemList)
{
- if (patchAsset.TargetFilePath == null)
+ if (itemPathSelector(patchAsset) is not {} filePath)
{
listFiltered.Add(patchAsset);
continue;
}
- int indexOfAny = patchAsset.TargetFilePath.IndexOfAny(searchValues);
+ int indexOfAny = filePath.IndexOfAny(searchValues);
if (indexOfAny >= 0)
{
continue;
@@ -98,8 +103,8 @@ private static void AddBothPersistentOrStreamingAssets(
HashSet.AlternateLookup> hashList,
ReadOnlySpan filePath)
{
- const string streamingAssetsSegment = "StarRail_Data/StreamingAssets/";
- const string persistentSegment = "StarRail_Data/Persistent/";
+ const string streamingAssetsSegment = @"StarRail_Data\StreamingAssets\";
+ const string persistentSegment = @"StarRail_Data\Persistent\";
bool isContainStreamingAssets = filePath.Contains(streamingAssetsSegment, StringComparison.OrdinalIgnoreCase);
bool isContainPersistent = filePath.Contains(persistentSegment, StringComparison.OrdinalIgnoreCase);
From 8db75575a3d2303e3e6e9b7f93a70e45cea5efd2 Mon Sep 17 00:00:00 2001
From: Kemal Setya Adhi
Date: Fri, 26 Dec 2025 22:50:02 +0700
Subject: [PATCH 11/47] Adjust FilterSophonPatchAssetList -> FilterAssetList
for ZZZ
---
.../Base/InstallManagerBase.SophonPatch.cs | 7 ++-
.../Zenless/ZenlessInstall.Sophon.cs | 23 -------
.../Zenless/ZenlessInstall.SophonPatch.cs | 60 +++++++++++++------
3 files changed, 46 insertions(+), 44 deletions(-)
delete mode 100644 CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.Sophon.cs
diff --git a/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.SophonPatch.cs b/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.SophonPatch.cs
index 6b5b68600..3d9dfa397 100644
--- a/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.SophonPatch.cs
+++ b/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.SophonPatch.cs
@@ -125,7 +125,7 @@ await GetAlterSophonPatchAssets(httpClient,
Token.Token);
// Filter asset list
- await FilterSophonPatchAssetList(patchAssets.AssetList, Token.Token);
+ await FilterAssetList(patchAssets.AssetList, x => x.TargetFilePath, Token.Token);
// Start the patch pipeline
await StartAlterSophonPatch(httpClient,
@@ -140,7 +140,10 @@ await StartAlterSophonPatch(httpClient,
return true;
}
- protected virtual Task FilterSophonPatchAssetList(List itemList, CancellationToken token)
+ public virtual Task FilterAssetList(
+ List itemList,
+ Func itemPathSelector,
+ CancellationToken token)
{
// NOP
return Task.CompletedTask;
diff --git a/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.Sophon.cs b/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.Sophon.cs
deleted file mode 100644
index 8c4ac349b..000000000
--- a/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.Sophon.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Hi3Helper.Sophon;
-using System.Collections.Generic;
-using System.Threading;
-using System.Threading.Tasks;
-// ReSharper disable CheckNamespace
-
-#nullable enable
-namespace CollapseLauncher.InstallManager.Zenless
-{
- internal partial class ZenlessInstall
- {
- protected override async Task FilterSophonPatchAssetList(List itemList, CancellationToken token)
- {
- HashSet exceptMatchFieldHashSet = await GetExceptMatchFieldHashSet(token);
- if (exceptMatchFieldHashSet.Count == 0)
- {
- return;
- }
-
- FilterSophonAsset(itemList, x => x, exceptMatchFieldHashSet, token);
- }
- }
-}
diff --git a/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs b/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs
index 26415d9b0..17d0ad7c3 100644
--- a/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs
+++ b/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs
@@ -24,7 +24,10 @@ internal partial class ZenlessInstall
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")]
private static extern ref SophonChunksInfo GetChunkAssetChunksInfoAlt(SophonAsset element);
- protected override async Task FilterSophonPatchAssetList(List itemList, CancellationToken token)
+ public override async Task FilterAssetList(
+ List itemList,
+ Func itemPathSelector,
+ CancellationToken token)
{
HashSet exceptMatchFieldHashSet = await GetExceptMatchFieldHashSet(token);
if (exceptMatchFieldHashSet.Count == 0)
@@ -32,13 +35,44 @@ protected override async Task FilterSophonPatchAssetList(List
return;
}
- FilterSophonAsset(itemList, x => x.MainAssetInfo, exceptMatchFieldHashSet, token);
+ FilterSophonAsset(itemList, Selector, exceptMatchFieldHashSet);
+ return;
+
+ string? Selector(T item)
+ {
+ // For Game Update
+ if (item is SophonPatchAsset asset)
+ {
+ if (asset.MainAssetInfo is not { } assetSelected)
+ {
+ return null;
+ }
+
+ token.ThrowIfCancellationRequested();
+ ref SophonChunksInfo chunkInfo = ref Unsafe.IsNullRef(ref assetSelected)
+ ? ref Unsafe.NullRef()
+ : ref GetChunkAssetChunksInfo(assetSelected);
+
+ if (Unsafe.IsNullRef(ref chunkInfo))
+ {
+ chunkInfo = ref GetChunkAssetChunksInfoAlt(assetSelected);
+ }
+
+ return Unsafe.IsNullRef(ref chunkInfo)
+ ? asset.MainAssetInfo.AssetName
+ : chunkInfo.ChunksBaseUrl;
+ }
+
+ // TODO: For Game Repair handle
+
+ return null;
+ }
}
private async Task> GetExceptMatchFieldHashSet(CancellationToken token)
{
string gameExecDataName =
- Path.GetFileNameWithoutExtension(GameVersionManager?.GamePreset.GameExecutableName) ?? "ZenlessZoneZero";
+ Path.GetFileNameWithoutExtension(GameVersionManager.GamePreset.GameExecutableName) ?? "ZenlessZoneZero";
string gameExecDataPath = $"{gameExecDataName}_Data";
string gamePersistentDataPath = Path.Combine(GamePath, gameExecDataPath, "Persistent");
string gameExceptMatchFieldFile = Path.Combine(gamePersistentDataPath, "KDelResource");
@@ -55,7 +89,7 @@ private async Task> GetExceptMatchFieldHashSet(CancellationToken to
}
// ReSharper disable once IdentifierTypo
- private static void FilterSophonAsset(List itemList, Func assetSelector, HashSet exceptMatchFieldHashSet, CancellationToken token)
+ private static void FilterSophonAsset(List itemList, Func assetSelector, HashSet exceptMatchFieldHashSet)
{
const string separators = "/\\";
scoped Span urlPathRanges = stackalloc Range[32];
@@ -63,27 +97,15 @@ private static void FilterSophonAsset(List itemList, Func
List filteredList = [];
foreach (T asset in itemList)
{
- SophonAsset? assetSelected = assetSelector(asset);
-
- token.ThrowIfCancellationRequested();
- ref SophonChunksInfo chunkInfo = ref assetSelected == null
- ? ref Unsafe.NullRef()
- : ref GetChunkAssetChunksInfo(assetSelected);
-
- if (assetSelected != null && Unsafe.IsNullRef(ref chunkInfo))
- {
- chunkInfo = ref GetChunkAssetChunksInfoAlt(assetSelected);
- }
-
- if (Unsafe.IsNullRef(ref chunkInfo))
+ string? assetPath = assetSelector(asset);
+ if (assetPath == null)
{
filteredList.Add(asset);
continue;
}
- ReadOnlySpan manifestUrl = chunkInfo.ChunksBaseUrl;
+ ReadOnlySpan manifestUrl = assetPath;
int rangeLen = manifestUrl.SplitAny(urlPathRanges, separators, SplitOptions);
-
if (rangeLen <= 0)
{
continue;
From 9c1ba768b169b57b9dfe0054b0f3b44037b96d44 Mon Sep 17 00:00:00 2001
From: Kemal Setya Adhi
Date: Fri, 26 Dec 2025 22:56:46 +0700
Subject: [PATCH 12/47] Fix HSR Game Repair still downloads blacklisted files
---
CollapseLauncher/Classes/GamePresetProperty.cs | 4 ++--
.../StarRail/StarRailInstall.cs | 3 ++-
.../RepairManagement/StarRail/StarRailRepair.cs | 17 ++++++++++++-----
3 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/CollapseLauncher/Classes/GamePresetProperty.cs b/CollapseLauncher/Classes/GamePresetProperty.cs
index 37e396bcc..66cdb4ed8 100644
--- a/CollapseLauncher/Classes/GamePresetProperty.cs
+++ b/CollapseLauncher/Classes/GamePresetProperty.cs
@@ -60,9 +60,9 @@ internal static GamePresetProperty Create(UIElement uiElementParent, ILauncherAp
case GameNameType.StarRail:
property.GameVersion = new GameTypeStarRailVersion(launcherApis, gamePreset);
property.GameSettings = new StarRailSettings(property.GameVersion);
- property.GameCache = new StarRailCache(uiElementParent, property.GameVersion, property.GameSettings);
- property.GameRepair = new StarRailRepair(uiElementParent, property.GameVersion, property.GameSettings);
property.GameInstall = new StarRailInstall(uiElementParent, property.GameVersion, property.GameSettings);
+ property.GameCache = new StarRailCache(uiElementParent, property.GameVersion, property.GameSettings);
+ property.GameRepair = new StarRailRepair(uiElementParent, property.GameVersion, property.GameInstall, property.GameSettings);
break;
case GameNameType.Genshin:
property.GameVersion = new GameTypeGenshinVersion(launcherApis, gamePreset);
diff --git a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs
index f6c85f102..af182cf6d 100644
--- a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs
+++ b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs
@@ -93,6 +93,7 @@ public override async ValueTask StartPackageVerification(List
new StarRailRepair(ParentUI,
GameVersionManager,
+ this,
GameSettings,
true,
versionString);
@@ -115,7 +116,7 @@ protected override async Task StartPackageInstallationInner(List OriginAssetIndex { get; set; }
private string ExecName { get; }
@@ -59,6 +61,7 @@ private string GameAudioLangListPath
public StarRailRepair(
UIElement parentUI,
IGameVersion gameVersionManager,
+ IGameInstallManager gameInstallManager,
IGameSettings gameSettings,
bool onlyRecoverMainAsset = false,
string versionOverride = null)
@@ -71,6 +74,7 @@ public StarRailRepair(
// Get flag to only recover main assets
IsOnlyRecoverMain = onlyRecoverMainAsset;
InnerGameVersionManager = gameVersionManager as GameTypeStarRailVersion;
+ InnerGameInstaller = gameInstallManager as StarRailInstall;
ExecName = Path.GetFileNameWithoutExtension(InnerGameVersionManager!.GamePreset.GameExecutableName);
}
@@ -105,15 +109,18 @@ private async Task CheckRoutine()
ResetStatusAndProgress();
// Step 1: Fetch asset indexes
- await Fetch(AssetIndex, Token.Token);
+ await Fetch(AssetIndex, Token!.Token);
- // Step 2: Calculate the total size and count of the files
+ // Step 2: Remove blacklisted files from asset index (borrow function from StarRailInstall)
+ await InnerGameInstaller.FilterAssetList(AssetIndex, x => x.N, Token.Token);
+
+ // Step 3: Calculate the total size and count of the files
CountAssetIndex(AssetIndex);
- // Step 3: Check for the asset indexes integrity
+ // Step 4: Check for the asset indexes integrity
await Check(AssetIndex, Token.Token);
- // Step 4: Summarize and returns true if the assetIndex count != 0 indicates broken file was found.
+ // Step 5: Summarize and returns true if the assetIndex count != 0 indicates broken file was found.
// either way, returns false.
return SummarizeStatusAndProgress(
AssetIndex,
@@ -124,7 +131,7 @@ private async Task CheckRoutine()
private async Task RepairRoutine()
{
// Assign repair task
- Task repairTask = Repair(AssetIndex, Token.Token);
+ Task repairTask = Repair(AssetIndex, Token!.Token);
// Run repair process
bool repairTaskSuccess = await TryRunExamineThrow(repairTask);
From 91971e917482bc16554897ff8e3c0efbf944785a Mon Sep 17 00:00:00 2001
From: Kemal Setya Adhi
Date: Mon, 29 Dec 2025 04:29:17 +0700
Subject: [PATCH 13/47] Attempt for parsing NativeData response
---
.../Versioning/StarRail/VersionCheck.cs | 3 +-
.../Classes/GamePresetProperty.cs | 2 +-
...cUnixStampToDateTimeOffsetJsonConverter.cs | 67 +++++++
.../StarRail/StarRailInstall.cs | 1 -
.../Interfaces/Class/GamePropertyBase.cs | 4 +-
.../HonkaiV2/HonkaiRepairV2.Fetch.cs | 62 +------
.../SophonRepairFetchUtility.cs | 96 ++++++++++
.../RepairManagement/StarRail/Fetch.cs | 104 +++++++----
.../StarRail/StarRailPersistentRefResult.cs | 117 ++++++++++++
.../StarRail/StarRailRepair.cs | 11 +-
.../Struct/Assets/StarRailBinaryDataNative.cs | 170 ++++++++++++++++++
.../StarRail/Struct/StarRailBinaryData.cs | 55 ++++++
.../Struct/StarRailBinaryDataExtension.cs | 105 +++++++++++
Hi3Helper.EncTool | 2 +-
14 files changed, 697 insertions(+), 102 deletions(-)
create mode 100644 CollapseLauncher/Classes/Helper/JsonConverter/UtcUnixStampToDateTimeOffsetJsonConverter.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/SophonRepairFetchUtility.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/StarRailPersistentRefResult.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailBinaryDataNative.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailBinaryData.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailBinaryDataExtension.cs
diff --git a/CollapseLauncher/Classes/GameManagement/Versioning/StarRail/VersionCheck.cs b/CollapseLauncher/Classes/GameManagement/Versioning/StarRail/VersionCheck.cs
index a14552503..26bfd5154 100644
--- a/CollapseLauncher/Classes/GameManagement/Versioning/StarRail/VersionCheck.cs
+++ b/CollapseLauncher/Classes/GameManagement/Versioning/StarRail/VersionCheck.cs
@@ -37,8 +37,7 @@ public GameTypeStarRailVersion(ILauncherApi launcherApi, PresetConfig presetConf
{
throw new NullReferenceException(GamePreset.GameDispatchArrayURL + " is null!");
}
- StarRailMetadataTool = new SRMetadata(
- GamePreset.GameDispatchArrayURL[0],
+ StarRailMetadataTool = new SRMetadata(GamePreset.GameDispatchArrayURL[0],
GamePreset.ProtoDispatchKey,
dispatchUrlTemplate,
gatewayUrlTemplate,
diff --git a/CollapseLauncher/Classes/GamePresetProperty.cs b/CollapseLauncher/Classes/GamePresetProperty.cs
index 66cdb4ed8..159dbc8a9 100644
--- a/CollapseLauncher/Classes/GamePresetProperty.cs
+++ b/CollapseLauncher/Classes/GamePresetProperty.cs
@@ -62,7 +62,7 @@ internal static GamePresetProperty Create(UIElement uiElementParent, ILauncherAp
property.GameSettings = new StarRailSettings(property.GameVersion);
property.GameInstall = new StarRailInstall(uiElementParent, property.GameVersion, property.GameSettings);
property.GameCache = new StarRailCache(uiElementParent, property.GameVersion, property.GameSettings);
- property.GameRepair = new StarRailRepair(uiElementParent, property.GameVersion, property.GameInstall, property.GameSettings);
+ property.GameRepair = new StarRailRepair(uiElementParent, property.GameVersion, property.GameSettings);
break;
case GameNameType.Genshin:
property.GameVersion = new GameTypeGenshinVersion(launcherApis, gamePreset);
diff --git a/CollapseLauncher/Classes/Helper/JsonConverter/UtcUnixStampToDateTimeOffsetJsonConverter.cs b/CollapseLauncher/Classes/Helper/JsonConverter/UtcUnixStampToDateTimeOffsetJsonConverter.cs
new file mode 100644
index 000000000..b634f31fd
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/JsonConverter/UtcUnixStampToDateTimeOffsetJsonConverter.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.Helper.JsonConverter;
+
+internal class UtcUnixStampToDateTimeOffsetJsonConverter : JsonConverter
+{
+ public override DateTimeOffset Read(
+ ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+ if (reader.ValueSpan.IsEmpty)
+ {
+ return default;
+ }
+
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ return double.TryParse(reader.ValueSpan, out double timestampStr)
+ ? Parse(timestampStr)
+ : throw new InvalidOperationException("Cannot parse string value to number for DateTimeOffset.");
+ }
+
+ if (reader.TryGetDouble(out double unixTimestampDouble))
+ {
+ return Parse(unixTimestampDouble);
+ }
+
+ if (reader.TryGetInt64(out long unixTimestampLong))
+ {
+ return Parse(unixTimestampLong);
+ }
+
+ if (reader.TryGetUInt64(out ulong unixTimestampUlong))
+ {
+ return Parse(unixTimestampUlong);
+ }
+
+ throw new InvalidOperationException("Cannot parse number to DateTimeOffset");
+ }
+
+ public override void Write(
+ Utf8JsonWriter writer,
+ DateTimeOffset value,
+ JsonSerializerOptions options)
+ {
+ long number = value.Millisecond != 0
+ ? value.ToUnixTimeMilliseconds()
+ : value.ToUnixTimeSeconds();
+
+ if (options.NumberHandling.HasFlag(JsonNumberHandling.WriteAsString))
+ {
+ writer.WriteStringValue($"{number}");
+ return;
+ }
+
+ writer.WriteNumberValue(number);
+ }
+
+ private static DateTimeOffset Parse(double timestamp) => timestamp >= 1000000000000
+ ? DateTimeOffset.FromUnixTimeMilliseconds((long)Math.Abs(timestamp))
+ : DateTimeOffset.FromUnixTimeSeconds((long)Math.Abs(timestamp));
+}
diff --git a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs
index af182cf6d..c03458352 100644
--- a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs
+++ b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.cs
@@ -93,7 +93,6 @@ public override async ValueTask StartPackageVerification(List
new StarRailRepair(ParentUI,
GameVersionManager,
- this,
GameSettings,
true,
versionString);
diff --git a/CollapseLauncher/Classes/Interfaces/Class/GamePropertyBase.cs b/CollapseLauncher/Classes/Interfaces/Class/GamePropertyBase.cs
index eb1975f49..834fa33ae 100644
--- a/CollapseLauncher/Classes/Interfaces/Class/GamePropertyBase.cs
+++ b/CollapseLauncher/Classes/Interfaces/Class/GamePropertyBase.cs
@@ -75,7 +75,7 @@ protected static int ThreadCount
get => (byte)LauncherConfig.AppCurrentThread;
}
- protected GameVersion GameVersion
+ internal GameVersion GameVersion
{
get
{
@@ -87,7 +87,7 @@ protected GameVersion GameVersion
}
}
- protected IGameVersion GameVersionManager
+ internal IGameVersion GameVersionManager
{
get;
}
diff --git a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs
index 4acbc4ae6..56c47fab4 100644
--- a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs
+++ b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs
@@ -44,64 +44,12 @@ internal class SenadinaFileResult
#region Fetch by Sophon
private async Task FetchAssetFromSophon(List assetIndex, CancellationToken token)
{
- // Set total activity string as "Fetching Caches Type: "
- Status.ActivityStatus = string.Format(Locale.Lang._CachesPage.CachesStatusFetchingType, "Sophon");
- Status.IsProgressAllIndetermined = true;
- Status.IsIncludePerFileIndicator = false;
- UpdateStatus();
-
- PresetConfig gamePreset = GameVersionManager!.GamePreset;
- string? sophonApiUrl = gamePreset.LauncherResourceChunksURL?.MainUrl;
- string? matchingField = gamePreset.LauncherResourceChunksURL?.MainBranchMatchingField;
-
- if (sophonApiUrl == null)
- {
- throw new NullReferenceException("Sophon API URL inside of the metadata is null. Try to clear your metadata by going to \"App Settings\" > \"Clear Metadata and Restart\"");
- }
-
- string versionApiToUse = GameVersion.ToString();
- sophonApiUrl += $"&tag={versionApiToUse}";
-
- SophonChunkManifestInfoPair infoPair = await SophonManifest
- .CreateSophonChunkManifestInfoPair(HttpClientGeneric,
- sophonApiUrl,
- matchingField,
- false,
+ await this.FetchAssetsFromSophonAsync(HttpClientGeneric,
+ assetIndex,
+ DetermineFileTypeFromExtension,
+ GameVersion,
+ ["en-us", "zh-cn", "ja-jp", "ko-kr"],
token);
-
- if (!infoPair.IsFound)
- {
- throw new InvalidOperationException($"Sophon cannot find matching field: {matchingField} from API URL: {sophonApiUrl}");
- }
-
- SearchValues excludedMatchingFields =
- SearchValues.Create(["en-us", "zh-cn", "ja-jp", "ko-kr"], StringComparison.OrdinalIgnoreCase);
- List infoPairs = [infoPair];
- infoPairs.AddRange(infoPair
- .OtherSophonBuildData?
- .ManifestIdentityList
- .Where(x => !x.MatchingField
- .ContainsAny(excludedMatchingFields) && !x.MatchingField.Equals(matchingField))
- .Select(x => infoPair.GetOtherManifestInfoPair(x.MatchingField)) ?? []);
-
-
- foreach (SophonChunkManifestInfoPair pair in infoPairs)
- {
- await foreach (SophonAsset asset in SophonManifest
- .EnumerateAsync(HttpClientGeneric,
- pair,
- token: token))
- {
- assetIndex.Add(new FilePropertiesRemote
- {
- AssociatedObject = asset,
- S = asset.AssetSize,
- N = asset.AssetName.NormalizePath(),
- CRC = asset.AssetHash,
- FT = DetermineFileTypeFromExtension(asset.AssetName)
- });
- }
- }
}
#endregion
diff --git a/CollapseLauncher/Classes/RepairManagement/SophonRepairFetchUtility.cs b/CollapseLauncher/Classes/RepairManagement/SophonRepairFetchUtility.cs
new file mode 100644
index 000000000..ffc75af88
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/SophonRepairFetchUtility.cs
@@ -0,0 +1,96 @@
+using CollapseLauncher.Helper.Metadata;
+using CollapseLauncher.Helper.StreamUtility;
+using CollapseLauncher.Interfaces;
+using Hi3Helper;
+using Hi3Helper.Plugin.Core.Management;
+using Hi3Helper.Shared.ClassStruct;
+using Hi3Helper.Sophon;
+using Hi3Helper.Sophon.Structs;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+#nullable enable
+#pragma warning disable IDE0130
+namespace CollapseLauncher.RepairManagement;
+
+internal static class RepairSharedUtility
+{
+ public static async Task FetchAssetsFromSophonAsync(
+ this ProgressBase instance,
+ HttpClient client,
+ List assetIndex,
+ Func assetTypeDeterminer,
+ GameVersion gameVersion,
+ string[] excludeMatchingFieldList,
+ CancellationToken token = default)
+ {
+ // Set total activity string as "Fetching Caches Type: "
+ instance.Status.ActivityStatus = string.Format(Locale.Lang._CachesPage.CachesStatusFetchingType, "Sophon");
+ instance.Status.IsProgressAllIndetermined = true;
+ instance.Status.IsIncludePerFileIndicator = false;
+ instance.UpdateStatus();
+
+ PresetConfig gamePreset = instance.GameVersionManager.GamePreset;
+ string? sophonApiUrl = gamePreset.LauncherResourceChunksURL?.MainUrl;
+ string? matchingField = gamePreset.LauncherResourceChunksURL?.MainBranchMatchingField;
+
+ if (sophonApiUrl == null)
+ {
+ throw new NullReferenceException("Sophon API URL inside of the metadata is null. Try to clear your metadata by going to \"App Settings\" > \"Clear Metadata and Restart\"");
+ }
+
+ string versionApiToUse = gameVersion.ToString();
+ sophonApiUrl += $"&tag={versionApiToUse}";
+
+ SophonChunkManifestInfoPair infoPair = await SophonManifest
+ .CreateSophonChunkManifestInfoPair(client,
+ sophonApiUrl,
+ matchingField,
+ false,
+ token);
+
+ if (!infoPair.IsFound)
+ {
+ throw new InvalidOperationException($"Sophon cannot find matching field: {matchingField} from API URL: {sophonApiUrl}");
+ }
+
+ SearchValues excludedMatchingFields = SearchValues.Create(excludeMatchingFieldList, StringComparison.OrdinalIgnoreCase);
+ List infoPairs = [infoPair];
+ infoPairs.AddRange(infoPair
+ .OtherSophonBuildData?
+ .ManifestIdentityList
+ .Where(x => !x.MatchingField
+ .ContainsAny(excludedMatchingFields) && !x.MatchingField.Equals(matchingField))
+ .Select(x => infoPair.GetOtherManifestInfoPair(x.MatchingField)) ?? []);
+
+
+ foreach (SophonChunkManifestInfoPair pair in infoPairs)
+ {
+ instance.Status.ActivityStatus = string.Format(Locale.Lang._CachesPage.CachesStatusFetchingType, $"Sophon ({pair.MatchingField})");
+ instance.Status.IsProgressAllIndetermined = true;
+ instance.Status.IsIncludePerFileIndicator = false;
+ instance.UpdateStatus();
+
+ await foreach (SophonAsset asset in SophonManifest
+ .EnumerateAsync(client,
+ pair,
+ token: token))
+ {
+ assetIndex.Add(new FilePropertiesRemote
+ {
+ AssociatedObject = asset,
+ S = asset.AssetSize,
+ N = asset.AssetName.NormalizePath(),
+ CRC = asset.AssetHash,
+ FT = assetTypeDeterminer(asset.AssetName)
+ });
+ }
+ }
+ }
+
+}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs
index 0546e066f..ac32206fb 100644
--- a/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs
@@ -1,8 +1,10 @@
using CollapseLauncher.Helper;
using CollapseLauncher.Helper.Metadata;
+using CollapseLauncher.RepairManagement;
using Hi3Helper;
using Hi3Helper.Data;
using Hi3Helper.EncTool.Parser.AssetIndex;
+using Hi3Helper.EncTool.Parser.AssetMetadata;
using Hi3Helper.EncTool.Parser.AssetMetadata.SRMetadataAsset;
using Hi3Helper.Http;
using Hi3Helper.Shared.ClassStruct;
@@ -25,6 +27,7 @@
// ReSharper disable StringLiteralTypo
// ReSharper disable SwitchStatementHandlesSomeKnownEnumValuesWithDefault
+#nullable enable
namespace CollapseLauncher
{
internal static partial class StarRailRepairExtension
@@ -101,13 +104,32 @@ private async Task Fetch(List assetIndex, CancellationToke
}
}
+ string regionId = GetExistingGameRegionID();
+
+ // Fetch assets from game server
+ if (!IsVersionOverride &&
+ !IsOnlyRecoverMain)
+ {
+ PresetConfig gamePreset = GameVersionManager.GamePreset;
+ SRDispatcherInfo dispatcherInfo = new(gamePreset.GameDispatchArrayURL,
+ gamePreset.ProtoDispatchKey,
+ gamePreset.GameDispatchURLTemplate,
+ gamePreset.GameGatewayURLTemplate,
+ gamePreset.GameDispatchChannelName,
+ GameVersionManager.GetGameVersionApi().ToString());
+ await dispatcherInfo.Initialize(client, regionId, token);
+
+ Task persistentRef = StarRailPersistentRefResult
+ .GetReferenceAsync(this, dispatcherInfo, client, GameDataPersistentPath, token);
+ }
+
// Subscribe the fetching progress and subscribe StarRailMetadataTool progress to adapter
// _innerGameVersionManager.StarRailMetadataTool.HttpEvent += _httpClient_FetchAssetProgress;
// Initialize the metadata tool (including dispatcher and gateway).
// Perform this if only base._isVersionOverride is false to indicate that the repair performed is
// not for delta patch integrity check.
- if (!IsVersionOverride && !IsOnlyRecoverMain && await InnerGameVersionManager.StarRailMetadataTool.Initialize(token, downloadClient, _httpClient_FetchAssetProgress, GetExistingGameRegionID(), Path.Combine(GamePath, $"{Path.GetFileNameWithoutExtension(InnerGameVersionManager.GamePreset.GameExecutableName)}_Data\\Persistent")))
+ if (!IsVersionOverride && !IsOnlyRecoverMain && await InnerGameVersionManager.StarRailMetadataTool.Initialize(token, downloadClient, _httpClient_FetchAssetProgress, regionId, Path.Combine(GamePath, $"{Path.GetFileNameWithoutExtension(InnerGameVersionManager.GamePreset.GameExecutableName)}_Data\\Persistent")))
{
await Task.WhenAll(
// Read Block metadata
@@ -147,43 +169,37 @@ await Task.WhenAll(
#region PrimaryManifest
private async Task GetPrimaryManifest(List assetIndex, CancellationToken token)
{
- // Initialize pkgVersion list
- List pkgVersion = [];
+ // 2025/12/28:
+ // Starting from this, we use Sophon as primary manifest source instead of relying on our Game Repair Index
+ // as miHoYo might remove uncompressed files from their CDN and fully moving to Sophon.
- // Initialize repo metadata
- try
- {
- // Get the metadata
- Dictionary repoMetadata = await FetchMetadata(token);
+ HttpClient client = FallbackCDNUtil.GetGlobalHttpClient(true);
- // Check for manifest. If it doesn't exist, then throw and warn the user
- if (!repoMetadata.TryGetValue(GameVersion.VersionString, out var value))
- {
- throw new VersionNotFoundException($"Manifest for {GameVersionManager.GamePreset.ZoneName} (version: {GameVersion.VersionString}) doesn't exist! Please contact @neon-nyan or open an issue for this!");
- }
-
- // Assign the URL based on the version
- GameRepoURL = value;
+ string[] excludedMatchingField = ["en-us", "zh-cn", "ja-jp", "ko-kr"];
+ if (File.Exists(GameAudioLangListPathStatic))
+ {
+ string[] installedAudioLang = (await File.ReadAllLinesAsync(GameAudioLangListPathStatic, token))
+ .Select(x => x switch
+ {
+ "English" => "en-us",
+ "Japanese" => "ja-jp",
+ "Chinese(PRC)" => "zh-cn",
+ "Korean" => "ko-kr",
+ _ => ""
+ })
+ .Where(x => !string.IsNullOrEmpty(x))
+ .ToArray();
+
+ excludedMatchingField = excludedMatchingField.Where(x => !installedAudioLang.Contains(x))
+ .ToArray();
}
- // If the base._isVersionOverride is true, then throw. This sanity check is required if the delta patch is being performed.
- catch when (IsVersionOverride) { throw; }
-
- // Fetch the asset index from CDN
- // Set asset index URL
- string urlIndex = string.Format(LauncherConfig.AppGameRepairIndexURLPrefix, GameVersionManager.GamePreset.ProfileName, GameVersion.VersionString) + ".binv2";
-
- // Start downloading asset index using FallbackCDNUtil and return its stream
- await using Stream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlIndex, token: token);
- // Deserialize asset index and set it to list
- AssetIndexV2 parserTool = new AssetIndexV2();
- pkgVersion = parserTool.Deserialize(stream, out DateTime timestamp);
- LogWriteLine($"Asset index timestamp: {timestamp}", LogType.Default, true);
- // Convert the pkg version list to asset index
- ConvertPkgVersionToAssetIndex(pkgVersion, assetIndex);
-
- // Clear the pkg version list
- pkgVersion.Clear();
+ await this.FetchAssetsFromSophonAsync(client,
+ assetIndex,
+ DetermineFileTypeFromExtension,
+ GameVersion,
+ excludedMatchingField,
+ token);
}
private async Task> FetchMetadata(CancellationToken token)
@@ -215,6 +231,26 @@ private void ConvertPkgVersionToAssetIndex(List pkgVersion
#endregion
#region Utilities
+ private static FileType DetermineFileTypeFromExtension(string fileName)
+ {
+ if (fileName.EndsWith(".block", StringComparison.OrdinalIgnoreCase))
+ {
+ return FileType.Block;
+ }
+
+ if (fileName.EndsWith(".usm", StringComparison.OrdinalIgnoreCase))
+ {
+ return FileType.Video;
+ }
+
+ if (fileName.EndsWith(".pck", StringComparison.OrdinalIgnoreCase))
+ {
+ return FileType.Audio;
+ }
+
+ return FileType.Generic;
+ }
+
private FilePropertiesRemote GetNormalizedFilePropertyTypeBased(string remoteParentURL,
string remoteRelativePath,
long fileSize,
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailPersistentRefResult.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailPersistentRefResult.cs
new file mode 100644
index 000000000..801431c03
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailPersistentRefResult.cs
@@ -0,0 +1,117 @@
+using CollapseLauncher.Helper.JsonConverter;
+using CollapseLauncher.Helper.StreamUtility;
+using CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
+using Hi3Helper;
+using Hi3Helper.Data;
+using Hi3Helper.EncTool;
+using Hi3Helper.EncTool.Parser.AssetMetadata;
+using Hi3Helper.EncTool.Proto.StarRail;
+using Microsoft.UI.Xaml.Shapes;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+using Path = System.IO.Path;
+
+#nullable enable
+#pragma warning disable IDE0130
+namespace CollapseLauncher;
+
+internal class StarRailPersistentRefResult
+{
+ public static async Task GetReferenceAsync(
+ StarRailRepair instance,
+ SRDispatcherInfo dispatcherInfo,
+ HttpClient client,
+ string persistentDir,
+ CancellationToken token)
+ {
+ StarRailGatewayStatic gateway = dispatcherInfo.RegionGateway;
+ Dictionary gatewayKvp = gateway.ValuePairs;
+
+ string mainUrlAsb = gatewayKvp["AssetBundleVersionUpdateUrl"];
+ string mainUrlDesignData = gatewayKvp["DesignDataBundleVersionUpdateUrl"];
+
+ string mInfoDesignArchiveUrl = mainUrlDesignData.CombineURLFromString("client/Windows/M_Design_ArchiveV.bytes");
+ Dictionary mInfoDesignArchive = await StarRailRefMainInfo
+ .ParseListFromUrlAsync(instance,
+ client,
+ mInfoDesignArchiveUrl,
+ token);
+
+ string mInfoArchiveUrl = mainUrlAsb.CombineURLFromString("client/Windows/Archive/M_ArchiveV.bytes");
+ Dictionary mInfoArchive = await StarRailRefMainInfo
+ .ParseListFromUrlAsync(instance,
+ client,
+ mInfoArchiveUrl,
+ token);
+
+ await using FileStream stream =
+ File.OpenRead(@"C:\Users\neon-nyan\AppData\LocalLow\CollapseLauncher\GameFolder\SRGlb\Games\StarRail_Data\StreamingAssets\NativeData\Windows\NativeDataV_2ebcf9e27323a0561ef4825a14819ed5.bytes");
+
+ StarRailBinaryDataNative binaryRefNativeData = new();
+ await binaryRefNativeData.ParseAsync(stream, token);
+
+ return default;
+ }
+}
+
+[JsonSerializable(typeof(StarRailRefMainInfo))]
+internal partial class StarRailRepairJsonContext : JsonSerializerContext;
+
+internal class StarRailRefMainInfo
+{
+ [JsonPropertyOrder(0)] public int MajorVersion { get; init; }
+ [JsonPropertyOrder(1)] public int MinorVersion { get; init; }
+ [JsonPropertyOrder(2)] public int PatchVersion { get; init; }
+ [JsonPropertyOrder(3)] public int PrevPatch { get; init; }
+ [JsonPropertyOrder(4)] public string ContentHash { get; init; } = "";
+ [JsonPropertyOrder(5)] public long FileSize { get; init; }
+ [JsonPropertyOrder(7)] public string FileName { get; init; } = "";
+ [JsonPropertyOrder(8)] public string BaseAssetsDownloadUrl { get; init; } = "";
+
+ [JsonPropertyOrder(6)]
+ [JsonConverter(typeof(UtcUnixStampToDateTimeOffsetJsonConverter))]
+ public DateTimeOffset TimeStamp { get; init; }
+
+ [JsonIgnore]
+ public string UnaliasedFileName =>
+ FileName.StartsWith("M_", StringComparison.OrdinalIgnoreCase) ? FileName[2..] : FileName;
+
+ [JsonIgnore]
+ public string RemoteFileName => field ??= $"{UnaliasedFileName}_{ContentHash}.bytes";
+
+ public override string ToString() => RemoteFileName;
+
+ public static async Task> ParseListFromUrlAsync(
+ StarRailRepair instance,
+ HttpClient client,
+ string url,
+ CancellationToken token)
+ {
+ // Set total activity string as "Fetching Caches Type: "
+ instance.Status.ActivityStatus = string.Format(Locale.Lang._CachesPage.CachesStatusFetchingType, $"Game Ref: {Path.GetFileNameWithoutExtension(url)}");
+ instance.Status.IsProgressAllIndetermined = true;
+ instance.Status.IsIncludePerFileIndicator = false;
+ instance.UpdateStatus();
+
+ await using Stream networkStream = (await client.TryGetCachedStreamFrom(url, token: token)).Stream;
+
+ Dictionary returnList = [];
+ using StreamReader reader = new(networkStream);
+ while (await reader.ReadLineAsync(token) is { } line)
+ {
+ StarRailRefMainInfo refInfo = line.Deserialize(StarRailRepairJsonContext.Default.StarRailRefMainInfo)
+ ?? throw new NullReferenceException();
+
+ returnList.Add(refInfo.UnaliasedFileName, refInfo);
+ }
+
+ return returnList;
+ }
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailRepair.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailRepair.cs
index b7f993867..72beddad7 100644
--- a/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailRepair.cs
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailRepair.cs
@@ -1,6 +1,7 @@
using CollapseLauncher.GameVersioning;
using CollapseLauncher.InstallManager.StarRail;
using CollapseLauncher.Interfaces;
+using CollapseLauncher.Statics;
using Hi3Helper.Data;
using Hi3Helper.Shared.ClassStruct;
using Microsoft.UI.Xaml;
@@ -23,8 +24,12 @@ public override string GamePath
set => GameVersionManager.GameDirPath = value;
}
- private GameTypeStarRailVersion InnerGameVersionManager { get; }
- private StarRailInstall InnerGameInstaller { get; }
+ private GameTypeStarRailVersion InnerGameVersionManager { get; }
+ private StarRailInstall InnerGameInstaller
+ {
+ get => field ??= GamePropertyVault.GetCurrentGameProperty().GameInstall as StarRailInstall;
+ }
+
private bool IsOnlyRecoverMain { get; }
private List OriginAssetIndex { get; set; }
private string ExecName { get; }
@@ -61,7 +66,6 @@ private string GameAudioLangListPath
public StarRailRepair(
UIElement parentUI,
IGameVersion gameVersionManager,
- IGameInstallManager gameInstallManager,
IGameSettings gameSettings,
bool onlyRecoverMainAsset = false,
string versionOverride = null)
@@ -74,7 +78,6 @@ public StarRailRepair(
// Get flag to only recover main assets
IsOnlyRecoverMain = onlyRecoverMainAsset;
InnerGameVersionManager = gameVersionManager as GameTypeStarRailVersion;
- InnerGameInstaller = gameInstallManager as StarRailInstall;
ExecName = Path.GetFileNameWithoutExtension(InnerGameVersionManager!.GamePreset.GameExecutableName);
}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailBinaryDataNative.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailBinaryDataNative.cs
new file mode 100644
index 000000000..6f96d2803
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailBinaryDataNative.cs
@@ -0,0 +1,170 @@
+using Hi3Helper.EncTool;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+#pragma warning disable IDE0130
+#nullable enable
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
+
+internal class StarRailBinaryDataNative : StarRailBinaryData
+{
+ protected override ReadOnlySpan MagicSignature => "SRBM"u8;
+
+ protected override async ValueTask ReadDataCoreAsync(long currentOffset, Stream dataStream, CancellationToken token)
+ {
+ (HeaderStruct header, int readHeader) = await dataStream
+ .ReadDataAssertAndSeekAsync(x => x.AssetInfoStructSize, token)
+ .ConfigureAwait(false);
+ currentOffset += readHeader;
+
+ Debug.Assert(header.FilenameBufferStartOffset == currentOffset); // ASSERT: Make sure the current data stream offset is at the exact filename buffer offset.
+ Debug.Assert(header.AssetCount is < ushort.MaxValue and > 0); // ASSERT: Make sure the data isn't more than ushort.MaxValue and not empty.
+
+ // Read file info
+ int fileInfoBufferLen = header.AssetInfoStructSize * header.AssetCount;
+ byte[] filenameBuffer = ArrayPool.Shared.Rent(header.FilenameBufferSize);
+ byte[] fileInfoBuffer = ArrayPool.Shared.Rent(fileInfoBufferLen);
+ try
+ {
+
+ // Read filename buffer
+ int read = await dataStream.ReadAtLeastAsync(filenameBuffer.AsMemory(0, header.FilenameBufferSize),
+ header.FilenameBufferSize,
+ cancellationToken: token)
+ .ConfigureAwait(false);
+ Debug.Assert(header.FilenameBufferSize == read); // ASSERT: Make sure the filename buffer size is equal as what we read.
+
+ currentOffset += read;
+ int paddingOffsetToInfoBuffer = header.AssetInfoAbsoluteStartOffset - (int)currentOffset;
+ await dataStream.SeekForwardAsync(paddingOffsetToInfoBuffer, token); // ASSERT: Seek forward to the asset info buffer.
+
+ // Read file info buffer
+ read = await dataStream.ReadAtLeastAsync(fileInfoBuffer.AsMemory(0, fileInfoBufferLen),
+ fileInfoBufferLen,
+ cancellationToken: token);
+ Debug.Assert(fileInfoBufferLen == read); // ASSERT: Make sure the asset info struct size is equal as what we read.
+ currentOffset += read;
+
+ // Create spans
+ Span filenameBufferSpan = filenameBuffer.AsSpan(0, header.FilenameBufferSize);
+ Span fileInfoBufferSpan = fileInfoBuffer.AsSpan(0, fileInfoBufferLen);
+
+ // Allocate list
+ DataList = new List(header.AssetCount);
+
+ for (int i = 0; i < header.AssetCount && !filenameBufferSpan.IsEmpty; i++)
+ {
+ ref StarRailAssetNative.StarRailAssetNativeInfo fileInfo =
+ ref MemoryMarshal.AsRef(fileInfoBufferSpan);
+ filenameBufferSpan = StarRailAssetNative.Parse(filenameBufferSpan,
+ ref fileInfo,
+ out StarRailAssetNative? asset);
+ fileInfoBufferSpan = fileInfoBufferSpan[header.AssetInfoStructSize..];
+
+ if (asset == null)
+ {
+ throw new IndexOutOfRangeException("Failed to parse NativeData assets as the buffer data might be insufficient or out-of-bounds!");
+ }
+ DataList.Add(asset);
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(filenameBuffer);
+ ArrayPool.Shared.Return(fileInfoBuffer);
+ }
+
+ return currentOffset;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ private struct HeaderStruct
+ {
+ public int Reserved;
+ ///
+ /// The absolute offset of struct array in the data stream.
+ ///
+ public int AssetInfoAbsoluteStartOffset;
+ ///
+ /// The count of the struct array.
+ ///
+ public int AssetCount;
+ ///
+ /// The size of the struct.
+ ///
+ public int AssetInfoStructSize;
+ public int Unknown1;
+ ///
+ /// The absolute offset of filename buffer in the data stream.
+ ///
+ public int FilenameBufferStartOffset;
+ public int Unknown2;
+ ///
+ /// The size of the filename buffer in bytes.
+ ///
+ public int FilenameBufferSize;
+ }
+}
+
+internal class StarRailAssetNative
+{
+ public required string Filename { get; init; }
+ public required long Size { get; init; }
+ public required byte[] MD5Checksum { get; init; }
+
+ public static Span Parse(Span filenameBuffer,
+ ref StarRailAssetNativeInfo assetInfo,
+ out StarRailAssetNative? result)
+ {
+ Unsafe.SkipInit(out result);
+ int filenameLength = assetInfo.FilenameLength;
+ if (filenameBuffer.Length < filenameLength)
+ {
+ return filenameBuffer;
+ }
+
+ string filename = Encoding.UTF8.GetString(filenameBuffer[..filenameLength]);
+ long fileSize = assetInfo.Size;
+ byte[] md5Checksum = new byte[16];
+
+ assetInfo.Shifted4BytesMD5Checksum.CopyTo(md5Checksum);
+ StarRailBinaryDataExtension.ReverseReorderBy4X4HashData(md5Checksum); // Reorder 4x4 hash using SIMD
+
+ result = new StarRailAssetNative
+ {
+ Filename = filename,
+ Size = fileSize,
+ MD5Checksum = md5Checksum
+ };
+
+ return filenameBuffer[filenameLength..];
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ public unsafe struct StarRailAssetNativeInfo
+ {
+ private fixed byte _shifted4BytesMD5Checksum[16];
+ public long Size;
+ public int FilenameLength;
+ public int FilenameStartAt;
+
+ public ReadOnlySpan Shifted4BytesMD5Checksum
+ {
+ get
+ {
+ fixed (byte* magicP = _shifted4BytesMD5Checksum)
+ {
+ return new ReadOnlySpan(magicP, 16);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailBinaryData.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailBinaryData.cs
new file mode 100644
index 000000000..57b368429
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailBinaryData.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct;
+
+internal abstract class StarRailBinaryData
+{
+ protected abstract ReadOnlySpan MagicSignature { get; }
+ public StarRailBinaryHeader Header { get; private set; }
+ public List DataList { get; protected set; }
+
+ public virtual async Task> ParseAsync(Stream dataStream, CancellationToken token)
+ {
+ (Header, int offset) = await dataStream.ReadDataAssertAndSeekAsync(x => x.SubStructStartOffset, token);
+ if (!Header.MagicSignature.SequenceEqual(MagicSignature))
+ {
+ throw new InvalidOperationException($"Magic Signature doesn't match! Expecting: {Encoding.UTF8.GetString(MagicSignature)} but got: {Encoding.UTF8.GetString(Header.MagicSignature)} instead.");
+ }
+
+ await ReadDataCoreAsync(offset, dataStream, token);
+ return this;
+ }
+
+ protected abstract ValueTask ReadDataCoreAsync(long currentOffset, Stream dataStream, CancellationToken token);
+}
+
+[StructLayout(LayoutKind.Sequential, Pack = 2)]
+public unsafe struct StarRailBinaryHeader
+{
+ private fixed byte _magicSignature[4];
+ public short ParentTypeFlag;
+ public short TypeVersionFlag;
+ public int HeaderLength;
+ public short SubStructCount;
+ public short SubStructStartOffset;
+
+ public Span MagicSignature
+ {
+ get
+ {
+ fixed (byte* magicP = _magicSignature)
+ {
+ return new Span(magicP, 4);
+ }
+ }
+ }
+
+ public override string ToString() => $"{Encoding.UTF8.GetString(MagicSignature)} | ParentTypeFlag: {ParentTypeFlag} | TypeVersionFlag: {TypeVersionFlag} | SubStructCount: {SubStructCount} | SubStructStartOffset: {SubStructStartOffset} | {HeaderLength} bytes";
+}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailBinaryDataExtension.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailBinaryDataExtension.cs
new file mode 100644
index 000000000..bd01e3d62
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailBinaryDataExtension.cs
@@ -0,0 +1,105 @@
+using Hi3Helper.EncTool;
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.X86;
+using System.Threading;
+using System.Threading.Tasks;
+// ReSharper disable InconsistentNaming
+
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct;
+
+internal static class StarRailBinaryDataExtension
+{
+ internal static async ValueTask<(T Data, int Read)> ReadDataAssertAndSeekAsync(
+ this Stream stream,
+ Func minimalSizeAssertGet,
+ CancellationToken token)
+ where T : unmanaged
+ {
+ T result = await stream.ReadAsync(token).ConfigureAwait(false);
+ int sizeOfImplemented = Unsafe.SizeOf();
+ int minimalSizeToAssert = minimalSizeAssertGet(result);
+
+ // ASSERT: Make sure the struct size is versionable and at least, bigger than what we currently implement.
+ // (cuz we know you might change this in the future, HoYo :/)
+ if (sizeOfImplemented > minimalSizeToAssert)
+ {
+ throw new InvalidOperationException($"Game data use {minimalSizeToAssert} bytes of struct for {nameof(T)} while current implementation only supports struct with size >= {sizeOfImplemented}. Please contact @neon-nyan or ping us on our Official Discord to report this issue :D");
+ }
+
+ // ASSERT: Make sure to advance the stream position if the struct is bigger than what we currently implement.
+ int read = sizeOfImplemented;
+ int remained = minimalSizeToAssert - read;
+ read += await stream.SeekForwardAsync(remained, token);
+
+ return (result, read);
+ }
+
+ internal static unsafe void ReverseReorderBy4X4HashData(Span data)
+ {
+ if (data.Length != 16)
+ throw new ArgumentException("Data length must be multiple of 4x4.", nameof(data));
+
+ void* dataP = Unsafe.AsPointer(ref MemoryMarshal.GetReference(data));
+ if (Sse3.IsSupported)
+ {
+ ReverseByInt32X4Sse3(dataP);
+ return;
+ }
+
+ if (Sse2.IsSupported)
+ {
+ ReverseByInt32X4Sse2(dataP);
+ return;
+ }
+
+ ReverseByInt32X4Scalar(dataP);
+ }
+
+ private static readonly Vector128 ReverseByInt32X4ByteMask =
+ Vector128.Create((byte)3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static unsafe void ReverseByInt32X4Sse3(void* int32X4VectorP)
+ => *(Vector128*)int32X4VectorP = Ssse3.Shuffle(*(Vector128*)int32X4VectorP, ReverseByInt32X4ByteMask); // Swap
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static unsafe void ReverseByInt32X4Sse2(void* int32X4VectorP)
+ {
+ Vector128 vector = *(Vector128*)int32X4VectorP;
+
+ // Masks
+ var mask00FF = Vector128.Create(0x00ff00ffu);
+ var maskFF00 = Vector128.Create(0xff00ff00u);
+
+ // Swap bytes within 16-bit halves
+ Vector128 t1 = Sse2.ShiftLeftLogical(vector, 8);
+ Vector128 t2 = Sse2.ShiftRightLogical(vector, 8);
+
+ vector = Sse2.Or(Sse2.And(t1, maskFF00),
+ Sse2.And(t2, mask00FF));
+
+ // Swap 16-bit halves
+ Vector128 result = Sse2.Or(Sse2.ShiftLeftLogical(vector, 16),
+ Sse2.ShiftRightLogical(vector, 16));
+ *(Vector128*)int32X4VectorP = result;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static unsafe void ReverseByInt32X4Scalar(void* int32X4P)
+ {
+ uint* uintP = (uint*)int32X4P;
+
+ uintP[0] = BinaryPrimitives.ReverseEndianness(uintP[0]);
+ uintP[1] = BinaryPrimitives.ReverseEndianness(uintP[1]);
+ uintP[2] = BinaryPrimitives.ReverseEndianness(uintP[2]);
+ uintP[3] = BinaryPrimitives.ReverseEndianness(uintP[3]);
+ }
+}
+
diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool
index 15f443056..d1f7c1ed9 160000
--- a/Hi3Helper.EncTool
+++ b/Hi3Helper.EncTool
@@ -1 +1 @@
-Subproject commit 15f443056e3019118ed37d653c6c06c13b368158
+Subproject commit d1f7c1ed921b58158f5ad25f232a81b0ad8fb8d9
From 0da50fc82253eb3a0233be8620bc605fe2c967d7 Mon Sep 17 00:00:00 2001
From: Bagus Nur Listiyono
Date: Tue, 30 Dec 2025 01:25:09 +0700
Subject: [PATCH 14/47] [DiscordRPC] Fix toggle not actually disabling RPC
Signed-off-by: Bagus Nur Listiyono
---
.../DiscordPresence/DiscordPresenceManager.cs | 34 ++++++++++++++-----
.../RegionManagement/RegionManagement.cs | 2 +-
.../XAMLs/MainApp/MainPage.xaml.cs | 4 +--
.../XAMLs/MainApp/Pages/SettingsPage.xaml.cs | 13 ++++---
4 files changed, 35 insertions(+), 18 deletions(-)
diff --git a/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs b/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs
index cbdea80e9..660f0811d 100644
--- a/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs
+++ b/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs
@@ -37,6 +37,24 @@ public enum ActivityType
public sealed partial class DiscordPresenceManager : IDisposable
{
#region Properties
+
+ [field: NonSerialized]
+ private bool? _isRpcEnabled;
+ public bool IsRpcEnabled
+ {
+ get => _isRpcEnabled ??= GetAppConfigValue("EnableDiscordRPC").ToBoolNullable() ?? false;
+ set
+ {
+ if (_isRpcEnabled == value) return;
+
+ _isRpcEnabled = value;
+
+ SetAndSaveConfigValue("EnableDiscordRPC", value);
+ if (value) SetupPresence();
+ else DisablePresence();
+ }
+ }
+
private const string CollapseLogoExt = "https://collapselauncher.com/img/logo@2x.webp";
private DiscordRpcClient? _client;
@@ -46,8 +64,8 @@ public sealed partial class DiscordPresenceManager : IDisposable
private DateTime? _lastPlayTime;
private bool _firstTimeConnect = true;
private readonly ActionBlock _presenceUpdateQueue;
-
- private bool _cachedIsIdleEnabled = true;
+
+ private bool _cachedIsIdleEnabled = true;
public bool IdleEnabled
{
@@ -103,6 +121,7 @@ public void Dispose()
private void EnablePresence(ulong applicationId)
{
+ if (!IsRpcEnabled) return;
_firstTimeConnect = true;
// Flush and dispose the session
@@ -168,8 +187,10 @@ public void DisablePresence()
public void SetupPresence()
{
- string? gameCategory = GetAppConfigValue("GameCategory").ToString();
- bool isGameStatusEnabled = GetAppConfigValue("EnableDiscordGameStatus").ToBool();
+ if (!IsRpcEnabled) return;
+
+ var gameCategory = GetAppConfigValue("GameCategory").ToString();
+ var isGameStatusEnabled = GetAppConfigValue("EnableDiscordGameStatus").ToBool();
if (isGameStatusEnabled)
{
@@ -220,10 +241,7 @@ private bool TryEnablePresenceIfPlugin()
public void SetActivity(ActivityType activity, DateTime? activityOffset = null)
{
- if (!GetAppConfigValue("EnableDiscordRPC").ToBool())
- {
- return;
- }
+ if (!IsRpcEnabled) return;
//_lastAttemptedActivityType = activity;
_activityType = activity;
diff --git a/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs b/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs
index 0f3ffbb11..a408dce14 100644
--- a/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs
+++ b/CollapseLauncher/Classes/RegionManagement/RegionManagement.cs
@@ -489,7 +489,7 @@ private async Task LoadRegionRootButton()
LogWriteLine($"Region changed to {Preset.ZoneFullname}", LogType.Scheme, true);
#if !DISABLEDISCORD
- if (GetAppConfigValue("EnableDiscordRPC").ToBool())
+ if (AppDiscordPresence.IsRpcEnabled)
AppDiscordPresence.SetupPresence();
#endif
return true;
diff --git a/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs b/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs
index 1f85b2483..7f1a15ab7 100644
--- a/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs
+++ b/CollapseLauncher/XAMLs/MainApp/MainPage.xaml.cs
@@ -881,8 +881,8 @@ private async void ChangeToActivatedRegion()
if (await LoadRegionFromCurrentConfigV2(preset, gameName, gameRegion))
{
#if !DISABLEDISCORD
- if (GetAppConfigValue("EnableDiscordRPC").ToBool() && !sameRegion)
- AppDiscordPresence?.SetupPresence();
+ if ((AppDiscordPresence?.IsRpcEnabled ?? false) && !sameRegion)
+ AppDiscordPresence.SetupPresence();
#endif
InvokeLoadingRegionPopup(false);
LauncherFrame.BackStack.Clear();
diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/SettingsPage.xaml.cs b/CollapseLauncher/XAMLs/MainApp/Pages/SettingsPage.xaml.cs
index 098c87d9b..afab85887 100644
--- a/CollapseLauncher/XAMLs/MainApp/Pages/SettingsPage.xaml.cs
+++ b/CollapseLauncher/XAMLs/MainApp/Pages/SettingsPage.xaml.cs
@@ -769,9 +769,9 @@ private bool IsDiscordRpcEnabled
{
get
{
- bool isEnabled = GetAppConfigValue("EnableDiscordRPC");
- ToggleDiscordGameStatus.IsEnabled = IsEnabled;
- if (isEnabled)
+ var e = AppDiscordPresence.IsRpcEnabled;
+ ToggleDiscordGameStatus.IsEnabled = e;
+ if (e)
{
ToggleDiscordGameStatus.Visibility = Visibility.Visible;
ToggleDiscordIdleStatus.Visibility = Visibility.Visible;
@@ -781,23 +781,22 @@ private bool IsDiscordRpcEnabled
ToggleDiscordGameStatus.Visibility = Visibility.Collapsed;
ToggleDiscordIdleStatus.Visibility = Visibility.Collapsed;
}
- return isEnabled;
+ return e;
}
set
{
if (value)
{
- AppDiscordPresence.SetupPresence();
ToggleDiscordGameStatus.Visibility = Visibility.Visible;
ToggleDiscordIdleStatus.Visibility = Visibility.Visible;
}
else
{
- AppDiscordPresence.DisablePresence();
ToggleDiscordGameStatus.Visibility = Visibility.Collapsed;
ToggleDiscordIdleStatus.Visibility = Visibility.Collapsed;
}
- SetAndSaveConfigValue("EnableDiscordRPC", value);
+
+ AppDiscordPresence.IsRpcEnabled = value;
ToggleDiscordGameStatus.IsEnabled = value;
}
}
From 47086d063c6d886f83a668188b3241a59192eaa2 Mon Sep 17 00:00:00 2001
From: Bagus Nur Listiyono
Date: Tue, 30 Dec 2025 01:26:10 +0700
Subject: [PATCH 15/47] [DiscordRPC] Disable regional QS toggle if global
toggle is off
Signed-off-by: Bagus Nur Listiyono
---
CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml | 1 +
CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml.cs | 2 ++
2 files changed, 3 insertions(+)
diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml
index e31018779..0c71d5fe9 100644
--- a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml
+++ b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml
@@ -2219,6 +2219,7 @@
Style="{ThemeResource BodyStrongTextBlockStyle}"
Text="{x:Bind helper:Locale.Lang._HomePage.GameSettings_Panel3RegionRpc}" />
diff --git a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml.cs b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml.cs
index 8fbba9fdb..8b1adeca0 100644
--- a/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml.cs
+++ b/CollapseLauncher/XAMLs/MainApp/Pages/HomePage.xaml.cs
@@ -79,6 +79,8 @@ public sealed partial class HomePage
private int barWidth;
private int consoleWidth;
+ private readonly bool IsRpcEnabled_QS = AppDiscordPresence?.IsRpcEnabled ?? false;
+
public static int RefreshRateDefault => 500;
public static int RefreshRateSlow => 1000;
From 6ece0a3094c0545de9d266406d8044105f3b3c34 Mon Sep 17 00:00:00 2001
From: Bagus Nur Listiyono <28079733+bagusnl@users.noreply.github.com>
Date: Tue, 30 Dec 2025 22:48:37 +0700
Subject: [PATCH 16/47] [skip ci][CI] Fix sentry release version format
Should use this format
`CollapseLauncher@+`
---
.github/workflows/release-signed.yml | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/release-signed.yml b/.github/workflows/release-signed.yml
index b841201ff..e13622120 100644
--- a/.github/workflows/release-signed.yml
+++ b/.github/workflows/release-signed.yml
@@ -210,10 +210,9 @@ jobs:
run: |
$exePath = "${{ runner.temp }}\SignedArtifact\BuildArtifact-${{ env.VERSION }}\CollapseLauncher.exe"
if (Test-Path $exePath) {
- $version = ((Get-Item $exePath).VersionInfo.FileVersion)
- if ($version.EndsWith(".0")) {
- $version = $version.Substring(0, $version.Length - 2)
- }
+ $prefix = "CollapseLauncher@"
+ $version = ((Get-Item $exePath).VersionInfo.ProductVersion)
+ $version = $prefix + $version
if ($version) {
sentry-cli releases new --org collapse --project collapse-launcher $version
From 6f45dbe3265fe3152796a1cb5973e3c72180cdfe Mon Sep 17 00:00:00 2001
From: Kemal Setya Adhi
Date: Wed, 31 Dec 2025 02:28:04 +0700
Subject: [PATCH 17/47] Implements parsers for SR metadata
---
.../SophonRepairFetchUtility.cs | 1 -
.../RepairManagement/StarRail/Fetch.cs | 2 +-
.../StarRail/StarRailPersistentRefResult.cs | 252 ++++++++++++++++--
.../Assets/StarRailAssetBlockMetadata.cs | 154 +++++++++++
.../Assets/StarRailAssetBundleMetadata.cs | 113 ++++++++
.../Assets/StarRailAssetGenericFileInfo.cs | 33 +++
.../Assets/StarRailAssetJsonMetadata.cs | 77 ++++++
.../Assets/StarRailAssetNativeDataMetadata.cs | 185 +++++++++++++
.../StarRailAssetSignaturelessMetadata.cs | 148 ++++++++++
.../Struct/Assets/StarRailBinaryDataNative.cs | 170 ------------
.../Struct/StarRailAssetBinaryMetadata.cs | 33 +++
.../Struct/StarRailAssetMetadataIndex.cs | 173 ++++++++++++
.../StarRail/Struct/StarRailBinaryData.cs | 72 ++++-
.../Struct/StarRailBinaryDataExtension.cs | 126 +++++++--
.../Struct/StarRailBinaryDataWritable.cs | 54 ++++
Hi3Helper.EncTool | 2 +-
Hi3Helper.Sophon | 2 +-
17 files changed, 1375 insertions(+), 222 deletions(-)
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetBlockMetadata.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetBundleMetadata.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetGenericFileInfo.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetJsonMetadata.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetNativeDataMetadata.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetSignaturelessMetadata.cs
delete mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailBinaryDataNative.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailAssetBinaryMetadata.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailAssetMetadataIndex.cs
create mode 100644 CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailBinaryDataWritable.cs
diff --git a/CollapseLauncher/Classes/RepairManagement/SophonRepairFetchUtility.cs b/CollapseLauncher/Classes/RepairManagement/SophonRepairFetchUtility.cs
index ffc75af88..0c241e92f 100644
--- a/CollapseLauncher/Classes/RepairManagement/SophonRepairFetchUtility.cs
+++ b/CollapseLauncher/Classes/RepairManagement/SophonRepairFetchUtility.cs
@@ -68,7 +68,6 @@ public static async Task FetchAssetsFromSophonAsync(
.ContainsAny(excludedMatchingFields) && !x.MatchingField.Equals(matchingField))
.Select(x => infoPair.GetOtherManifestInfoPair(x.MatchingField)) ?? []);
-
foreach (SophonChunkManifestInfoPair pair in infoPairs)
{
instance.Status.ActivityStatus = string.Format(Locale.Lang._CachesPage.CachesStatusFetchingType, $"Sophon ({pair.MatchingField})");
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs
index ac32206fb..8fc558b9d 100644
--- a/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs
@@ -119,7 +119,7 @@ private async Task Fetch(List assetIndex, CancellationToke
GameVersionManager.GetGameVersionApi().ToString());
await dispatcherInfo.Initialize(client, regionId, token);
- Task persistentRef = StarRailPersistentRefResult
+ StarRailPersistentRefResult persistentRef = await StarRailPersistentRefResult
.GetReferenceAsync(this, dispatcherInfo, client, GameDataPersistentPath, token);
}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailPersistentRefResult.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailPersistentRefResult.cs
index 801431c03..91e903dac 100644
--- a/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailPersistentRefResult.cs
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/StarRailPersistentRefResult.cs
@@ -1,22 +1,21 @@
using CollapseLauncher.Helper.JsonConverter;
using CollapseLauncher.Helper.StreamUtility;
+using CollapseLauncher.RepairManagement.StarRail.Struct;
using CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
using Hi3Helper;
using Hi3Helper.Data;
using Hi3Helper.EncTool;
using Hi3Helper.EncTool.Parser.AssetMetadata;
using Hi3Helper.EncTool.Proto.StarRail;
-using Microsoft.UI.Xaml.Shapes;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
-using System.Text;
-using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
-using Path = System.IO.Path;
+
+// ReSharper disable CommentTypo
#nullable enable
#pragma warning disable IDE0130
@@ -34,31 +33,201 @@ public static async Task GetReferenceAsync(
StarRailGatewayStatic gateway = dispatcherInfo.RegionGateway;
Dictionary gatewayKvp = gateway.ValuePairs;
- string mainUrlAsb = gatewayKvp["AssetBundleVersionUpdateUrl"];
- string mainUrlDesignData = gatewayKvp["DesignDataBundleVersionUpdateUrl"];
+ string mainUrlAsb = gatewayKvp["AssetBundleVersionUpdateUrl"].CombineURLFromString("client/Windows");
+ string mainUrlDesignData = gatewayKvp["DesignDataBundleVersionUpdateUrl"].CombineURLFromString("client/Windows");
+ string mainUrlArchive = mainUrlAsb.CombineURLFromString("Archive");
+ string mainUrlAudio = mainUrlAsb.CombineURLFromString("AudioBlock");
+ string mainUrlAsbBlock = mainUrlAsb.CombineURLFromString("Block");
+ string mainUrlNativeData = mainUrlDesignData.CombineURLFromString("NativeData");
+ string mainUrlVideo = mainUrlAsb.CombineURLFromString("Video");
+
+ string lDirArchive = Path.Combine(persistentDir, @"Archive\Windows");
+ string lDirAsbBlock = Path.Combine(persistentDir, @"Asb\Windows");
+ string lDirAudio = Path.Combine(persistentDir, @"Audio\AudioPackage\Windows");
+ string lDirDesignData = Path.Combine(persistentDir, @"DesignData\Windows");
+ string lDirNativeData = Path.Combine(persistentDir, @"NativeData\Windows");
+ string lDirVideo = Path.Combine(persistentDir, @"Video\Windows");
+
+ string refDesignArchiveUrl = mainUrlDesignData.CombineURLFromString("M_Design_ArchiveV.bytes");
+ string refArchiveUrl = mainUrlArchive.CombineURLFromString("M_ArchiveV.bytes");
- string mInfoDesignArchiveUrl = mainUrlDesignData.CombineURLFromString("client/Windows/M_Design_ArchiveV.bytes");
- Dictionary mInfoDesignArchive = await StarRailRefMainInfo
+ // -- Fetch and parse the index references
+ Dictionary handleDesignArchive = await StarRailRefMainInfo
.ParseListFromUrlAsync(instance,
client,
- mInfoDesignArchiveUrl,
+ refDesignArchiveUrl,
+ null,
token);
- string mInfoArchiveUrl = mainUrlAsb.CombineURLFromString("client/Windows/Archive/M_ArchiveV.bytes");
- Dictionary mInfoArchive = await StarRailRefMainInfo
+ Dictionary handleArchive = await StarRailRefMainInfo
.ParseListFromUrlAsync(instance,
client,
- mInfoArchiveUrl,
+ refArchiveUrl,
+ lDirArchive,
token);
- await using FileStream stream =
- File.OpenRead(@"C:\Users\neon-nyan\AppData\LocalLow\CollapseLauncher\GameFolder\SRGlb\Games\StarRail_Data\StreamingAssets\NativeData\Windows\NativeDataV_2ebcf9e27323a0561ef4825a14819ed5.bytes");
+ // -- Save local index files
+ // Notes to Dev: HoYo no longer provides a proper raw bytes data anymore and the client creates it based
+ // on data provided by "handleArchive", so we need to emulate how the game generates these data.
+ await SaveLocalIndexFiles(instance, handleDesignArchive, lDirDesignData, "DesignV", token);
+ await SaveLocalIndexFiles(instance, handleArchive, lDirAsbBlock, "AsbV", token);
+ await SaveLocalIndexFiles(instance, handleArchive, lDirAsbBlock, "BlockV", token);
+ await SaveLocalIndexFiles(instance, handleArchive, lDirAsbBlock, "Start_AsbV", token);
+ await SaveLocalIndexFiles(instance, handleArchive, lDirAsbBlock, "Start_BlockV", token);
+ await SaveLocalIndexFiles(instance, handleArchive, lDirAudio, "AudioV", token);
+ await SaveLocalIndexFiles(instance, handleArchive, lDirVideo, "VideoV", token);
+
+ // -- Load metadata files
+ // -- DesignV
+ StarRailAssetSignaturelessMetadata? metadataDesignV =
+ await LoadMetadataFile(instance,
+ handleDesignArchive,
+ client,
+ mainUrlDesignData,
+ "DesignV",
+ lDirDesignData,
+ token);
+
+ // -- NativeDataV
+ StarRailAssetNativeDataMetadata? metadataNativeDataV =
+ await LoadMetadataFile(instance,
+ handleDesignArchive,
+ client,
+ mainUrlNativeData,
+ "NativeDataV",
+ lDirNativeData,
+ token);
+
+ // -- Start_AsbV
+ StarRailAssetBundleMetadata? metadataStartAsbV =
+ await LoadMetadataFile(instance,
+ handleArchive,
+ client,
+ mainUrlAsbBlock,
+ "Start_AsbV",
+ lDirAsbBlock,
+ token);
- StarRailBinaryDataNative binaryRefNativeData = new();
- await binaryRefNativeData.ParseAsync(stream, token);
+ // -- Start_BlockV
+ StarRailAssetBlockMetadata? metadataStartBlockV =
+ await LoadMetadataFile(instance,
+ handleArchive,
+ client,
+ mainUrlAsbBlock,
+ "Start_BlockV",
+ lDirAsbBlock,
+ token);
+
+ // -- AsbV
+ StarRailAssetBundleMetadata? metadataAsbV =
+ await LoadMetadataFile(instance,
+ handleArchive,
+ client,
+ mainUrlAsbBlock,
+ "AsbV",
+ null,
+ token);
+
+ // -- BlockV
+ StarRailAssetBlockMetadata? metadataBlockV =
+ await LoadMetadataFile(instance,
+ handleArchive,
+ client,
+ mainUrlAsbBlock,
+ "BlockV",
+ null,
+ token);
+
+ // -- AudioV
+ StarRailAssetJsonMetadata? metadataAudioV =
+ await LoadMetadataFile(instance,
+ handleArchive,
+ client,
+ mainUrlAudio,
+ "AudioV",
+ lDirAudio,
+ token);
+
+ // -- VideoV
+ StarRailAssetJsonMetadata? metadataVideoV =
+ await LoadMetadataFile(instance,
+ handleArchive,
+ client,
+ mainUrlVideo,
+ "VideoV",
+ lDirVideo,
+ token);
return default;
}
+
+ private static async ValueTask SaveLocalIndexFiles(
+ StarRailRepair instance,
+ Dictionary handleArchiveSource,
+ string outputDir,
+ string indexKey,
+ CancellationToken token)
+ {
+ if (!handleArchiveSource.TryGetValue(indexKey, out StarRailRefMainInfo? index))
+ {
+ Logger.LogWriteLine($"Game server doesn't serve index file: {indexKey}. Please contact our developer to get this fixed!", LogType.Warning, true);
+ return;
+ }
+
+ // Set total activity string as "Fetching Caches Type: "
+ instance.Status.ActivityStatus = string.Format(Locale.Lang._CachesPage.CachesStatusFetchingType, $"Game Index: {index.FileName}");
+ instance.Status.IsProgressAllIndetermined = true;
+ instance.Status.IsIncludePerFileIndicator = false;
+ instance.UpdateStatus();
+
+ StarRailAssetMetadataIndex indexMetadata = index;
+ string filePath = Path.Combine(outputDir, index.FileName + ".bytes");
+ await indexMetadata.WriteAsync(filePath, token);
+ }
+
+ private static async ValueTask LoadMetadataFile(
+ StarRailRepair instance,
+ Dictionary handleArchiveSource,
+ HttpClient client,
+ string baseUrl,
+ string indexKey,
+ string? saveToLocalDir = null,
+ CancellationToken token = default)
+ where T : StarRailBinaryData, new()
+ {
+ T parser = StarRailBinaryData.CreateDefault();
+
+ if (!handleArchiveSource.TryGetValue(indexKey, out StarRailRefMainInfo? index))
+ {
+ Logger.LogWriteLine($"Game server doesn't serve index file: {indexKey}. Please contact our developer to get this fixed!", LogType.Warning, true);
+ return null;
+ }
+
+ // Set total activity string as "Fetching Caches Type: "
+ instance.Status.ActivityStatus = string.Format(Locale.Lang._CachesPage.CachesStatusFetchingType, $"Game Metadata: {index.FileName}");
+ instance.Status.IsProgressAllIndetermined = true;
+ instance.Status.IsIncludePerFileIndicator = false;
+ instance.UpdateStatus();
+
+ string filename = index.RemoteFileName;
+ string fileUrl = baseUrl.CombineURLFromString(filename);
+ await using Stream networkStream = (await client.TryGetCachedStreamFrom(fileUrl, token: token)).Stream;
+ await using Stream sourceStream = !string.IsNullOrEmpty(saveToLocalDir)
+ ? CreateLocalStream(networkStream, Path.Combine(saveToLocalDir, filename))
+ : networkStream;
+
+ await parser.ParseAsync(sourceStream, true, token);
+ return parser;
+
+ static Stream CreateLocalStream(Stream thisSourceStream, string filePath)
+ {
+ FileInfo fileInfo = new FileInfo(filePath)
+ .EnsureCreationOfDirectory()
+ .EnsureNoReadOnly()
+ .StripAlternateDataStream();
+ return new CopyToStream(thisSourceStream, fileInfo.Create(), null, true);
+ }
+ }
}
[JsonSerializable(typeof(StarRailRefMainInfo))]
@@ -88,11 +257,46 @@ internal class StarRailRefMainInfo
public override string ToString() => RemoteFileName;
+ ///
+ /// Converts this instance to .
+ /// This is necessary as SRMI (Star Rail Metadata Index) file is now being generated on Client-side, so
+ /// we wanted to save the files locally to save the information about the reference file.
+ ///
+ /// An instance of .
+ public StarRailAssetMetadataIndex ToMetadataIndex()
+ {
+ StarRailAssetMetadataIndex metadataIndex =
+ StarRailBinaryData.CreateDefault();
+
+ StarRailAssetMetadataIndex.MetadataIndex indexData = new()
+ {
+ MajorVersion = MajorVersion,
+ MinorVersion = MinorVersion,
+ PatchVersion = PatchVersion,
+ MD5Checksum = HexTool.HexToBytesUnsafe(ContentHash),
+ MetadataIndexFileSize = (int)FileSize,
+ PrevPatch = 0, // Leave PrevPatch to be 0
+ Timestamp = TimeStamp
+ };
+
+ metadataIndex.DataList.Add(indexData);
+ return metadataIndex;
+ }
+
+ ///
+ /// Converts this instance to .
+ /// This is necessary as SRMI (Star Rail Metadata Index) file is now being generated on Client-side, so
+ /// we wanted to save the files locally to save the information about the reference file.
+ ///
+ /// An instance of .
+ public static implicit operator StarRailAssetMetadataIndex(StarRailRefMainInfo instance) => instance.ToMetadataIndex();
+
public static async Task> ParseListFromUrlAsync(
StarRailRepair instance,
HttpClient client,
string url,
- CancellationToken token)
+ string? saveToLocalDir = null,
+ CancellationToken token = default)
{
// Set total activity string as "Fetching Caches Type: "
instance.Status.ActivityStatus = string.Format(Locale.Lang._CachesPage.CachesStatusFetchingType, $"Game Ref: {Path.GetFileNameWithoutExtension(url)}");
@@ -101,9 +305,12 @@ public static async Task> ParseListFromU
instance.UpdateStatus();
await using Stream networkStream = (await client.TryGetCachedStreamFrom(url, token: token)).Stream;
+ await using Stream sourceStream = !string.IsNullOrEmpty(saveToLocalDir)
+ ? CreateLocalStream(networkStream, Path.Combine(saveToLocalDir, Path.GetFileName(url)))
+ : networkStream;
Dictionary returnList = [];
- using StreamReader reader = new(networkStream);
+ using StreamReader reader = new(sourceStream);
while (await reader.ReadLineAsync(token) is { } line)
{
StarRailRefMainInfo refInfo = line.Deserialize(StarRailRepairJsonContext.Default.StarRailRefMainInfo)
@@ -113,5 +320,14 @@ public static async Task> ParseListFromU
}
return returnList;
+
+ static Stream CreateLocalStream(Stream thisSourceStream, string filePath)
+ {
+ FileInfo fileInfo = new FileInfo(filePath)
+ .EnsureCreationOfDirectory()
+ .EnsureNoReadOnly()
+ .StripAlternateDataStream();
+ return new CopyToStream(thisSourceStream, fileInfo.Create(), null, true);
+ }
}
}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetBlockMetadata.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetBlockMetadata.cs
new file mode 100644
index 000000000..5e79b4725
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetBlockMetadata.cs
@@ -0,0 +1,154 @@
+using Hi3Helper.Data;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+#pragma warning disable IDE0290 // Shut the fuck up
+#pragma warning disable IDE0130
+#nullable enable
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
+
+public sealed class StarRailAssetBlockMetadata : StarRailAssetBinaryMetadata
+{
+ public StarRailAssetBlockMetadata()
+ : base(256,
+ 768,
+ 16,
+ 2,
+ 12)
+ { }
+
+ protected override async ValueTask ReadDataCoreAsync(
+ long currentOffset,
+ Stream dataStream,
+ CancellationToken token = default)
+ {
+ (DataSectionHeaderStruct dataHeader, int readHeader) =
+ await dataStream
+ .ReadDataAssertAndSeekAsync(_ => Header.SubStructSize,
+ token)
+ .ConfigureAwait(false);
+ currentOffset += readHeader;
+
+ // ASSERT: Make sure the current data stream offset is at the exact data buffer offset.
+ Debug.Assert(dataHeader.DataStartOffset == currentOffset);
+
+ int dataBufferLen = dataHeader.DataSize * dataHeader.DataCount;
+ byte[] dataBuffer = ArrayPool.Shared.Rent(dataBufferLen);
+ try
+ {
+ // -- Read data buffer
+ currentOffset += await dataStream.ReadBufferAssertAsync(currentOffset,
+ dataBuffer.AsMemory(0, dataBufferLen),
+ token);
+
+ // -- Create span
+ ReadOnlySpan dataBufferSpan = dataBuffer.AsSpan(0, dataBufferLen);
+
+ // -- Allocate list
+ DataList = new List(dataHeader.DataCount);
+ while (!dataBufferSpan.IsEmpty)
+ {
+ dataBufferSpan = Metadata.Parse(dataBufferSpan,
+ dataHeader.DataSize,
+ out Metadata result);
+ DataList.Add(result);
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(dataBuffer);
+ }
+
+ return currentOffset;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ internal struct DataSectionHeaderStruct
+ {
+ ///
+ /// The absolute offset of data array in the data stream.
+ ///
+ public int DataStartOffset;
+
+ ///
+ /// The count of data array in the data stream.
+ ///
+ public int DataCount;
+
+ ///
+ /// The size of the one data inside the array.
+ ///
+ public int DataSize;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ internal unsafe struct MetadataStruct
+ {
+ ///
+ /// 4x4 bytes Reordered hash (each 4 bytes reordered as Big-endian).
+ ///
+ private fixed byte _shifted4BytesMD5Checksum[16];
+
+ ///
+ /// Defined flags of the asset bundle block file.
+ ///
+ public uint Flags;
+
+ ///
+ /// The size of the block file.
+ ///
+ public int FileSize;
+
+ ///
+ /// 4x4 bytes Reordered hash (each 4 bytes reordered as Big-endian).
+ ///
+ public ReadOnlySpan Shifted4BytesMD5Checksum
+ {
+ get
+ {
+ fixed (byte* magicP = _shifted4BytesMD5Checksum)
+ {
+ return new ReadOnlySpan(magicP, 16);
+ }
+ }
+ }
+ }
+
+ public class Metadata : StarRailAssetGenericFileInfo
+ {
+ ///
+ /// Defined flags of the asset bundle block file.
+ ///
+ public required uint Flags { get; init; }
+
+ public static ReadOnlySpan Parse(ReadOnlySpan buffer,
+ int sizeOfStruct,
+ out Metadata result)
+ {
+ ref readonly MetadataStruct structRef = ref MemoryMarshal.AsRef(buffer);
+ byte[] md5Buffer = new byte[16];
+
+ structRef.Shifted4BytesMD5Checksum.CopyTo(md5Buffer);
+ StarRailBinaryDataExtension.ReverseReorderBy4X4HashData(md5Buffer);
+
+ result = new Metadata
+ {
+ Filename = $"{HexTool.BytesToHexUnsafe(md5Buffer)}.block",
+ FileSize = structRef.FileSize,
+ Flags = structRef.Flags,
+ MD5Checksum = md5Buffer
+ };
+
+ return buffer[sizeOfStruct..];
+ }
+
+ public override string ToString() =>
+ $"{Filename} | Flags: {ConverterTool.ToBinaryString(Flags)} | Hash: {HexTool.BytesToHexUnsafe(MD5Checksum)} | Size: {FileSize}";
+ }
+}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetBundleMetadata.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetBundleMetadata.cs
new file mode 100644
index 000000000..8bcc0856f
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetBundleMetadata.cs
@@ -0,0 +1,113 @@
+using Hi3Helper.Data;
+using Hi3Helper.EncTool;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+#pragma warning disable IDE0290
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
+
+public class StarRailAssetBundleMetadata : StarRailAssetBinaryMetadata
+{
+ public StarRailAssetBundleMetadata()
+ : base(1280,
+ 256,
+ 40,
+ 8,
+ 16) { }
+
+ private static ReadOnlySpan MagicSignatureStatic => "SRAM"u8;
+ protected override ReadOnlySpan MagicSignature => MagicSignatureStatic;
+
+ protected override async ValueTask<(StarRailBinaryDataHeaderStruct Header, int Offset)>
+ ReadHeaderCoreAsync(
+ Stream dataStream,
+ CancellationToken token = default)
+ {
+ return await dataStream
+ .ReadDataAssertWithPosAndSeekAsync // Use ReadDataAssertWithPosAndSeekAsync for SRAM
+ (0,
+ x => x.HeaderOrDataLength,
+ token);
+ }
+
+ protected override async ValueTask ReadDataCoreAsync(
+ long currentOffset,
+ Stream dataStream,
+ CancellationToken token = default)
+ {
+ // -- Read the first data section header.
+ (DataSectionHeaderStruct headerStructs, int read) =
+ await dataStream.ReadDataAssertAndSeekAsync
+ (_ => Header.SubStructSize,
+ token);
+ currentOffset += read;
+
+ // -- Skip other data section header.
+ int remainedBufferSize = Header.SubStructSize * (Header.SubStructCount - 1);
+ currentOffset += await dataStream.SeekForwardAsync(remainedBufferSize, token);
+
+ // ASSERT: Make sure we are at data start offset before reading.
+ Debug.Assert(currentOffset == headerStructs.DataStartOffset);
+
+ // -- Read data buffer.
+ int dataBufferLen = headerStructs.DataCount * headerStructs.DataSize;
+ byte[] dataBuffer = ArrayPool.Shared.Rent(dataBufferLen);
+ try
+ {
+ // -- Read data buffer
+ currentOffset += await dataStream.ReadBufferAssertAsync(currentOffset,
+ dataBuffer.AsMemory(0, dataBufferLen),
+ token);
+
+ // -- Create span
+ ReadOnlySpan dataBufferSpan = dataBuffer.AsSpan(0, dataBufferLen);
+
+ // -- Allocate list
+ DataList = new List(headerStructs.DataCount);
+ while (!dataBufferSpan.IsEmpty)
+ {
+ dataBufferSpan = StarRailAssetBlockMetadata
+ .Metadata
+ .Parse(dataBufferSpan,
+ headerStructs.DataSize,
+ out StarRailAssetBlockMetadata.Metadata result);
+ DataList.Add(result);
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(dataBuffer);
+ }
+
+ return currentOffset;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ private struct DataSectionHeaderStruct
+ {
+ public int Reserved;
+
+ ///
+ /// The absolute offset of data array in the data stream.
+ ///
+ public int DataStartOffset;
+
+ ///
+ /// The count of data array in the data stream.
+ ///
+ public int DataCount;
+
+ ///
+ /// The size of the one data inside the array.
+ ///
+ public int DataSize;
+ }
+}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetGenericFileInfo.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetGenericFileInfo.cs
new file mode 100644
index 000000000..f65978e97
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetGenericFileInfo.cs
@@ -0,0 +1,33 @@
+using Hi3Helper.Data;
+using Hi3Helper.Plugin.Core.Utility.Json.Converters;
+using System.Text.Json.Serialization;
+
+#pragma warning disable IDE0130
+#nullable enable
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
+
+public class StarRailAssetGenericFileInfo
+{
+ ///
+ /// The filename of the asset.
+ ///
+ [JsonPropertyName("Path")]
+ public virtual required string? Filename { get; init; }
+
+ ///
+ /// Size of the file in bytes.
+ ///
+ [JsonPropertyName("Size")]
+ public required long FileSize { get; init; }
+
+ ///
+ /// The MD5 hash checksum of the file.
+ ///
+ [JsonPropertyName("Md5")]
+ [JsonConverter(typeof(HexStringToArrayJsonConverter))]
+ public required byte[] MD5Checksum { get; init; }
+
+ public override string ToString() =>
+ $"{Filename} | Hash: {HexTool.BytesToHexUnsafe(MD5Checksum)} | Size: {FileSize} bytes";
+}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetJsonMetadata.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetJsonMetadata.cs
new file mode 100644
index 000000000..222a02e88
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetJsonMetadata.cs
@@ -0,0 +1,77 @@
+using Hi3Helper.EncTool;
+using Hi3Helper.EncTool.Streams;
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+#pragma warning disable IDE0290
+#pragma warning disable IDE0130
+#nullable enable
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
+
+internal partial class StarRailAssetJsonMetadata : StarRailAssetBinaryMetadata
+{
+ public StarRailAssetJsonMetadata()
+ : base(0, // Leave the rest of it to 0 as this metadata has JSON struct
+ 0,
+ 0,
+ 0,
+ 0)
+ { }
+
+ protected override ReadOnlySpan MagicSignature => "\0\0\0\0"u8;
+
+ protected override ValueTask<(StarRailBinaryDataHeaderStruct Header, int Offset)>
+ ReadHeaderCoreAsync(Stream dataStream,
+ CancellationToken token = default)
+ {
+ return ValueTask.FromResult((default(StarRailBinaryDataHeaderStruct), 0));
+ }
+
+ protected override async ValueTask ReadDataCoreAsync(
+ long currentOffset,
+ Stream dataStream,
+ CancellationToken token = default)
+ {
+ // -- Allocate list
+ DataList = [];
+
+ // -- Read list
+ await using NullPositionTrackableStream trackingNullStream = new();
+ await using CopyToStream bridgeStream = new(dataStream, trackingNullStream, null, false);
+ using StreamReader reader = new(bridgeStream, leaveOpen: true);
+ while (await reader.ReadLineAsync(token) is { } line)
+ {
+ Metadata? metadata = JsonSerializer.Deserialize(line, MetadataJsonContext.Default.Metadata);
+ if (metadata == null)
+ {
+ continue;
+ }
+
+ DataList.Add(metadata);
+ }
+
+ return trackingNullStream.Position;
+ }
+
+ [JsonSerializable(typeof(Metadata))]
+ public partial class MetadataJsonContext : JsonSerializerContext;
+
+ public class Metadata : StarRailAssetGenericFileInfo
+ {
+ [JsonPropertyName("Patch")]
+ public bool IsPatch { get; init; }
+
+ [JsonPropertyName("SubPackId")]
+ public int SubPackId { get; init; }
+
+ [JsonPropertyName("TaskIds")]
+ public int[]? TaskIdList { get; init; }
+
+ public override string ToString() => $"{base.ToString()} | Patch: {IsPatch} | SubPackId: {SubPackId}" +
+ (TaskIdList?.Length == 0 ? "" : $" | TaskIds: [{string.Join(", ", TaskIdList ?? [])}]");
+ }
+}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetNativeDataMetadata.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetNativeDataMetadata.cs
new file mode 100644
index 000000000..3aab78b0a
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetNativeDataMetadata.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+#pragma warning disable IDE0290 // Shut the fuck up
+#pragma warning disable IDE0130
+#nullable enable
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
+
+internal sealed class StarRailAssetNativeDataMetadata : StarRailAssetBinaryMetadata
+{
+ public StarRailAssetNativeDataMetadata()
+ : base(256,
+ 768,
+ 16,
+ 2,
+ 16)
+ { }
+
+ protected override async ValueTask ReadDataCoreAsync(
+ long currentOffset,
+ Stream dataStream,
+ CancellationToken token = default)
+ {
+ (DataSectionHeaderStruct fileInfoHeaderStruct, int readHeader) =
+ await dataStream
+ .ReadDataAssertAndSeekAsync(_ => Header.SubStructSize,
+ token)
+ .ConfigureAwait(false);
+ currentOffset += readHeader;
+
+ (DataSectionHeaderStruct filenameHeaderStruct, readHeader) =
+ await dataStream
+ .ReadDataAssertAndSeekAsync(_ => Header.SubStructSize,
+ token)
+ .ConfigureAwait(false);
+ currentOffset += readHeader;
+
+ // ASSERT: Make sure the current data stream offset is at the exact filename buffer offset.
+ Debug.Assert(filenameHeaderStruct.DataStartOffset == currentOffset);
+
+ int filenameBufferLen = filenameHeaderStruct.DataSize * filenameHeaderStruct.DataCount;
+ int fileInfoBufferLen = fileInfoHeaderStruct.DataSize * fileInfoHeaderStruct.DataCount;
+ byte[] filenameBuffer = ArrayPool.Shared.Rent(filenameBufferLen);
+ byte[] fileInfoBuffer = ArrayPool.Shared.Rent(fileInfoBufferLen);
+ try
+ {
+ // -- Read filename buffer
+ currentOffset += await dataStream.ReadBufferAssertAsync(currentOffset,
+ filenameBuffer.AsMemory(0, filenameBufferLen),
+ token);
+
+ // -- Read file info buffer
+ currentOffset += await dataStream.ReadBufferAssertAsync(currentOffset,
+ fileInfoBuffer.AsMemory(0, fileInfoBufferLen),
+ token);
+
+ // -- Create spans
+ Span filenameBufferSpan = filenameBuffer.AsSpan(0, filenameBufferLen);
+ Span fileInfoBufferSpan = fileInfoBuffer.AsSpan(0, fileInfoBufferLen);
+
+ // -- Allocate list
+ DataList = new List(fileInfoHeaderStruct.DataCount);
+
+ for (int i = 0; i < fileInfoHeaderStruct.DataCount && !filenameBufferSpan.IsEmpty; i++)
+ {
+ ref FileInfoStruct fileInfo = ref MemoryMarshal.AsRef(fileInfoBufferSpan);
+ filenameBufferSpan = Metadata.Parse(filenameBufferSpan,
+ ref fileInfo,
+ out Metadata? asset);
+ fileInfoBufferSpan = fileInfoBufferSpan[fileInfoHeaderStruct.DataSize..];
+
+ if (asset == null)
+ {
+ throw new IndexOutOfRangeException("Failed to parse NativeData assets as the buffer data might be insufficient or out-of-bounds!");
+ }
+ DataList.Add(asset);
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(filenameBuffer);
+ ArrayPool.Shared.Return(fileInfoBuffer);
+ }
+
+ return currentOffset;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ private struct DataSectionHeaderStruct
+ {
+ public int Reserved;
+
+ ///
+ /// The absolute offset of data array in the data stream.
+ ///
+ public int DataStartOffset;
+
+ ///
+ /// The count of data array in the data stream.
+ ///
+ public int DataCount;
+
+ ///
+ /// The size of the one data inside the array.
+ ///
+ public int DataSize;
+ }
+
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ public unsafe struct FileInfoStruct
+ {
+ ///
+ /// 4x4 bytes Reordered hash (each 4 bytes reordered as Big-endian).
+ ///
+ private fixed byte _shifted4BytesMD5Checksum[16];
+
+ ///
+ /// The size of the file.
+ ///
+ public long FileSize;
+
+ ///
+ /// The filename length on the filename buffer.
+ ///
+ public int FilenameLength;
+
+ ///
+ /// The start offset of the filename inside the filename buffer.
+ ///
+ public int FilenameStartAt;
+
+ ///
+ /// 4x4 bytes Reordered hash (each 4 bytes reordered as Big-endian).
+ ///
+ public ReadOnlySpan Shifted4BytesMD5Checksum
+ {
+ get
+ {
+ fixed (byte* magicP = _shifted4BytesMD5Checksum)
+ {
+ return new ReadOnlySpan(magicP, 16);
+ }
+ }
+ }
+ }
+
+ public class Metadata : StarRailAssetGenericFileInfo
+ {
+ public static Span Parse(Span filenameBuffer,
+ ref FileInfoStruct assetInfo,
+ out Metadata? result)
+ {
+ Unsafe.SkipInit(out result);
+ int filenameLength = assetInfo.FilenameLength;
+ if (filenameBuffer.Length < filenameLength)
+ {
+ return filenameBuffer;
+ }
+
+ string filename = Encoding.UTF8.GetString(filenameBuffer[..filenameLength]);
+ long fileSize = assetInfo.FileSize;
+ byte[] md5Checksum = new byte[16];
+
+ assetInfo.Shifted4BytesMD5Checksum.CopyTo(md5Checksum);
+ StarRailBinaryDataExtension.ReverseReorderBy4X4HashData(md5Checksum); // Reorder 4x4 hash using SIMD
+
+ result = new Metadata
+ {
+ Filename = filename,
+ FileSize = fileSize,
+ MD5Checksum = md5Checksum
+ };
+
+ return filenameBuffer[filenameLength..];
+ }
+ }
+}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetSignaturelessMetadata.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetSignaturelessMetadata.cs
new file mode 100644
index 000000000..25ffad544
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailAssetSignaturelessMetadata.cs
@@ -0,0 +1,148 @@
+using Hi3Helper.Data;
+using Hi3Helper.EncTool;
+using System;
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+#pragma warning disable IDE0290
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
+
+internal class StarRailAssetSignaturelessMetadata : StarRailAssetBinaryMetadata
+{
+ public StarRailAssetSignaturelessMetadata()
+ : base(0,
+ 256,
+ 0, // Leave the rest of it to 0 as this metadata has non-consistent header struct
+ 0,
+ 0) { }
+
+ protected override ReadOnlySpan MagicSignature => [0x00, 0x00, 0x00, 0xFF];
+
+ protected override async ValueTask<(StarRailBinaryDataHeaderStruct Header, int Offset)>
+ ReadHeaderCoreAsync(
+ Stream dataStream,
+ CancellationToken token = default)
+ {
+ // We manipulate the Header to only read its first 8 bytes so
+ // the signature assertion can be bypassed as the other data
+ // is needed to be read by ReadDataCoreAsync, we don't want to
+ // read the whole 16 bytes of it.
+ byte[] first8BytesBuffer = new byte[8];
+ StarRailBinaryDataHeaderStruct header = default;
+
+ _ = await dataStream.ReadAtLeastAsync(first8BytesBuffer,
+ first8BytesBuffer.Length,
+ cancellationToken: token);
+
+ CopyHeaderUnsafe(ref header, first8BytesBuffer);
+
+ return (header, first8BytesBuffer.Length);
+
+ static unsafe void CopyHeaderUnsafe(scoped ref StarRailBinaryDataHeaderStruct target,
+ scoped Span dataBuffer)
+ => dataBuffer.CopyTo(new Span(Unsafe.AsPointer(in target), dataBuffer.Length));
+ }
+
+ protected override async ValueTask ReadDataCoreAsync(
+ long currentOffset,
+ Stream dataStream,
+ CancellationToken token = default)
+ {
+ // In this read data section, we will read the data manually since
+ // the entire data is Big-endian ordered.
+
+ // -- Read the count from the header
+ byte[] countBuffer = new byte[8];
+ currentOffset += await dataStream.ReadAtLeastAsync(countBuffer,
+ countBuffer.Length,
+ cancellationToken: token)
+ .ConfigureAwait(false);
+
+ Span countBufferSpan = countBuffer;
+ int parentDataCount = BinaryPrimitives.ReadInt32BigEndian(countBufferSpan);
+
+ // -- Declare constant length for each parent and children data
+ const int parentDataBufferLen = 32;
+ const int childrenDataBufferLen = 12;
+
+ // -- Read the rest of the data buffer and parse it
+ byte[] parentDataBuffer = ArrayPool.Shared.Rent(parentDataBufferLen);
+ Memory parentDataBufferMemory = parentDataBuffer.AsMemory(0, parentDataBufferLen);
+
+ // -- Allocate list
+ DataList = new List(parentDataCount);
+
+ try
+ {
+ for (int i = 0; i < parentDataCount; i++)
+ {
+ long lastPos = currentOffset;
+ // -- Parse data and add to list
+ currentOffset += await dataStream.ReadAtLeastAsync(parentDataBufferMemory,
+ parentDataBufferMemory.Length,
+ cancellationToken: token)
+ .ConfigureAwait(false);
+ Metadata.Parse(parentDataBuffer,
+ childrenDataBufferLen,
+ lastPos,
+ out int bytesToSkip,
+ out Metadata result);
+ DataList.Add(result);
+
+ // -- Skip children data
+ currentOffset += await dataStream.SeekForwardAsync(bytesToSkip, token)
+ .ConfigureAwait(false);
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(parentDataBuffer);
+ }
+
+ return currentOffset;
+ }
+
+ public class Metadata : StarRailAssetGenericFileInfo
+ {
+ ///
+ /// Defined flags of the asset bundle block file.
+ ///
+ public required uint Flags { get; init; }
+
+ public static void Parse(ReadOnlySpan buffer,
+ int subDataSize,
+ long lastDataStreamPos,
+ out int bytesToSkip,
+ out Metadata result)
+ {
+ // uint firstAssetId = BinaryPrimitives.ReadUInt32BigEndian(buffer);
+ uint assetType = BinaryPrimitives.ReadUInt32BigEndian(buffer[20..]);
+ long fileSize = BinaryPrimitives.ReadUInt32BigEndian(buffer[24..]);
+ int subDataCount = BinaryPrimitives.ReadInt32BigEndian(buffer[28..]);
+
+ byte[] md5Hash = new byte[16];
+ buffer[4..20].CopyTo(md5Hash);
+
+ // subDataCount = Number of sub-data struct count
+ // subDataSize = Number of sub-data struct size
+ // 1 = Unknown offset, seek +1
+ bytesToSkip = subDataCount * subDataSize + 1;
+ result = new Metadata
+ {
+ MD5Checksum = md5Hash,
+ Filename = $"{HexTool.BytesToHexUnsafe(md5Hash)}.block",
+ FileSize = fileSize,
+ Flags = assetType
+ };
+ }
+
+ public override string ToString() =>
+ $"{Filename} | Flags: {ConverterTool.ToBinaryString(Flags)} | Hash: {HexTool.BytesToHexUnsafe(MD5Checksum)} | Size: {FileSize}";
+ }
+}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailBinaryDataNative.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailBinaryDataNative.cs
deleted file mode 100644
index 6f96d2803..000000000
--- a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/Assets/StarRailBinaryDataNative.cs
+++ /dev/null
@@ -1,170 +0,0 @@
-using Hi3Helper.EncTool;
-using System;
-using System.Buffers;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-#pragma warning disable IDE0130
-#nullable enable
-
-namespace CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
-
-internal class StarRailBinaryDataNative : StarRailBinaryData
-{
- protected override ReadOnlySpan MagicSignature => "SRBM"u8;
-
- protected override async ValueTask ReadDataCoreAsync(long currentOffset, Stream dataStream, CancellationToken token)
- {
- (HeaderStruct header, int readHeader) = await dataStream
- .ReadDataAssertAndSeekAsync(x => x.AssetInfoStructSize, token)
- .ConfigureAwait(false);
- currentOffset += readHeader;
-
- Debug.Assert(header.FilenameBufferStartOffset == currentOffset); // ASSERT: Make sure the current data stream offset is at the exact filename buffer offset.
- Debug.Assert(header.AssetCount is < ushort.MaxValue and > 0); // ASSERT: Make sure the data isn't more than ushort.MaxValue and not empty.
-
- // Read file info
- int fileInfoBufferLen = header.AssetInfoStructSize * header.AssetCount;
- byte[] filenameBuffer = ArrayPool.Shared.Rent(header.FilenameBufferSize);
- byte[] fileInfoBuffer = ArrayPool.Shared.Rent(fileInfoBufferLen);
- try
- {
-
- // Read filename buffer
- int read = await dataStream.ReadAtLeastAsync(filenameBuffer.AsMemory(0, header.FilenameBufferSize),
- header.FilenameBufferSize,
- cancellationToken: token)
- .ConfigureAwait(false);
- Debug.Assert(header.FilenameBufferSize == read); // ASSERT: Make sure the filename buffer size is equal as what we read.
-
- currentOffset += read;
- int paddingOffsetToInfoBuffer = header.AssetInfoAbsoluteStartOffset - (int)currentOffset;
- await dataStream.SeekForwardAsync(paddingOffsetToInfoBuffer, token); // ASSERT: Seek forward to the asset info buffer.
-
- // Read file info buffer
- read = await dataStream.ReadAtLeastAsync(fileInfoBuffer.AsMemory(0, fileInfoBufferLen),
- fileInfoBufferLen,
- cancellationToken: token);
- Debug.Assert(fileInfoBufferLen == read); // ASSERT: Make sure the asset info struct size is equal as what we read.
- currentOffset += read;
-
- // Create spans
- Span filenameBufferSpan = filenameBuffer.AsSpan(0, header.FilenameBufferSize);
- Span fileInfoBufferSpan = fileInfoBuffer.AsSpan(0, fileInfoBufferLen);
-
- // Allocate list
- DataList = new List(header.AssetCount);
-
- for (int i = 0; i < header.AssetCount && !filenameBufferSpan.IsEmpty; i++)
- {
- ref StarRailAssetNative.StarRailAssetNativeInfo fileInfo =
- ref MemoryMarshal.AsRef(fileInfoBufferSpan);
- filenameBufferSpan = StarRailAssetNative.Parse(filenameBufferSpan,
- ref fileInfo,
- out StarRailAssetNative? asset);
- fileInfoBufferSpan = fileInfoBufferSpan[header.AssetInfoStructSize..];
-
- if (asset == null)
- {
- throw new IndexOutOfRangeException("Failed to parse NativeData assets as the buffer data might be insufficient or out-of-bounds!");
- }
- DataList.Add(asset);
- }
- }
- finally
- {
- ArrayPool.Shared.Return(filenameBuffer);
- ArrayPool.Shared.Return(fileInfoBuffer);
- }
-
- return currentOffset;
- }
-
- [StructLayout(LayoutKind.Sequential, Pack = 4)]
- private struct HeaderStruct
- {
- public int Reserved;
- ///
- /// The absolute offset of struct array in the data stream.
- ///
- public int AssetInfoAbsoluteStartOffset;
- ///
- /// The count of the struct array.
- ///
- public int AssetCount;
- ///
- /// The size of the struct.
- ///
- public int AssetInfoStructSize;
- public int Unknown1;
- ///
- /// The absolute offset of filename buffer in the data stream.
- ///
- public int FilenameBufferStartOffset;
- public int Unknown2;
- ///
- /// The size of the filename buffer in bytes.
- ///
- public int FilenameBufferSize;
- }
-}
-
-internal class StarRailAssetNative
-{
- public required string Filename { get; init; }
- public required long Size { get; init; }
- public required byte[] MD5Checksum { get; init; }
-
- public static Span Parse(Span filenameBuffer,
- ref StarRailAssetNativeInfo assetInfo,
- out StarRailAssetNative? result)
- {
- Unsafe.SkipInit(out result);
- int filenameLength = assetInfo.FilenameLength;
- if (filenameBuffer.Length < filenameLength)
- {
- return filenameBuffer;
- }
-
- string filename = Encoding.UTF8.GetString(filenameBuffer[..filenameLength]);
- long fileSize = assetInfo.Size;
- byte[] md5Checksum = new byte[16];
-
- assetInfo.Shifted4BytesMD5Checksum.CopyTo(md5Checksum);
- StarRailBinaryDataExtension.ReverseReorderBy4X4HashData(md5Checksum); // Reorder 4x4 hash using SIMD
-
- result = new StarRailAssetNative
- {
- Filename = filename,
- Size = fileSize,
- MD5Checksum = md5Checksum
- };
-
- return filenameBuffer[filenameLength..];
- }
-
- [StructLayout(LayoutKind.Sequential, Pack = 4)]
- public unsafe struct StarRailAssetNativeInfo
- {
- private fixed byte _shifted4BytesMD5Checksum[16];
- public long Size;
- public int FilenameLength;
- public int FilenameStartAt;
-
- public ReadOnlySpan Shifted4BytesMD5Checksum
- {
- get
- {
- fixed (byte* magicP = _shifted4BytesMD5Checksum)
- {
- return new ReadOnlySpan(magicP, 16);
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailAssetBinaryMetadata.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailAssetBinaryMetadata.cs
new file mode 100644
index 000000000..d0adc2c96
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailAssetBinaryMetadata.cs
@@ -0,0 +1,33 @@
+using System;
+using CollapseLauncher.RepairManagement.StarRail.Struct.Assets;
+// ReSharper disable CommentTypo
+#pragma warning disable IDE0290
+#pragma warning disable IDE0130
+#nullable enable
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct;
+
+///
+/// Star Rail Binary Metadata (SRBM) data parser. This parser is an abstract, read-only and cannot be written back.
+/// This implementation inherit these subtypes:
+///
+///
+///
+public abstract class StarRailAssetBinaryMetadata : StarRailBinaryData
+{
+ protected StarRailAssetBinaryMetadata(
+ short parentTypeFlag,
+ short typeVersionFlag,
+ int headerOrDataLength,
+ short subStructCount,
+ short subStructSize)
+ : base(MagicSignatureStatic,
+ parentTypeFlag,
+ typeVersionFlag,
+ headerOrDataLength,
+ subStructCount,
+ subStructSize) { }
+
+ private static ReadOnlySpan MagicSignatureStatic => "SRBM"u8;
+ protected override ReadOnlySpan MagicSignature => MagicSignatureStatic;
+}
diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailAssetMetadataIndex.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailAssetMetadataIndex.cs
new file mode 100644
index 000000000..74259c6a1
--- /dev/null
+++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Struct/StarRailAssetMetadataIndex.cs
@@ -0,0 +1,173 @@
+using System;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+// ReSharper disable CommentTypo
+#pragma warning disable IDE0290
+#pragma warning disable IDE0130
+#nullable enable
+
+namespace CollapseLauncher.RepairManagement.StarRail.Struct;
+
+///
+/// Star Rail Metadata Index (SRMI) data parser. This parser also implements to write back the result to a .
+/// This implementation inherit these subtypes:
+///
+internal class StarRailAssetMetadataIndex : StarRailBinaryDataWritable
+{
+ public StarRailAssetMetadataIndex()
+ : base(MagicSignatureStatic,
+ 768,
+ 256,
+ 0, // On SRMI, the header length is actually the entire size of the data inside the stream (including header).
+ // The size will be recalculated if something changed.
+
+ 0, // On SRMI, the value is always be 0.
+ 12) // On SRMI, the subStruct header length is 12 bytes (compared to SRBM's 16 bytes)
+ { }
+
+ private static ReadOnlySpan MagicSignatureStatic => "SRMI"u8;
+ protected override ReadOnlySpan MagicSignature => MagicSignatureStatic;
+
+ protected override async ValueTask ReadDataCoreAsync(
+ long currentOffset,
+ Stream dataStream,
+ CancellationToken token = default)
+ {
+ (MetadataIndexStruct indexData, int readHeader) =
+ await dataStream
+ .ReadDataAssertAndSeekAsync(_ => Unsafe.SizeOf(), token)
+ .ConfigureAwait(false);
+ currentOffset += readHeader;
+
+ MetadataIndex.Parse(in indexData, out MetadataIndex metadataIndex);
+ DataList = [metadataIndex];
+
+ return currentOffset;
+ }
+
+ protected override async ValueTask WriteHeaderCoreAsync(Stream dataStream, CancellationToken token = default)
+ {
+ int sizeOfHeader = Marshal.SizeOf