From 9b002d19c3a7c36d9aeab4114944397b4e8a1291 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sun, 5 Apr 2026 22:05:17 +0700 Subject: [PATCH 1/6] Update NuGet --- CollapseLauncher/CollapseLauncher.csproj | 38 +- CollapseLauncher/packages.lock.json | 376 +++++++++--------- ColorThief | 2 +- H.NotifyIcon | 2 +- ...Toolkit.WinUI.Controls.ImageCropper.csproj | 14 +- .../ImageCropper/packages.lock.json | 156 ++++---- ...kit.WinUI.Controls.SettingsControls.csproj | 10 +- .../SettingsControls/packages.lock.json | 136 +++---- Hi3Helper.Core/Hi3Helper.Core.csproj | 4 +- Hi3Helper.Core/packages.lock.json | 42 +- Hi3Helper.EncTool | 2 +- Hi3Helper.Plugin.Core | 2 +- Hi3Helper.SharpDiscordRPC | 2 +- Hi3Helper.Sophon | 2 +- Hi3Helper.Win32 | 2 +- ImageEx | 2 +- InnoSetupHelper/InnoSetupHelper.csproj | 2 +- InnoSetupHelper/packages.lock.json | 12 +- env.json | 2 +- global.json | 2 +- 20 files changed, 405 insertions(+), 405 deletions(-) diff --git a/CollapseLauncher/CollapseLauncher.csproj b/CollapseLauncher/CollapseLauncher.csproj index d86796b57..2794ddad0 100644 --- a/CollapseLauncher/CollapseLauncher.csproj +++ b/CollapseLauncher/CollapseLauncher.csproj @@ -256,9 +256,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -266,14 +266,14 @@ - - - - - - - - + + + + + + + + - 1.83.15 + 1.83.16 preview x64 From 60e2530fae6bc0345438f3499e1a752f5433c727 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sun, 5 Apr 2026 22:15:33 +0700 Subject: [PATCH 3/6] Manual Cherry-pick backport from Main --- .../Base/InstallManagerBase.Sophon.cs | 8 +-- .../StarRail/StarRailInstall.SophonPatch.cs | 20 +++--- .../Zenless/ZenlessInstall.SophonPatch.cs | 20 ++---- .../HonkaiV2/HonkaiRepairV2.AsbExt.Video.cs | 63 ++++++++++--------- .../HonkaiV2/HonkaiRepairV2.Fetch.cs | 16 ++--- 5 files changed, 56 insertions(+), 71 deletions(-) diff --git a/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.Sophon.cs b/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.Sophon.cs index fae5f6687..c1a8b19c8 100644 --- a/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.Sophon.cs +++ b/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.Sophon.cs @@ -796,7 +796,7 @@ await TryGetAdditionalPackageForSophonDiff(httpClient, } // Filter asset list - await FilterSophonPatchAssetList(sophonUpdateAssetList, Token!.Token); + await FilterAssetList(sophonUpdateAssetList, x => x.AssetName, Token!.Token); // Get the remote chunk size ProgressPerFileSizeTotal = sophonUpdateAssetList.GetCalculatedDiffSize(!isPreloadMode); @@ -925,12 +925,6 @@ await Parallel.ForEachAsync(sophonUpdateAssetList } } - protected virtual Task FilterSophonPatchAssetList(List itemList, CancellationToken token) - { - // NOP - return Task.CompletedTask; - } - private ValueTask RunSophonAssetDownloadThread(HttpClient client, SophonAsset asset, ParallelOptions parallelOptions) diff --git a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs index 156db6930..47bd02c24 100644 --- a/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs +++ b/CollapseLauncher/Classes/InstallManagement/StarRail/StarRailInstall.SophonPatch.cs @@ -1,4 +1,5 @@ -using Hi3Helper.Data; +using Hi3Helper; +using Hi3Helper.Data; using System; using System.Buffers; using System.Collections.Generic; @@ -56,15 +57,19 @@ public override async Task FilterAssetList( List listFiltered = []; foreach (T patchAsset in itemList) { - if (itemPathSelector(patchAsset) is not {} filePath) + if (itemPathSelector(patchAsset) is not { } filePath) { listFiltered.Add(patchAsset); continue; } + ConverterTool.NormalizePathInplaceNoTrim(filePath); + int indexOfAny = filePath.IndexOfAny(searchValues); if (indexOfAny >= 0) { + Logger.LogWriteLine($"[StarRailInstall::FilterAssetList] Asset: {patchAsset} is ignored due to marked as deleted asset.", + writeToLog: true); continue; } @@ -90,12 +95,9 @@ static ReadOnlySpan GetFilePathFromJson(ReadOnlySpan line) line = line[(firstIndexOf + first.Length)..]; int endIndexOf = line.IndexOf(end); - if (endIndexOf <= 0) - { - return ReadOnlySpan.Empty; - } - - return line[..endIndexOf]; + return endIndexOf <= 0 + ? ReadOnlySpan.Empty + : line[..endIndexOf]; } } @@ -127,4 +129,4 @@ private static void AddBothPersistentOrStreamingAssets( } } } -} +} \ No newline at end of file diff --git a/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs b/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs index c867ee2fb..c541434d3 100644 --- a/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs +++ b/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs @@ -1,11 +1,9 @@ -using Hi3Helper.Sophon; -using Hi3Helper.Sophon.Infos; +using Hi3Helper; using Hi3Helper.Sophon.Structs; using System; using System.Buffers; using System.Collections.Generic; using System.IO; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; // ReSharper disable CheckNamespace @@ -17,17 +15,9 @@ internal partial class ZenlessInstall { private const StringSplitOptions SplitOptions = StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries; - // ReSharper disable once StringLiteralTypo - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] - private static extern ref SophonChunksInfo GetChunkAssetChunksInfo(SophonAsset element); - - // ReSharper disable once StringLiteralTypo - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] - private static extern ref SophonChunksInfo GetChunkAssetChunksInfoAlt(SophonAsset element); - public override async Task FilterAssetList( - List itemList, - Func itemPathSelector, + List itemList, + Func itemPathSelector, CancellationToken token) { HashSet exceptMatchFieldHashSet = await GetExceptMatchFieldHashSet(token); @@ -67,6 +57,8 @@ private static void FilterSophonAsset(List itemList, HashSet excep if (asset is SophonIdentifiableProperty { MatchingField: { } assetMatchingField } && exceptMatchFieldHashSet.Contains(assetMatchingField)) { + Logger.LogWriteLine($"[ZenlessInstall::FilterSophonAsset] Asset: {asset} is ignored due to marked as deleted asset.", + writeToLog: true); continue; } @@ -114,4 +106,4 @@ internal static HashSet CreateExceptMatchFieldHashSet(string exceptMatch return hashSetReturn; } } -} +} \ No newline at end of file diff --git a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.AsbExt.Video.cs b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.AsbExt.Video.cs index d5405cc0a..1ccb90054 100644 --- a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.AsbExt.Video.cs +++ b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.AsbExt.Video.cs @@ -1,12 +1,12 @@ -using CollapseLauncher.Helper.Metadata; +using CollapseLauncher.Helper; +using CollapseLauncher.Helper.Metadata; using CollapseLauncher.Interfaces; using Hi3Helper; using Hi3Helper.Data; using Hi3Helper.EncTool; using Hi3Helper.EncTool.Parser.AssetMetadata; -using Hi3Helper.EncTool.Parser.Cache; +using Hi3Helper.EncTool.Parser.CacheParser; using Hi3Helper.EncTool.Parser.KianaDispatch; -using Hi3Helper.Plugin.Core.Management; using Hi3Helper.Preset; using Hi3Helper.Shared.ClassStruct; using System; @@ -14,12 +14,9 @@ using System.IO; using System.Linq; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; -using CGMetadataHashId = Hi3Helper.EncTool.Parser.Cache.HashID; - // ReSharper disable CheckNamespace // ReSharper disable IdentifierTypo // ReSharper disable StringLiteralTypo @@ -32,6 +29,7 @@ namespace CollapseLauncher.RepairManagement; internal static partial class AssetBundleExtension { internal const string RelativePathVideo = @"BH3_Data\StreamingAssets\Video\"; + internal const string MetadataFilename = "107438912"; internal static async Task> GetVideoAssetListAsync( @@ -57,11 +55,11 @@ await GetCacheAssetBundleListAsync(assetBundleHttpClient, token); CacheAssetInfo? cgMetadataFile = assetInfoList - .FirstOrDefault(x => x.Asset.N.EndsWith(CGMetadataHashId.CgMetadataFilename)); + .FirstOrDefault(x => x.Asset.N.EndsWith(MetadataFilename)); if (cgMetadataFile == null) { - Logger.LogWriteLine($"[AssetBundleExtension::GetVideoAssetListAsync] Cannot find CG Metadata file with Asset ID: {CGMetadataHashId.CgMetadataFilename}", + Logger.LogWriteLine($"[AssetBundleExtension::GetVideoAssetListAsync] Cannot find CG Metadata file with Asset ID: {MetadataFilename}", LogType.Error, true); return []; @@ -83,36 +81,36 @@ await GetCacheAssetBundleListAsync(assetBundleHttpClient, cgFileStreamMemory.Position = 0; await using CacheStream dechipheredCgStream = - new CacheStream(cgFileStreamMemory, preSeed: cgMetadataFile.MhyMersenneTwisterSeed); + new(cgFileStreamMemory, preSeed: cgMetadataFile.MhyMersenneTwisterSeed); List cgEntries = []; await Parallel - .ForEachAsync(CGMetadata.Enumerate(dechipheredCgStream, Encoding.UTF8), + .ForEachAsync(KianaCgMetadata.Parse(dechipheredCgStream), new ParallelOptions { - CancellationToken = token, + CancellationToken = token, MaxDegreeOfParallelism = parallelThread }, ImplCheckAndAdd); return cgEntries; - async ValueTask ImplCheckAndAdd(CGMetadata entry, CancellationToken innerToken) + async ValueTask ImplCheckAndAdd(KeyValuePair entry, CancellationToken innerToken) { - if (entry.InStreamingAssets || - ignoredCgHashset.Contains(entry.CgSubCategory)) + if (entry.Value.DownloadMode == CGDownloadMode.DownloadTipAlways || + ignoredCgHashset.Contains(entry.Value.SubCategoryId)) { return; } - string assetName = gameLanguageType == AudioLanguageType.Japanese - ? entry.CgPathHighBitrateJP - : entry.CgPathHighBitrateCN; + string assetName = (gameLanguageType == AudioLanguageType.Japanese + ? entry.Value.PathJp + : entry.Value.PathCn) ?? throw new NullReferenceException(); assetName += ".usm"; long assetFilesize = gameLanguageType == AudioLanguageType.Japanese - ? entry.FileSizeHighBitrateJP - : entry.FileSizeHighBitrateCN; + ? entry.Value.SizeJp + : entry.Value.SizeCn; foreach (string baseUrl in gameServerInfo.ExternalAssetUrls) { @@ -130,25 +128,28 @@ async ValueTask ImplCheckAndAdd(CGMetadata entry, CancellationToken innerToken) // Update status if (progressibleInstance != null) { - progressibleInstance.Status.ActivityStatus = string.Format(Locale.Lang._GameRepairPage.Status14, entry.CgExtraKey); + progressibleInstance.Status.ActivityStatus = string.Format(Locale.Lang._GameRepairPage.Status14, assetName); progressibleInstance.Status.IsProgressAllIndetermined = true; progressibleInstance.Status.IsProgressPerFileIndetermined = true; progressibleInstance.UpdateStatus(); } - UrlStatus urlStatus = await assetBundleHttpClient.GetURLStatusCode(assetUrl, innerToken); - Logger.LogWriteLine($"The CG asset: {assetName} " + - (urlStatus.IsSuccessStatusCode ? "is" : "is not") + $" available (Status code: {urlStatus.StatusCode})", LogType.Default, true); - if (!urlStatus.IsSuccessStatusCode) + if (entry.Value.Category is CGCategory.Birthday or CGCategory.Activity or CGCategory.VersionPV) { - continue; - } + UrlStatus urlStatus = await assetBundleHttpClient.GetURLStatusCode(assetUrl, innerToken); + Logger.LogWriteLine($"The CG asset: {assetName} " + + (urlStatus.IsSuccessStatusCode ? "is" : "is not") + $" available (Status code: {urlStatus.StatusCode})", LogType.Default, true); + if (!urlStatus.IsSuccessStatusCode) + { + continue; + } - if (urlStatus.FileSize > 0) - { - assetFilesize = urlStatus.FileSize; + if (urlStatus.FileSize > 0) + { + assetFilesize = urlStatus.FileSize; + } } - + lock (cgEntries) { cgEntries.Add(new FilePropertiesRemote @@ -166,4 +167,4 @@ async ValueTask ImplCheckAndAdd(CGMetadata entry, CancellationToken innerToken) throw new HttpRequestException("No Asset bundle URLs were reachable"); } } -} +} \ No newline at end of file diff --git a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs index 56c47fab4..970d39cfc 100644 --- a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs @@ -2,19 +2,15 @@ using CollapseLauncher.Helper.Metadata; using CollapseLauncher.Helper.StreamUtility; using CollapseLauncher.RepairManagement; -using Hi3Helper; using Hi3Helper.Data; using Hi3Helper.EncTool; using Hi3Helper.EncTool.Parser.AssetMetadata; -using Hi3Helper.EncTool.Parser.Cache; +using Hi3Helper.EncTool.Parser.CacheParser; using Hi3Helper.EncTool.Parser.KianaDispatch; using Hi3Helper.EncTool.Parser.Senadina; using Hi3Helper.Shared.ClassStruct; -using Hi3Helper.Sophon; -using Hi3Helper.Sophon.Structs; using Microsoft.Win32; using System; -using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; using System.IO; @@ -56,7 +52,7 @@ await this.FetchAssetsFromSophonAsync(HttpClientGeneric, #region Fetch by Game AssetBundle private async Task FetchAssetFromGameAssetBundle(List assetIndex, CancellationToken token) { - PresetConfig gamePresetConfig = GameVersionManager!.GamePreset; + PresetConfig gamePresetConfig = GameVersionManager.GamePreset; FinalizeBasicAssetsPath(assetIndex); // Get ignored assets from registry @@ -280,7 +276,7 @@ private void FinalizeVideoAssetsPath(List assetList) { string relativePath = Path.Combine(AssetBundleExtension.RelativePathVideo, asset.N); ConverterTool.NormalizePathInplaceNoTrim(relativePath); - if (asset.AssociatedObject is CGMetadata { InStreamingAssets: false }) + if (asset.AssociatedObject is KianaCgMetadata { DownloadMode: CGDownloadMode.DownloadTipAlways }) { versionStreamWriter.WriteLine($"Video/{asset.N}\t1"); } @@ -385,8 +381,8 @@ private async Task FinalizeBlockAssetsPath( string relativePath = Path.Combine(AssetBundleExtension.RelativePathBlock, asset.N); asset.N = relativePath; - if (asset.BlockPatchInfo is {} patchInfo && - patchInfo.PatchPairs.FirstOrDefault() is {} patchPair) + if (asset.BlockPatchInfo is { } patchInfo && + patchInfo.PatchPairs.FirstOrDefault() is { } patchPair) { oldBlockNames.Add(Path.Combine(AssetBundleExtension.RelativePathBlock, patchPair.OldName)); } @@ -474,4 +470,4 @@ await identifier } } #endregion -} +} \ No newline at end of file From 4b21ccfa15e4063d2ce91c558a0716250fe7ba41 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Mon, 6 Apr 2026 19:02:10 +0700 Subject: [PATCH 4/6] Improve Hi3 Cache Update and Game Repair file checks + Fix broken cutscene detection due to unmatching file size reported by metadata vs. the actual size reported by the URL (good job, HoYo. As always :]) + Dynamically loads Cache Update's unused file ignore list from preset config. --- .../Classes/CachesManagement/Honkai/Check.cs | 23 ++++++----- .../Classes/Helper/Metadata/PresetConfig.cs | 11 ++--- .../Helper/StreamUtility/StreamExtension.cs | 32 +++++++++++---- .../HonkaiV2/HonkaiRepairV2.AsbExt.Video.cs | 39 ++++++++++-------- .../HonkaiV2/HonkaiRepairV2.Check.Generic.cs | 41 +++++++++++++++++++ .../HonkaiV2/HonkaiRepairV2.Fetch.cs | 18 ++++++-- 6 files changed, 120 insertions(+), 44 deletions(-) diff --git a/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs b/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs index 92e956854..d8ee5ca82 100644 --- a/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs +++ b/CollapseLauncher/Classes/CachesManagement/Honkai/Check.cs @@ -58,15 +58,6 @@ private async Task> Check(List assetIndex, Cancella return returnAsset; } - private readonly SearchValues _unusedSearchValues = SearchValues.Create([ - "output_log", - "Crashes", - "Verify.txt", - "APM", - "FBData", - "asb.dat" - ], StringComparison.OrdinalIgnoreCase); - private void CheckUnusedAssets(List assetIndex, List returnAsset) { // Directory info and if the directory doesn't exist, return @@ -76,13 +67,25 @@ private void CheckUnusedAssets(List assetIndex, List ret return; } + SearchValues unusedSearchValues = SearchValues + .Create(GameVersionManager.GamePreset.GameInstallFileInfo?.CacheUpdateUnusedFilesIgnoreList + ?? [ + "output_log", + "Crashes", + "Verify.txt", + "APM", + "FBData", + "asb.dat", + "MiHoYoSDK.log" + ], StringComparison.OrdinalIgnoreCase); + // Iterate the file contained in the _gamePath foreach (FileInfo fileInfo in directoryInfo.EnumerateFiles("*", SearchOption.AllDirectories) .EnumerateNoReadOnly()) { ReadOnlySpan filePath = fileInfo.FullName; - if (filePath.ContainsAny(_unusedSearchValues) + if (filePath.ContainsAny(unusedSearchValues) || assetIndex.Exists(x => x.ConcatPath == fileInfo.FullName)) { continue; diff --git a/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs b/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs index f1b1e3283..a77bf3935 100644 --- a/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs +++ b/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs @@ -81,11 +81,12 @@ public enum LauncherType public class GameInstallFileInfo { - public string GameDataFolderName { get; init; } = string.Empty; - public string[] FilesToDelete { get; init; } = []; - public string[] FoldersToDelete { get; init; } = []; - public string[] FoldersToKeepInData { get; init; } = []; - public string[] FilesCleanupIgnoreList { get; init; } = []; + public string GameDataFolderName { get; init; } = string.Empty; + public string[] FilesToDelete { get; init; } = []; + public string[] FoldersToDelete { get; init; } = []; + public string[] FoldersToKeepInData { get; init; } = []; + public string[] FilesCleanupIgnoreList { get; init; } = []; + public string[] CacheUpdateUnusedFilesIgnoreList { get; init; } = []; } public class SophonChunkUrls diff --git a/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs b/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs index 0e7cae51b..90df5161d 100644 --- a/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs +++ b/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs @@ -309,7 +309,7 @@ public static FileInfo ResolveSymlink(this FileInfo fileInfo) /// /// The directory to remove. /// Whether to remove all possibly empty directories recursively. - public static void DeleteEmptyDirectory(this string dir, bool recursive = false) + public static bool DeleteEmptyDirectory(this string dir, bool recursive = false) => new DirectoryInfo(dir).DeleteEmptyDirectory(recursive); /// @@ -317,20 +317,34 @@ public static void DeleteEmptyDirectory(this string dir, bool recursive = false) /// /// The directory to remove. /// Whether to remove all possibly empty directories recursively. - public static void DeleteEmptyDirectory(this DirectoryInfo dir, bool recursive = false) + public static bool DeleteEmptyDirectory(this DirectoryInfo dir, bool recursive = false) { - if (recursive) + try { - foreach (DirectoryInfo childDir in dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) + if (!dir.Exists) { - childDir.DeleteEmptyDirectory(); + return true; + } + + if (recursive) + { + foreach (DirectoryInfo childDir in dir.EnumerateDirectories("*", SearchOption.TopDirectoryOnly)) + { + childDir.DeleteEmptyDirectory(); + } } - } - FindFiles.TryIsDirectoryEmpty(dir.FullName, out bool isEmpty); - if (isEmpty) + FindFiles.TryIsDirectoryEmpty(dir.FullName, out bool isEmpty); + if (isEmpty) + { + dir.Delete(true); + } + + return true; + } + catch { - dir.Delete(true); + return false; } } diff --git a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.AsbExt.Video.cs b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.AsbExt.Video.cs index 1ccb90054..98b8e5890 100644 --- a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.AsbExt.Video.cs +++ b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.AsbExt.Video.cs @@ -31,6 +31,20 @@ internal static partial class AssetBundleExtension internal const string RelativePathVideo = @"BH3_Data\StreamingAssets\Video\"; internal const string MetadataFilename = "107438912"; + internal static void RemoveUnlistedVideoAssetFromList(this List originList, + List assetListFromVideo) + { + List originOthersListOnly = originList.Where(x => x.FT != FileType.Video).ToList(); + List originVideoListOnly = originList.Where(x => x.FT == FileType.Video).ToList(); + originList.Clear(); + originList.AddRange(originOthersListOnly); + + HashSet assetListVideoDict = + assetListFromVideo.Select(x => x.N).ToHashSet(StringComparer.OrdinalIgnoreCase); + + originList.AddRange(originVideoListOnly.Where(originVideoAsset => assetListVideoDict.Contains(originVideoAsset.N))); + } + internal static async Task> GetVideoAssetListAsync( this HttpClient assetBundleHttpClient, @@ -47,12 +61,12 @@ internal static async Task> HashSet ignoredCgHashset = new(ignoredCgIds ?? []); List assetInfoList = - await GetCacheAssetBundleListAsync(assetBundleHttpClient, - presetConfig, - gameServerInfo, - CacheAssetType.Data, - progressibleInstance, - token); + await assetBundleHttpClient + .GetCacheAssetBundleListAsync(presetConfig, + gameServerInfo, + CacheAssetType.Data, + progressibleInstance, + token); CacheAssetInfo? cgMetadataFile = assetInfoList .FirstOrDefault(x => x.Asset.N.EndsWith(MetadataFilename)); @@ -97,8 +111,7 @@ await Parallel async ValueTask ImplCheckAndAdd(KeyValuePair entry, CancellationToken innerToken) { - if (entry.Value.DownloadMode == CGDownloadMode.DownloadTipAlways || - ignoredCgHashset.Contains(entry.Value.SubCategoryId)) + if (ignoredCgHashset.Contains(entry.Value.SubCategoryId)) { return; } @@ -117,14 +130,6 @@ async ValueTask ImplCheckAndAdd(KeyValuePair entry, Cancel string assetUrl = (isUseHttpRepairOverride ? "http://" : "https://") + baseUrl; assetUrl = assetUrl.CombineURLFromString("Video", assetName); - // If the file has no appoinment schedule (like non-birthday CG), then return true - /* - if (entry.AppointmentDownloadScheduleID == 0) - { - goto AddCgEntry; // I love goto. Dun ask me why :> - } - */ - // Update status if (progressibleInstance != null) { @@ -158,7 +163,7 @@ async ValueTask ImplCheckAndAdd(KeyValuePair entry, Cancel N = assetName, RN = assetUrl, S = assetFilesize, - AssociatedObject = entry + AssociatedObject = entry.Value }); } return; diff --git a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Check.Generic.cs b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Check.Generic.cs index 46f5e25e2..f8f160dbb 100644 --- a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Check.Generic.cs +++ b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Check.Generic.cs @@ -3,7 +3,9 @@ using CollapseLauncher.RepairManagement; using Hi3Helper; using Hi3Helper.Data; +using Hi3Helper.EncTool; using Hi3Helper.EncTool.Hashes; +using Hi3Helper.EncTool.Parser.CacheParser; using Hi3Helper.EncTool.Parser.Senadina; using Hi3Helper.Shared.ClassStruct; using System; @@ -49,6 +51,39 @@ private async Task IsHashMatchedAuto( token)).IsHashMatched; } + /// + /// Check actual remote size asset from the actual URL.
+ ///
+ /// Note: This method only works on asset type with URL defined. + /// Otherwise, the method will immediately returns . + ///
+ private async ValueTask TryIsAssetRemoteSizeEquals( + FilePropertiesRemote asset, + FileInfo fileInfo, + bool useFastCheck, + CancellationToken token = default) + { + if (!fileInfo.Exists) + { + return false; + } + + if (asset.FT != FileType.Video || + string.IsNullOrEmpty(asset.RN) || + useFastCheck) + { + return true; + } + + UrlStatus status = await HttpClientAssetBundle.GetCachedUrlStatus(asset.RN, token); + if (!status.IsSuccessStatusCode || status.FileSize == 0) // Returns true if status is not successful or size is 0 anyways + { + return true; + } + + return fileInfo.Exists && status.FileSize == fileInfo.Length; + } + private async Task<(bool IsHashMatched, int HashSize)> IsHashMatchedAuto( FilePropertiesRemote asset, byte[] hashBuffer, @@ -78,6 +113,12 @@ private async Task IsHashMatchedAuto( if (!isAssetExist || (assetFileInfo.Length != asset.S && !isSkipSizeCheck)) { + // Try alternate check for video + if (await TryIsAssetRemoteSizeEquals(asset, assetFileInfo, useFastCheck, token)) + { + return (true, hashSize); + } + Interlocked.Add(ref ProgressAllSizeCurrent, asset.S); if (addAssetIfBroken) { diff --git a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs index 970d39cfc..ef4625bb6 100644 --- a/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/HonkaiV2/HonkaiRepairV2.Fetch.cs @@ -83,6 +83,7 @@ private async Task FetchAssetFromGameAssetBundle(List asse #region Fetch Video Assets from AssetBundle List assetListFromVideo = []; + List assetListFromVideoOnlyDownloadable = []; Task assetListFromVideoTask = HttpClientAssetBundle .GetVideoAssetListAsync(gamePresetConfig, @@ -93,6 +94,7 @@ private async Task FetchAssetFromGameAssetBundle(List asse .GetResultFromAction(result => { assetListFromVideo.AddRange(result); + assetListFromVideoOnlyDownloadable.AddRange(result.Where(x => ((KianaCgMetadata)x.AssociatedObject).DownloadMode == CGDownloadMode.DownloadTipOnce)); FinalizeVideoAssetsPath(assetListFromVideo); }); #endregion @@ -144,9 +146,18 @@ await Task.WhenAll(assetListFromVideoTask, assetListFromBlockTask); #endregion + #region Remove Video Assets from base + + if (!IsMainAssetOnlyMode && !IsCacheMode) + { + assetIndex.RemoveUnlistedVideoAssetFromList(assetListFromVideo); + } + + #endregion + // Finalize the asset index list by overriding it from above additional sources. FinalizeBaseAssetIndex(assetIndex, - assetListFromVideo, + assetListFromVideoOnlyDownloadable, assetListFromAudio, assetListFromBlock); } @@ -155,6 +166,7 @@ await Task.WhenAll(assetListFromVideoTask, #region Fetch by Game Cache Files private static Task FetchAssetFromGameCacheFiles(List assetIndex, CancellationToken token) { + // TODO: Use it for altering assets for Cache Update mode return Task.CompletedTask; } #endregion @@ -276,7 +288,7 @@ private void FinalizeVideoAssetsPath(List assetList) { string relativePath = Path.Combine(AssetBundleExtension.RelativePathVideo, asset.N); ConverterTool.NormalizePathInplaceNoTrim(relativePath); - if (asset.AssociatedObject is KianaCgMetadata { DownloadMode: CGDownloadMode.DownloadTipAlways }) + if (asset.AssociatedObject is KianaCgMetadata { DownloadMode: CGDownloadMode.DownloadTipOnce }) { versionStreamWriter.WriteLine($"Video/{asset.N}\t1"); } @@ -375,7 +387,7 @@ private async Task FinalizeBlockAssetsPath( CancellationToken token) { // Block assets replacement and add - HashSet oldBlockNames = new HashSet(StringComparer.OrdinalIgnoreCase); + HashSet oldBlockNames = new(StringComparer.OrdinalIgnoreCase); foreach (FilePropertiesRemote asset in targetAssetList) { string relativePath = Path.Combine(AssetBundleExtension.RelativePathBlock, asset.N); From 52c70c72b8933388460232500c99e0c2734a87e5 Mon Sep 17 00:00:00 2001 From: Ron Friedman <9833218+Cryotechnic@users.noreply.github.com> Date: Mon, 6 Apr 2026 09:41:37 -0400 Subject: [PATCH 5/6] [skip ci] Upgrade DataDog static analyzer action to v3 --- .github/workflows/dd-static-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dd-static-analysis.yml b/.github/workflows/dd-static-analysis.yml index 91458315e..4c9464f8a 100644 --- a/.github/workflows/dd-static-analysis.yml +++ b/.github/workflows/dd-static-analysis.yml @@ -14,11 +14,11 @@ jobs: - name: Check code meets quality and security standards id: datadog-static-analysis - uses: DataDog/datadog-static-analyzer-github-action@v1 + uses: DataDog/datadog-static-analyzer-github-action@v3 with: dd_api_key: ${{ secrets.DD_API_KEY }} dd_app_key: ${{ secrets.DD_APP_KEY }} dd_site: ap1.datadoghq.com secrets_enabled: false static_analysis_enabled: true - cpu_count: 2 \ No newline at end of file + cpu_count: 2 From 9cce3af95a300b6d0316b7b8c3d507717f58b879 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Wed, 8 Apr 2026 19:11:22 +0700 Subject: [PATCH 6/6] Allow Genshin Repair if no dispatch token is available --- .../Classes/RepairManagement/Genshin/Repair.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/CollapseLauncher/Classes/RepairManagement/Genshin/Repair.cs b/CollapseLauncher/Classes/RepairManagement/Genshin/Repair.cs index 80a099c52..589ef85a0 100644 --- a/CollapseLauncher/Classes/RepairManagement/Genshin/Repair.cs +++ b/CollapseLauncher/Classes/RepairManagement/Genshin/Repair.cs @@ -21,6 +21,7 @@ // ReSharper disable StringLiteralTypo // ReSharper disable CommentTypo +#nullable enable namespace CollapseLauncher { internal partial class GenshinRepair @@ -46,7 +47,15 @@ private async Task Repair(List repairAssetIndex, Can DownloadClient downloadClient = DownloadClient.CreateInstance(client); // Get the Dispatcher Query - QueryProperty queryProperty = await GetCachedDispatcherQuery(downloadClient.GetHttpClient(), token); + QueryProperty? queryProperty = null; + try + { + queryProperty = await GetCachedDispatcherQuery(downloadClient.GetHttpClient(), token); + } + catch (Exception ex) + { + LogWriteLine($"An error has occurred while parsing Persistent Manifests! {ex}", LogType.Warning, true); + } // Iterate repair asset ObservableCollection assetProperty = [.. AssetEntry]; @@ -97,7 +106,10 @@ await Parallel.ForEachAsync( } } - await SavePersistentRevision(queryProperty, token); + if (queryProperty != null) + { + await SavePersistentRevision(queryProperty, token); + } // Duplicate ctable.dat to ctable_streaming.dat string streamingAssetsPath = Path.Combine(GamePath, $"{ExecPrefix}_Data", "StreamingAssets"); @@ -157,7 +169,7 @@ private async Task RepairAssetTypeGeneric((PkgVersionProperties AssetIndex, IAss if (isUseSophonDownload) { ReadOnlySpan splittedPath = asset.AssetIndex.remoteName.TrimStart('\\'); - if (!SophonAssetDictRefLookup.TryGetValue(splittedPath, out SophonAsset downloadAsSophon)) + if (!SophonAssetDictRefLookup.TryGetValue(splittedPath, out SophonAsset? downloadAsSophon)) { throw new InvalidOperationException($"Asset {splittedPath} is marked as \"SophonGeneric\" but it wasn't included in the manifest"); }