From ab945f926a625c3d529b03f17104f3dcc509b7f2 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sun, 6 Oct 2024 10:49:13 +0700 Subject: [PATCH 01/18] Eepy --- .../GameVersion/Zenless/VersionCheck.cs | 81 ++++++++++++++++- CollapseLauncher/Classes/GamePropertyVault.cs | 2 +- .../Classes/Helper/Metadata/DataCooker.cs | 87 ++++++++++++++----- .../Classes/Helper/SimpleProtectData.cs | 1 - Hi3Helper.EncTool | 2 +- 5 files changed, 147 insertions(+), 26 deletions(-) diff --git a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs index f1ce6bb50..d48014b9f 100644 --- a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs +++ b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs @@ -1,17 +1,96 @@ +using CollapseLauncher.Helper.Metadata; +using Hi3Helper; using Microsoft.UI.Xaml; +using System; +using System.Buffers; +using System.Buffers.Text; +using System.Security.Cryptography; +using System.Text; namespace CollapseLauncher.GameVersioning { internal sealed class GameTypeZenlessVersion : GameVersionBase { #region Properties + private RSA SleepyInstance { get; set; } #endregion - public GameTypeZenlessVersion(UIElement parentUIElement, RegionResourceProp gameRegionProp, string gameName, string gameRegion) + #region Initialize Sleepy + private void InitializeSleepy(PresetConfig gamePreset) + { + // Go YEET + SleepyInstance = RSA.Create(); + goto StartCheck; + + // Go to the DOOM + QuitFail: + DisableRepairAndCacheInstance(gamePreset); + return; + + StartCheck: + // Check if the thing does not have thing, then DOOMED + if (gamePreset.DispatcherKey == null) + goto QuitFail; + + // We cannot pay the house so rent. + byte[] keyUtf8Base64 = ArrayPool.Shared.Rent(gamePreset.DispatcherKey.Length * 2); + + try + { + // Check if the data is an impostor, then eject (basically DOOMED) + if (!Encoding.UTF8.TryGetBytes(gamePreset.DispatcherKey, keyUtf8Base64, out int keyWrittenLen)) + goto QuitFail; + + // Also if the data is not a crew, then YEET. + OperationStatus base64DecodeStatus = Base64.DecodeFromUtf8InPlace(keyUtf8Base64.AsSpan(0, keyWrittenLen), out int keyFromBase64Len); + if (OperationStatus.Done != base64DecodeStatus) + { + Logger.LogWriteLine($"OOF, we cannot go to sleep as the bed is collapsing! :( Operation Status: {base64DecodeStatus}", LogType.Error, true); + goto QuitFail; + } + + // Try serve a dinner and if it fails, then GET OUT! + bool isServed = DataCooker.IsServeV3Data(keyUtf8Base64); + if (!isServed) + goto QuitFail; + + // Enjoy the meal (i guess?) + DataCooker.GetServeV3DataSize(keyUtf8Base64, out long servedCompressedSize, out long servedDecompressedSize); + Span outServeData = keyUtf8Base64.AsSpan(keyFromBase64Len, (int)servedDecompressedSize); + DataCooker.ServeV3Data(keyUtf8Base64.AsSpan(0, keyFromBase64Len), outServeData, (int)servedCompressedSize, (int)servedDecompressedSize, out int dataWritten); + + // Load the load + SleepyInstance.ImportRSAPrivateKey(outServeData.Slice(0, dataWritten), out int bytesRead); + + // If you're food poisoned, then go to the hospital + if (0 == bytesRead) + goto QuitFail; + + // Uh, what else? nothing to do? then go to sleep :amimir: + } + finally + { + // After you wake up, get out from the rent and pay for it. + ArrayPool.Shared.Return(keyUtf8Base64, true); + } + + return; + + // Close the door + void DisableRepairAndCacheInstance(PresetConfig config) + { + config.IsRepairEnabled = false; + config.IsCacheUpdateEnabled = false; + } + } + #endregion + + public GameTypeZenlessVersion(UIElement parentUIElement, RegionResourceProp gameRegionProp, PresetConfig gamePreset, string gameName, string gameRegion) : base(parentUIElement, gameRegionProp, gameName, gameRegion) { // Try check for reinitializing game version. TryReinitializeGameVersion(); + InitializeSleepy(gamePreset); } public override bool IsGameHasDeltaPatch() => false; diff --git a/CollapseLauncher/Classes/GamePropertyVault.cs b/CollapseLauncher/Classes/GamePropertyVault.cs index cc02bf1c6..b5510b41b 100644 --- a/CollapseLauncher/Classes/GamePropertyVault.cs +++ b/CollapseLauncher/Classes/GamePropertyVault.cs @@ -57,7 +57,7 @@ internal GamePresetProperty(UIElement UIElementParent, RegionResourceProp APIRes _GameInstall = new GenshinInstall(UIElementParent, _GameVersion); break; case GameNameType.Zenless: - _GameVersion = new GameTypeZenlessVersion(UIElementParent, _APIResouceProp, GameName, GameRegion); + _GameVersion = new GameTypeZenlessVersion(UIElementParent, _APIResouceProp, GamePreset, GameName, GameRegion); _GameSettings = new ZenlessSettings(_GameVersion); _GameCache = null; _GameRepair = null; diff --git a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs index 6a9c49256..1399931f7 100644 --- a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs +++ b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs @@ -2,17 +2,21 @@ using System; using System.Buffers; using System.Buffers.Text; +using System.IO; using System.IO.Compression; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; +using ZstdDecompressStream = ZstdNet.DecompressionStream; + namespace CollapseLauncher.Helper.Metadata { internal enum CompressionType : byte { None, - Brotli + Brotli, + Zstd } internal static class DataCooker @@ -150,27 +154,10 @@ internal static void ServeV3Data(ReadOnlySpan data, Span dataWritten = decompressedSize; break; case CompressionType.Brotli: - { - Span dataDecompressed = outData; - BrotliDecoder decoder = new BrotliDecoder(); - - int offset = 0; - int decompressedWritten = 0; - while (offset < compressedSize) - { - decoder.Decompress(dataRawBuffer.Slice(offset), dataDecompressed.Slice(decompressedWritten), - out int dataConsumedWritten, out int dataDecodedWritten); - decompressedWritten += dataDecodedWritten; - offset += dataConsumedWritten; - } - - if (decompressedSize != decompressedWritten) - { - throw new DataMisalignedException("Decompressed data is misaligned!"); - } - - dataWritten = decompressedWritten; - } + dataWritten = DecompressDataFromBrotli(outData, compressedSize, decompressedSize, dataRawBuffer); + break; + case CompressionType.Zstd: + dataWritten = DecompressDataFromZstd(outData, compressedSize, decompressedSize, dataRawBuffer); break; default: throw new FormatException($"Decompression format is not supported! ({compressionType})"); @@ -189,5 +176,61 @@ internal static void ServeV3Data(ReadOnlySpan data, Span } } } + + private static int DecompressDataFromBrotli(Span outData, int compressedSize, int decompressedSize, ReadOnlySpan dataRawBuffer) + { + int dataWritten; + BrotliDecoder decoder = new BrotliDecoder(); + + int offset = 0; + int decompressedWritten = 0; + while (offset < compressedSize) + { + decoder.Decompress(dataRawBuffer.Slice(offset), outData.Slice(decompressedWritten), + out int dataConsumedWritten, out int dataDecodedWritten); + decompressedWritten += dataDecodedWritten; + offset += dataConsumedWritten; + } + + if (decompressedSize != decompressedWritten) + { + throw new DataMisalignedException("Decompressed data is misaligned!"); + } + + dataWritten = decompressedWritten; + return dataWritten; + } + + private static unsafe int DecompressDataFromZstd(Span outData, int compressedSize, int decompressedSize, ReadOnlySpan dataRawBuffer) + { + int dataWritten; + + fixed (byte* inputBuffer = &dataRawBuffer[0]) + fixed (byte* outputBuffer = &outData[0]) + { + int decompressedWritten = 0; + + byte[] buffer = new byte[4 << 10]; + + using UnmanagedMemoryStream inputStream = new UnmanagedMemoryStream(inputBuffer, dataRawBuffer.Length); + using UnmanagedMemoryStream outputStream = new UnmanagedMemoryStream(outputBuffer, outData.Length); + using ZstdDecompressStream decompStream = new ZstdDecompressStream(inputStream); + + int read = 0; + while ((read = decompStream.Read(buffer)) > 0) + { + outputStream.Write(buffer, 0, read); + decompressedWritten += read; + } + + if (decompressedSize != decompressedWritten) + { + throw new DataMisalignedException("Decompressed data is misaligned!"); + } + + dataWritten = decompressedWritten; + return dataWritten; + } + } } } \ No newline at end of file diff --git a/CollapseLauncher/Classes/Helper/SimpleProtectData.cs b/CollapseLauncher/Classes/Helper/SimpleProtectData.cs index 739c29cee..923f68a08 100644 --- a/CollapseLauncher/Classes/Helper/SimpleProtectData.cs +++ b/CollapseLauncher/Classes/Helper/SimpleProtectData.cs @@ -1,7 +1,6 @@ using Hi3Helper; using System; using System.Buffers.Text; -using System.Security; using System.Security.Cryptography; using System.Text; diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool index d692d38e9..5252cce9f 160000 --- a/Hi3Helper.EncTool +++ b/Hi3Helper.EncTool @@ -1 +1 @@ -Subproject commit d692d38e9770e75a2b11c1cee5253f9be128517f +Subproject commit 5252cce9f484c55a4182409c35409b0d2ec69518 From 20480fe3f647ec02a75a8fc87e1e45e15d30c669 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Tue, 8 Oct 2024 02:04:45 +0700 Subject: [PATCH 02/18] Eat dessert --- .../GameVersion/Zenless/VersionCheck.cs | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs index d48014b9f..4807f50f3 100644 --- a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs +++ b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs @@ -3,7 +3,9 @@ using Microsoft.UI.Xaml; using System; using System.Buffers; +using System.Buffers.Binary; using System.Buffers.Text; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; @@ -13,6 +15,8 @@ internal sealed class GameTypeZenlessVersion : GameVersionBase { #region Properties private RSA SleepyInstance { get; set; } + internal string SleepyIdentity { get; set; } + internal string SleepyArea { get; set; } #endregion #region Initialize Sleepy @@ -58,11 +62,34 @@ private void InitializeSleepy(PresetConfig gamePreset) DataCooker.GetServeV3DataSize(keyUtf8Base64, out long servedCompressedSize, out long servedDecompressedSize); Span outServeData = keyUtf8Base64.AsSpan(keyFromBase64Len, (int)servedDecompressedSize); DataCooker.ServeV3Data(keyUtf8Base64.AsSpan(0, keyFromBase64Len), outServeData, (int)servedCompressedSize, (int)servedDecompressedSize, out int dataWritten); - + + // Time for dessert!!! + ReadOnlySpan cheeseCake = outServeData.Slice(0, dataWritten); + int identityN = BinaryPrimitives.ReadInt16LittleEndian(cheeseCake.Slice(dataWritten - 4)); + int identityN2 = identityN * 2; + int areaN = BinaryPrimitives.ReadInt16LittleEndian(cheeseCake.Slice(dataWritten - 2)); + int areaN2 = areaN * 2; + + int nInBite = identityN2 + areaN2; + int wine = dataWritten - (4 + nInBite); + Span applePie = outServeData.Slice(wine, nInBite); + + // And eat good + int len = applePie.Length; + int i = 0; + NomNom: + int pos = wine % ((len - i) & unchecked((int)0xFFFFFFFF)); + applePie[i] ^= outServeData[0x10 | pos]; + if (++i < len) goto NomNom; + + // Then sleep + SleepyIdentity = MemoryMarshal.Cast(applePie.Slice(0, identityN2)).ToString(); + SleepyArea = MemoryMarshal.Cast(applePie.Slice(identityN2, areaN2)).ToString(); + // Load the load SleepyInstance.ImportRSAPrivateKey(outServeData.Slice(0, dataWritten), out int bytesRead); - // If you're food poisoned, then go to the hospital + // If you felt food poisoned since last night's dinner, then go to the hospital if (0 == bytesRead) goto QuitFail; @@ -79,11 +106,13 @@ private void InitializeSleepy(PresetConfig gamePreset) // Close the door void DisableRepairAndCacheInstance(PresetConfig config) { +#if !DEBUG config.IsRepairEnabled = false; config.IsCacheUpdateEnabled = false; +#endif } } - #endregion +#endregion public GameTypeZenlessVersion(UIElement parentUIElement, RegionResourceProp gameRegionProp, PresetConfig gamePreset, string gameName, string gameRegion) : base(parentUIElement, gameRegionProp, gameName, gameRegion) From 9de578a83ee71ad85488e506295fc026f0523169 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Tue, 8 Oct 2024 02:05:19 +0700 Subject: [PATCH 03/18] Adding basic repair function for ZZZ --- CollapseLauncher/Classes/GamePropertyVault.cs | 2 +- .../Classes/Helper/Metadata/PresetConfig.cs | 4 +- .../Zenless/ZenlessRepair.Check.cs | 160 ++++++++++++ .../Zenless/ZenlessRepair.Fetch.cs | 243 ++++++++++++++++++ .../Zenless/ZenlessRepair.Repair.cs | 134 ++++++++++ .../RepairManagement/Zenless/ZenlessRepair.cs | 164 ++++++++++++ Hi3Helper.EncTool | 2 +- 7 files changed, 705 insertions(+), 4 deletions(-) create mode 100644 CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs create mode 100644 CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs create mode 100644 CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Repair.cs create mode 100644 CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs diff --git a/CollapseLauncher/Classes/GamePropertyVault.cs b/CollapseLauncher/Classes/GamePropertyVault.cs index b5510b41b..f94aa25e1 100644 --- a/CollapseLauncher/Classes/GamePropertyVault.cs +++ b/CollapseLauncher/Classes/GamePropertyVault.cs @@ -60,7 +60,7 @@ internal GamePresetProperty(UIElement UIElementParent, RegionResourceProp APIRes _GameVersion = new GameTypeZenlessVersion(UIElementParent, _APIResouceProp, GamePreset, GameName, GameRegion); _GameSettings = new ZenlessSettings(_GameVersion); _GameCache = null; - _GameRepair = null; + _GameRepair = new ZenlessRepair(UIElementParent, _GameVersion, _GameSettings as ZenlessSettings); _GameInstall = new ZenlessInstall(UIElementParent, _GameVersion, _GameSettings as ZenlessSettings); break; default: diff --git a/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs b/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs index e303d79a7..486d81108 100644 --- a/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs +++ b/CollapseLauncher/Classes/Helper/Metadata/PresetConfig.cs @@ -425,8 +425,8 @@ public SophonChunkUrls? LauncherResourceChunksURL public bool? IsHideSocMedDesc { get; init; } = true; #if !DEBUG - public bool? IsRepairEnabled { get; init; } - public bool? IsCacheUpdateEnabled { get; init; } + public bool? IsRepairEnabled { get; set; } + public bool? IsCacheUpdateEnabled { get; set; } #else public bool? IsRepairEnabled = true; public bool? IsCacheUpdateEnabled = true; diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs new file mode 100644 index 000000000..9a4165aef --- /dev/null +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs @@ -0,0 +1,160 @@ +using Hi3Helper; +using Hi3Helper.Data; +using Hi3Helper.Shared.ClassStruct; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; + +namespace CollapseLauncher +{ + internal partial class ZenlessRepair + { + private async Task Check(List assetIndex, CancellationToken token) + { + List brokenAssetIndex = new List(); + + // Set Indetermined status as false + _status.IsProgressAllIndetermined = false; + _status.IsProgressPerFileIndetermined = false; + + // Show the asset entry panel + _status.IsAssetEntryPanelShow = true; + + // Await the task for parallel processing + try + { + // Reset stopwatch + RestartStopwatch(); + + // Iterate assetIndex and check it using different method for each type and run it in parallel + await Parallel.ForEachAsync(assetIndex, new ParallelOptions { MaxDegreeOfParallelism = _threadCount, CancellationToken = token }, async (asset, threadToken) => + { + // Assign a task depends on the asset type + switch (asset.FT) + { + case FileType.Generic: + await CheckGenericAssetType(asset, brokenAssetIndex, threadToken); + break; + case FileType.Blocks: + // await CheckAssetType(asset, brokenAssetIndex, threadToken); + break; + case FileType.Audio: + // await CheckAssetType(asset, brokenAssetIndex, threadToken); + break; + case FileType.Video: + // await CheckAssetType(asset, brokenAssetIndex, threadToken); + break; + } + }); + } + catch (AggregateException ex) + { + throw ex.Flatten().InnerExceptions.First(); + } + catch (Exception) + { + throw; + } + + // Re-add the asset index with a broken asset index + assetIndex.Clear(); + assetIndex.AddRange(brokenAssetIndex); + } + + private async ValueTask CheckGenericAssetType(FilePropertiesRemote asset, List targetAssetIndex, CancellationToken token) + { + // Update activity status + _status.ActivityStatus = string.Format(Locale.Lang._GameRepairPage.Status6, StarRailRepairExtension.GetFileRelativePath(asset.N, _gamePath)); + + // Increment current total count + _progressAllCountCurrent++; + + // Reset per file size counter + _progressPerFileSizeTotal = asset.S; + _progressPerFileSizeCurrent = 0; + + // Get the file info + FileInfo fileInfo = new FileInfo(asset.N); + + // Check if the file exist or has unmatched size + if (!fileInfo.Exists) + { + // Update the total progress and found counter + _progressAllSizeFound += asset.S; + _progressAllCountFound++; + + // Set the per size progress + _progressPerFileSizeCurrent = asset.S; + + // Increment the total current progress + _progressAllSizeCurrent += asset.S; + + Dispatch(() => AssetEntry.Add( + new AssetProperty( + Path.GetFileName(asset.N), + ConvertRepairAssetTypeEnum(asset.FT), + Path.GetDirectoryName(asset.N), + asset.S, + null, + null + ) + )); + targetAssetIndex.Add(asset); + + Logger.LogWriteLine($"File [T: {asset.FT}]: {asset.N} is not found", LogType.Warning, true); + + return; + } + + // Skip CRC check if fast method is used + if (_useFastMethod) + { + return; + } + + // Open and read fileInfo as FileStream + using (FileStream filefs = fileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.Read)) + { + // If pass the check above, then do CRC calculation + // Additional: the total file size progress is disabled and will be incremented after this + byte[] localCRC = await CheckHashAsync(filefs, MD5.Create(), token); + + // If local and asset CRC doesn't match, then add the asset + if (!IsArrayMatch(localCRC, asset.CRCArray)) + { + _progressAllSizeFound += asset.S; + _progressAllCountFound++; + + Dispatch(() => AssetEntry.Add( + new AssetProperty( + Path.GetFileName(asset.N), + ConvertRepairAssetTypeEnum(asset.FT), + Path.GetDirectoryName(asset.N), + asset.S, + localCRC, + asset.CRCArray + ) + )); + + // Mark the main block as "need to be repaired" + asset.IsBlockNeedRepair = true; + targetAssetIndex.Add(asset); + + Logger.LogWriteLine($"File [T: {asset.FT}]: {asset.N} is broken! Index CRC: {asset.CRC} <--> File CRC: {HexTool.BytesToHexUnsafe(localCRC)}", LogType.Warning, true); + } + } + } + + private RepairAssetType ConvertRepairAssetTypeEnum(FileType assetType) => assetType switch + { + FileType.Blocks => RepairAssetType.Block, + FileType.Audio => RepairAssetType.Audio, + FileType.Video => RepairAssetType.Video, + _ => RepairAssetType.General + }; + } +} diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs new file mode 100644 index 000000000..deffc2f77 --- /dev/null +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -0,0 +1,243 @@ +using CollapseLauncher.Helper; +using CollapseLauncher.Helper.Metadata; +using Hi3Helper; +using Hi3Helper.Data; +using Hi3Helper.EncTool.Parser.AssetIndex; +using Hi3Helper.EncTool.Parser.Sleepy; +using Hi3Helper.Http; +using Hi3Helper.Shared.ClassStruct; +using Hi3Helper.Shared.Region; +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace CollapseLauncher +{ + internal partial class ZenlessRepair + { + #region Main Fetch Routine + private async Task Fetch(List assetIndex, CancellationToken token) + { + // Set total activity string as "Loading Indexes..." + _status.ActivityStatus = Locale.Lang._GameRepairPage.Status2; + _status.IsProgressAllIndetermined = true; + UpdateStatus(); + StarRailRepairExtension.ClearHashtable(); + + // Initialize new proxy-aware HttpClient + using HttpClient client = new HttpClientBuilder() + .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) + .SetUserAgent(_userAgent) + .SetAllowedDecompression(DecompressionMethods.None) + .Create(); + + // Initialize the new DownloadClient + DownloadClient downloadClient = DownloadClient.CreateInstance(client); + + try + { + // Get the primary manifest + await GetPrimaryManifest(downloadClient, token, assetIndex); + + // Get the in-game res manifest + await GetResManifest(downloadClient, token, assetIndex); + + // Force-Fetch the Bilibili SDK (if exist :pepehands:) + await FetchBilibiliSDK(token); + + // Remove plugin from assetIndex + // Skip the removal for Delta-Patch + if (!IsOnlyRecoverMain) + { + EliminatePluginAssetIndex(assetIndex); + } + } + finally + { + // Clear the hashtable + StarRailRepairExtension.ClearHashtable(); + // Unsubscribe the fetching progress and dispose it and unsubscribe cacheUtil progress to adapter + // _innerGameVersionManager.StarRailMetadataTool.HttpEvent -= _httpClient_FetchAssetProgress; + } + } + #endregion + + #region PrimaryManifest + private async Task GetPrimaryManifest(DownloadClient downloadClient, CancellationToken token, List assetIndex) + { + // Initialize pkgVersion list + List pkgVersion = new List(); + + // Initialize repo metadata + try + { + // Get the metadata + Dictionary repoMetadata = await FetchMetadata(token); + + // 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; + } + // If the base._isVersionOverride is true, then throw. This sanity check is required if the delta patch is being performed. + catch when (base._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 BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlIndex, token); + if (stream != null) + { + // Deserialize asset index and set it to list + AssetIndexV2 parserTool = new AssetIndexV2(); + pkgVersion = new List(parserTool.Deserialize(stream, out DateTime timestamp)); + Logger.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(); + } + + private async Task> FetchMetadata(CancellationToken token) + { + // Set metadata URL + string urlMetadata = string.Format(LauncherConfig.AppGameRepoIndexURLPrefix, GameVersionManagerCast.GamePreset.ProfileName); + + // Start downloading metadata using FallbackCDNUtil + await using BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlMetadata, token); + return await stream.DeserializeAsync(CoreLibraryJSONContext.Default.DictionaryStringString, token); + } + #endregion + + #region ResManifest + private async Task GetResManifest(DownloadClient downloadClient, CancellationToken token, List assetIndex) + { + PresetConfig gamePreset = GameVersionManagerCast.GamePreset; + SleepyProperty sleepyProperty = SleepyProperty.Create( + GameVersionManagerCast.GetGameExistingVersion()?.VersionString, + gamePreset.GameDispatchChannelName, + gamePreset.GameDispatchURL, + gamePreset.GameDispatchURLTemplate, + GameSettings?.GeneralData?.SelectedServerName ?? gamePreset.GameDispatchDefaultName, + gamePreset.GameGatewayURLTemplate, + gamePreset.ProtoDispatchKey, + SleepyBuildProperty.Create(GameVersionManagerCast.SleepyIdentity, GameVersionManagerCast.SleepyArea) + ); + } + #endregion + + #region Utilities + private void ConvertPkgVersionToAssetIndex(List pkgVersion, List assetIndex) + { + foreach (PkgVersionProperties entry in pkgVersion) + { + // Add the pkgVersion entry to asset index + FilePropertiesRemote normalizedProperty = GetNormalizedFilePropertyTypeBased( + _gameRepoURL, + entry.remoteName, + entry.fileSize, + entry.md5, + FileType.Generic, + true); + assetIndex.AddSanitize(normalizedProperty); + } + } + + private void EliminatePluginAssetIndex(List assetIndex) + { + _gameVersionManager.GameAPIProp.data.plugins?.ForEach(plugin => + { + assetIndex.RemoveAll(asset => + { + return plugin.package.validate?.Exists(validate => validate.path == asset.N) ?? false; + }); + }); + } + + private FilePropertiesRemote GetNormalizedFilePropertyTypeBased(string remoteParentURL, + string remoteRelativePath, + long fileSize, + string hash, + FileType type = FileType.Generic, + bool isPatchApplicable = false, + bool isHasHashMark = false) + { + string remoteAbsolutePath = type switch + { + FileType.Generic => ConverterTool.CombineURLFromString(remoteParentURL, remoteRelativePath), + _ => remoteParentURL + }; + var localAbsolutePath = Path.Combine(_gamePath, ConverterTool.NormalizePath(remoteRelativePath)); + + return new FilePropertiesRemote + { + FT = type, + CRC = hash, + S = fileSize, + N = localAbsolutePath, + RN = remoteAbsolutePath, + IsPatchApplicable = isPatchApplicable, + IsHasHashMark = isHasHashMark, + }; + } + + private string[] GetCurrentAudioLangList(string fallbackCurrentLangname) + { + // Initialize the variable. + string audioLangListPath = _gameAudioLangListPath; + string audioLangListPathStatic = _gameAudioLangListPathStatic; + string[] returnValue; + + // Check if the audioLangListPath is null or the file is not exist, + // then create a new one from the fallback value + if (audioLangListPath == null || !File.Exists(audioLangListPathStatic)) + { + // Try check if the folder is exist. If not, create one. + string audioLangPathDir = Path.GetDirectoryName(audioLangListPathStatic); + if (Directory.Exists(audioLangPathDir)) + Directory.CreateDirectory(audioLangPathDir); + + // Assign the default value and write to the file, then return. + returnValue = new string[] { fallbackCurrentLangname }; + File.WriteAllLines(audioLangListPathStatic, returnValue); + return returnValue; + } + + // Read all the lines. If empty, then assign the default value and rewrite it + returnValue = File.ReadAllLines(audioLangListPathStatic); + if (returnValue.Length == 0) + { + returnValue = new string[] { fallbackCurrentLangname }; + File.WriteAllLines(audioLangListPathStatic, returnValue); + } + + // Return the value + return returnValue; + } + + private void CountAssetIndex(List assetIndex) + { + // Sum the assetIndex size and assign to _progressAllSize + _progressAllSizeTotal = assetIndex.Sum(x => x.S); + + // Assign the assetIndex count to _progressAllCount + _progressAllCountTotal = assetIndex.Count; + } + #endregion + } +} diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Repair.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Repair.cs new file mode 100644 index 000000000..4c9a626b2 --- /dev/null +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Repair.cs @@ -0,0 +1,134 @@ +using CollapseLauncher.Helper; +using Hi3Helper.Http; +using Hi3Helper.Shared.ClassStruct; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net.Http; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Hi3Helper; +using Hi3Helper.Data; +using System.IO; + +namespace CollapseLauncher +{ + internal partial class ZenlessRepair + { + private async Task Repair(List repairAssetIndex, CancellationToken token) + { + // Set total activity string as "Waiting for repair process to start..." + _status.ActivityStatus = Locale.Lang._GameRepairPage.Status11; + _status.IsProgressAllIndetermined = true; + _status.IsProgressPerFileIndetermined = true; + + // Update status + UpdateStatus(); + + // Reset stopwatch + RestartStopwatch(); + + // Initialize new proxy-aware HttpClient + using HttpClient client = new HttpClientBuilder() + .UseLauncherConfig(_downloadThreadCount + _downloadThreadCountReserved) + .SetUserAgent(_userAgent) + .SetAllowedDecompression(DecompressionMethods.None) + .Create(); + + // Use the new DownloadClient instance + DownloadClient downloadClient = DownloadClient.CreateInstance(client); + + // Iterate repair asset and check it using different method for each type + ObservableCollection assetProperty = new ObservableCollection(AssetEntry); + if (_isBurstDownloadEnabled) + { + await Parallel.ForEachAsync( + PairEnumeratePropertyAndAssetIndexPackage( +#if ENABLEHTTPREPAIR + EnforceHTTPSchemeToAssetIndex(repairAssetIndex) +#else + repairAssetIndex +#endif + , assetProperty), + new ParallelOptions { CancellationToken = token, MaxDegreeOfParallelism = _downloadThreadCount }, + async (asset, innerToken) => + { + // Assign a task depends on the asset type + Task assetTask = asset.AssetIndex.FT switch + { + FileType.Blocks => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), + FileType.Audio => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), + FileType.Video => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), + _ => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken) + }; + + // Await the task + await assetTask; + }); + } + else + { + foreach ((FilePropertiesRemote AssetIndex, IAssetProperty AssetProperty) asset in + PairEnumeratePropertyAndAssetIndexPackage( +#if ENABLEHTTPREPAIR + EnforceHTTPSchemeToAssetIndex(repairAssetIndex) +#else + repairAssetIndex +#endif + , assetProperty)) + { + // Assign a task depends on the asset type + Task assetTask = asset.AssetIndex.FT switch + { + FileType.Blocks => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), + FileType.Audio => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), + FileType.Video => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), + _ => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token) + }; + + // Await the task + await assetTask; + } + } + + return true; + } + + #region GenericRepair + private async Task RepairAssetTypeGeneric((FilePropertiesRemote AssetIndex, IAssetProperty AssetProperty) asset, DownloadClient downloadClient, DownloadProgressDelegate downloadProgress, CancellationToken token) + { + // Increment total count current + _progressAllCountCurrent++; + // Set repair activity status + UpdateRepairStatus( + string.Format(Locale.Lang._GameRepairPage.Status8, Path.GetFileName(asset.AssetIndex.N)), + string.Format(Locale.Lang._GameRepairPage.PerProgressSubtitle2, ConverterTool.SummarizeSizeSimple(_progressAllSizeCurrent), ConverterTool.SummarizeSizeSimple(_progressAllSizeTotal)), + true); + + // If asset type is unused, then delete it + if (asset.AssetIndex.FT == FileType.Unused) + { + FileInfo fileInfo = new FileInfo(asset.AssetIndex.N!); + if (fileInfo.Exists) + { + fileInfo.IsReadOnly = false; + fileInfo.Delete(); + Logger.LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Blocks ? asset.AssetIndex.CRC : asset.AssetIndex.N)} deleted!", LogType.Default, true); + } + } + else + { + // Start asset download task + await RunDownloadTask(asset.AssetIndex.S, asset.AssetIndex.N!, asset.AssetIndex.RN, downloadClient, downloadProgress, token); + Logger.LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Blocks ? asset.AssetIndex.CRC : asset.AssetIndex.N)} has been downloaded!", LogType.Default, true); + } + + // Pop repair asset display entry + PopRepairAssetEntry(asset.AssetProperty); + } + #endregion + } +} diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs new file mode 100644 index 000000000..1cf2845a4 --- /dev/null +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs @@ -0,0 +1,164 @@ +using CollapseLauncher.GameSettings.Zenless; +using CollapseLauncher.GameVersioning; +using CollapseLauncher.Interfaces; +using Hi3Helper; +using Hi3Helper.Data; +using Hi3Helper.EncTool.Parser.AssetIndex; +using Hi3Helper.Shared.ClassStruct; +using Microsoft.UI.Xaml; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +#nullable enable +namespace CollapseLauncher +{ + internal partial class ZenlessRepair : ProgressBase, IRepair, IRepairAssetIndex + { + #region Properties + internal const string _assetGamePersistentPath = @"{0}_Data\Persistent"; + internal const string _assetGameStreamingPath = @"{0}_Data\StreamingAssets"; + + private bool IsOnlyRecoverMain { get; set; } + private string? ExecutableName { get; set; } + + private List? OriginAssetIndex { get; set; } + private GameTypeZenlessVersion? GameVersionManagerCast { get => _gameVersionManager as GameTypeZenlessVersion; } + private ZenlessSettings? GameSettings { get; init; } + + private string GameDataPersistentPath { get => Path.Combine(_gamePath, string.Format(_assetGamePersistentPath, ExecutableName)); } + private string GameDataStreamingPath { get => Path.Combine(_gamePath, string.Format(_assetGameStreamingPath, ExecutableName)); } + + protected string? _gameAudioLangListPath + { + get + { + // If the persistent folder is not exist, then return null + if (!Directory.Exists(GameDataPersistentPath)) + return null; + + // Get the audio lang path index + string audioLangPath = _gameAudioLangListPathStatic; + return File.Exists(audioLangPath) ? audioLangPath : null; + } + } + + private string? _gameAudioLangListPathAlternate + { + get + { + // If the persistent folder is not exist, then return null + if (!Directory.Exists(GameDataPersistentPath)) + return null; + + // Get the audio lang path index + string audioLangPath = _gameAudioLangListPathAlternateStatic; + return File.Exists(audioLangPath) ? audioLangPath : null; + } + } + + private string _gameAudioLangListPathStatic => + Path.Combine(GameDataPersistentPath, "audio_lang_launcher"); + private string _gameAudioLangListPathAlternateStatic => + Path.Combine(GameDataPersistentPath, "audio_lang"); + + protected override string _userAgent { get; set; } = "UnityPlayer/2019.4.40f1 (UnityWebRequest/1.0, libcurl/7.80.0-DEV)"; + + public ZenlessRepair(UIElement parentUI, IGameVersionCheck gameVersionManager, ZenlessSettings gameSettings, bool isOnlyRecoverMain = false, string? versionOverride = null) + : base(parentUI, gameVersionManager, null, "", versionOverride) + { + // Use IsOnlyRecoverMain for future delta-patch or main game only files + IsOnlyRecoverMain = isOnlyRecoverMain; + ExecutableName = Path.GetFileNameWithoutExtension(gameVersionManager.GamePreset.GameExecutableName); + GameSettings = gameSettings; + } + #endregion + + #region Public Methods + + ~ZenlessRepair() => Dispose(); + + public List GetAssetIndex() => OriginAssetIndex!; + + public async Task StartCheckRoutine(bool useFastCheck) + { + _useFastMethod = useFastCheck; + return await TryRunExamineThrow(CheckRoutine()); + } + + public async Task StartRepairRoutine(bool showInteractivePrompt = false, Action actionIfInteractiveCancel = null) + { + if (_assetIndex.Count == 0) throw new InvalidOperationException("There's no broken file being reported! You can't do the repair process!"); + + if (showInteractivePrompt) + { + await SpawnRepairDialog(_assetIndex, actionIfInteractiveCancel); + } + + _ = await TryRunExamineThrow(RepairRoutine()); + } + + private async Task CheckRoutine() + { + // Always clear the asset index list + _assetIndex.Clear(); + + // Reset status and progress + ResetStatusAndProgress(); + + // Step 1: Fetch asset indexes + await Fetch(_assetIndex, _token.Token); + + // Step 2: Calculate the total size and count of the files + CountAssetIndex(_assetIndex); + + // Step 3: 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. + // either way, returns false. + return SummarizeStatusAndProgress( + _assetIndex, + string.Format(Locale.Lang._GameRepairPage.Status3, _progressAllCountFound, ConverterTool.SummarizeSizeSimple(_progressAllSizeFound)), + Locale.Lang._GameRepairPage.Status4); + } + + private async Task RepairRoutine() + { + // Restart Stopwatch + RestartStopwatch(); + + // Assign repair task + Task repairTask = Repair(_assetIndex, _token.Token); + + // Run repair process + bool repairTaskSuccess = await TryRunExamineThrow(repairTask); + + // Reset status and progress + ResetStatusAndProgress(); + + // Set as completed + _status.IsCompleted = true; + _status.IsCanceled = false; + _status.ActivityStatus = Locale.Lang._GameRepairPage.Status7; + + // Update status and progress + UpdateAll(); + + return repairTaskSuccess; + } + + public void CancelRoutine() + { + // Trigger token cancellation + _token.Cancel(); + } + + public void Dispose() + { + CancelRoutine(); + } + #endregion + } +} diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool index 5252cce9f..3bdfa2fc3 160000 --- a/Hi3Helper.EncTool +++ b/Hi3Helper.EncTool @@ -1 +1 @@ -Subproject commit 5252cce9f484c55a4182409c35409b0d2ec69518 +Subproject commit 3bdfa2fc3c683beac32f1460e47ba64e98925c67 From 922b035543676e32a705ae6863d82fef90e265d5 Mon Sep 17 00:00:00 2001 From: Bagus Nur Listiyono Date: Wed, 9 Oct 2024 19:38:36 +0700 Subject: [PATCH 04/18] Update to .NET9 RC2 - EncTool --- Hi3Helper.EncTool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool index 3bdfa2fc3..13d14f990 160000 --- a/Hi3Helper.EncTool +++ b/Hi3Helper.EncTool @@ -1 +1 @@ -Subproject commit 3bdfa2fc3c683beac32f1460e47ba64e98925c67 +Subproject commit 13d14f9909dc4a078c02d9aa5fbb1d715cbc13fe From c3b1ec3bd763a44c47a438ca913221157022b248 Mon Sep 17 00:00:00 2001 From: Bagus Nur Listiyono Date: Wed, 9 Oct 2024 19:54:33 +0700 Subject: [PATCH 05/18] [skip ci] Fix NuGet error --- Hi3Helper.EncTool | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool index 13d14f990..2d96954f2 160000 --- a/Hi3Helper.EncTool +++ b/Hi3Helper.EncTool @@ -1 +1 @@ -Subproject commit 13d14f9909dc4a078c02d9aa5fbb1d715cbc13fe +Subproject commit 2d96954f2357563c649d64b5770270c5aeac7ef7 From 4323dcd0a2e29a8f116b272d5c2e22c7e93b2d8f Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Thu, 10 Oct 2024 03:05:48 +0700 Subject: [PATCH 06/18] Initial support for dispatch-based repair and cache update --- .../CachesManagement/Zenless/ZenlessCache.cs | 19 + .../GameVersion/Zenless/VersionCheck.cs | 4 +- CollapseLauncher/Classes/GamePropertyVault.cs | 2 +- .../GameConversionManagement.cs | 2 +- .../Classes/Interfaces/Class/Enums.cs | 2 +- .../Classes/Interfaces/Class/ProgressBase.cs | 32 ++ .../Classes/RepairManagement/Genshin/Check.cs | 6 +- .../RepairManagement/Genshin/Repair.cs | 2 +- .../Classes/RepairManagement/Honkai/Check.cs | 8 +- .../Classes/RepairManagement/Honkai/Fetch.cs | 6 +- .../Classes/RepairManagement/Honkai/Repair.cs | 8 +- .../RepairManagement/StarRail/Check.cs | 6 +- .../RepairManagement/StarRail/Fetch.cs | 12 +- .../RepairManagement/StarRail/Repair.cs | 8 +- .../Zenless/ZenlessRepair.Check.cs | 13 +- .../Zenless/ZenlessRepair.Extensions.cs | 399 ++++++++++++++++++ .../Zenless/ZenlessRepair.Fetch.cs | 306 +++++++++++--- .../Zenless/ZenlessRepair.Repair.cs | 19 +- .../RepairManagement/Zenless/ZenlessRepair.cs | 8 +- .../Zenless/ZenlessResManifest.cs | 35 ++ 20 files changed, 779 insertions(+), 118 deletions(-) create mode 100644 CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs create mode 100644 CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs create mode 100644 CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessResManifest.cs diff --git a/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs b/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs new file mode 100644 index 000000000..e44956c4b --- /dev/null +++ b/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs @@ -0,0 +1,19 @@ +using CollapseLauncher.GameSettings.Zenless; +using CollapseLauncher.Interfaces; +using Microsoft.UI.Xaml; +using System.Threading.Tasks; + +namespace CollapseLauncher +{ + internal class ZenlessCache : ZenlessRepair, ICache, ICacheBase + { + public ZenlessCache(UIElement parentUI, IGameVersionCheck gameVersionManager, ZenlessSettings gameSettings) + : base(parentUI, gameVersionManager, gameSettings, false, null, true) + { } + + public ZenlessCache AsBaseType() => this; + + public async Task StartUpdateRoutine(bool showInteractivePrompt = false) + => await base.StartRepairRoutine(showInteractivePrompt); + } +} diff --git a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs index 4807f50f3..8d07d328c 100644 --- a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs +++ b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs @@ -14,7 +14,7 @@ namespace CollapseLauncher.GameVersioning internal sealed class GameTypeZenlessVersion : GameVersionBase { #region Properties - private RSA SleepyInstance { get; set; } + internal RSA SleepyInstance { get; set; } internal string SleepyIdentity { get; set; } internal string SleepyArea { get; set; } #endregion @@ -112,7 +112,7 @@ void DisableRepairAndCacheInstance(PresetConfig config) #endif } } -#endregion + #endregion public GameTypeZenlessVersion(UIElement parentUIElement, RegionResourceProp gameRegionProp, PresetConfig gamePreset, string gameName, string gameRegion) : base(parentUIElement, gameRegionProp, gameName, gameRegion) diff --git a/CollapseLauncher/Classes/GamePropertyVault.cs b/CollapseLauncher/Classes/GamePropertyVault.cs index f94aa25e1..8327cca35 100644 --- a/CollapseLauncher/Classes/GamePropertyVault.cs +++ b/CollapseLauncher/Classes/GamePropertyVault.cs @@ -59,7 +59,7 @@ internal GamePresetProperty(UIElement UIElementParent, RegionResourceProp APIRes case GameNameType.Zenless: _GameVersion = new GameTypeZenlessVersion(UIElementParent, _APIResouceProp, GamePreset, GameName, GameRegion); _GameSettings = new ZenlessSettings(_GameVersion); - _GameCache = null; + _GameCache = new ZenlessCache(UIElementParent, _GameVersion, _GameSettings as ZenlessSettings); _GameRepair = new ZenlessRepair(UIElementParent, _GameVersion, _GameSettings as ZenlessSettings); _GameInstall = new ZenlessInstall(UIElementParent, _GameVersion, _GameSettings as ZenlessSettings); break; diff --git a/CollapseLauncher/Classes/InstallManagement/GameConversionManagement.cs b/CollapseLauncher/Classes/InstallManagement/GameConversionManagement.cs index 445b9b9bf..a8ab0c68b 100644 --- a/CollapseLauncher/Classes/InstallManagement/GameConversionManagement.cs +++ b/CollapseLauncher/Classes/InstallManagement/GameConversionManagement.cs @@ -215,7 +215,7 @@ private List BuildManifest(List FileRemote }); } break; - case FileType.Blocks: + case FileType.Block: { _out.AddRange(BuildBlockManifest(Entry.BlkC, Entry.N)); } diff --git a/CollapseLauncher/Classes/Interfaces/Class/Enums.cs b/CollapseLauncher/Classes/Interfaces/Class/Enums.cs index 9c8033fd8..df3d3a116 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/Enums.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/Enums.cs @@ -2,7 +2,7 @@ { internal enum RepairAssetType { - General, + Generic, Block, BlockUpdate, Audio, diff --git a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs index c09953757..dfbc9fa0f 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs @@ -1093,6 +1093,38 @@ protected virtual async ValueTask CheckHashAsync(Stream stream, HashAlgo // Return computed hash byte return hashProvider.Hash; } + + + protected virtual async ValueTask CheckHashAsync(Stream stream, XxHash64 hashProvider, CancellationToken token, bool updateTotalProgress = true) + { + // Initialize Xxh64 instance and assign buffer + byte[] buffer = new byte[_bufferBigLength]; + + // Do read activity + int read; + while ((read = await stream!.ReadAsync(buffer, token)) > 0) + { + // Throw Cancellation exception if detected + token.ThrowIfCancellationRequested(); + + // Append buffer into hash block + hashProvider.Append(buffer.AsSpan(0, read)); + + lock (this) + { + // Increment total size counter + if (updateTotalProgress) _progressAllSizeCurrent += read; + // Increment per file size counter + _progressPerFileSizeCurrent += read; + } + + // Update status and progress for Xxh64 calculation + UpdateProgressCRC(); + } + + // Return computed hash byte + return hashProvider.GetHashAndReset(); + } #endregion #region PatchTools diff --git a/CollapseLauncher/Classes/RepairManagement/Genshin/Check.cs b/CollapseLauncher/Classes/RepairManagement/Genshin/Check.cs index 1708f2ca9..24250e6cf 100644 --- a/CollapseLauncher/Classes/RepairManagement/Genshin/Check.cs +++ b/CollapseLauncher/Classes/RepairManagement/Genshin/Check.cs @@ -201,7 +201,7 @@ private async ValueTask CheckAssetAllType(PkgVersionProperties asset, List AssetEntry.Add( new AssetProperty( Path.GetFileName(UsePersistent ? asset.remoteNamePersistent : asset.remoteName), - RepairAssetType.General, + RepairAssetType.Generic, Path.GetDirectoryName(UsePersistent ? asset.remoteNamePersistent : asset.remoteName), asset.fileSize, null, @@ -211,7 +211,7 @@ private async ValueTask CheckAssetAllType(PkgVersionProperties asset, List AssetEntry.Add( new AssetProperty( Path.GetFileName(asset.remoteName), - RepairAssetType.General, + RepairAssetType.Generic, Path.GetDirectoryName(asset.remoteName), asset.fileSize, localCRC, diff --git a/CollapseLauncher/Classes/RepairManagement/Genshin/Repair.cs b/CollapseLauncher/Classes/RepairManagement/Genshin/Repair.cs index 4dbfa2158..84140aca0 100644 --- a/CollapseLauncher/Classes/RepairManagement/Genshin/Repair.cs +++ b/CollapseLauncher/Classes/RepairManagement/Genshin/Repair.cs @@ -101,7 +101,7 @@ private async Task RepairAssetTypeGeneric((PkgVersionProperties AssetIndex, IAss // or start asset download task await RunDownloadTask(asset.AssetIndex.fileSize, assetPath, asset.AssetIndex.remoteURL, downloadClient, downloadProgress, token); - LogWriteLine($"File [T: {RepairAssetType.General}] {asset.AssetIndex.remoteName} has been downloaded!", LogType.Default, true); + LogWriteLine($"File [T: {RepairAssetType.Generic}] {asset.AssetIndex.remoteName} has been downloaded!", LogType.Default, true); } // Pop repair asset display entry diff --git a/CollapseLauncher/Classes/RepairManagement/Honkai/Check.cs b/CollapseLauncher/Classes/RepairManagement/Honkai/Check.cs index 38f64108d..7815c92da 100644 --- a/CollapseLauncher/Classes/RepairManagement/Honkai/Check.cs +++ b/CollapseLauncher/Classes/RepairManagement/Honkai/Check.cs @@ -51,7 +51,7 @@ private async Task Check(List assetIndex, CancellationToke // Assign a task depends on the asset type switch (asset.FT) { - case FileType.Blocks: + case FileType.Block: await CheckAssetTypeBlocks(asset, brokenAssetIndex, threadToken); break; case FileType.Audio: @@ -261,7 +261,7 @@ private async ValueTask CheckAssetTypeGeneric(FilePropertiesRemote asset, List AssetEntry.Add( new AssetProperty( Path.GetFileName(asset.N), - RepairAssetType.General, + RepairAssetType.Generic, Path.GetDirectoryName(asset.N), asset.S, null, @@ -303,7 +303,7 @@ private async ValueTask CheckAssetTypeGeneric(FilePropertiesRemote asset, List AssetEntry.Add( new AssetProperty( Path.GetFileName(asset.N), - asset.FT == FileType.Audio ? RepairAssetType.Audio : RepairAssetType.General, + asset.FT == FileType.Audio ? RepairAssetType.Audio : RepairAssetType.Generic, Path.GetDirectoryName(asset.N), asset.S, localCRC, @@ -559,7 +559,7 @@ private void BuildAssetIndexCatalog(List catalog, List assetIndex, Cancel private void DeserializeAssetIndexV2(Stream stream, List assetIndexList) { AssetIndexV2 assetIndex = new AssetIndexV2(); - PkgVersionProperties[] pkgVersionEntries = assetIndex.Deserialize(stream, out DateTime timestamp); - LogWriteLine($"[HonkaiRepair::DeserializeAssetIndexV2()] Asset index V2 has been deserialized with: {pkgVersionEntries!.Length} assets found." + + List pkgVersionEntries = assetIndex.Deserialize(stream, out DateTime timestamp); + LogWriteLine($"[HonkaiRepair::DeserializeAssetIndexV2()] Asset index V2 has been deserialized with: {pkgVersionEntries!.Count} assets found." + $"Asset index was generated at: {timestamp} (UTC)", LogType.Default, true); bool isOnlyRecoverMain = _isOnlyRecoverMain; @@ -737,7 +737,7 @@ private void BuildBlockIndex(List assetIndex, BlockPatchMa RN = CombineURLFromString(_blockAsbBaseURL, xmfParser.BlockEntry[i]!.HashString + ".wmv")!, S = xmfParser.BlockEntry[i]!.Size, CRC = xmfParser.BlockEntry[i]!.HashString!, - FT = FileType.Blocks, + FT = FileType.Block, BlockPatchInfo = blockPatchInfo }; diff --git a/CollapseLauncher/Classes/RepairManagement/Honkai/Repair.cs b/CollapseLauncher/Classes/RepairManagement/Honkai/Repair.cs index 43dac1eed..204b6c67c 100644 --- a/CollapseLauncher/Classes/RepairManagement/Honkai/Repair.cs +++ b/CollapseLauncher/Classes/RepairManagement/Honkai/Repair.cs @@ -59,7 +59,7 @@ await Parallel.ForEachAsync( // Assign a task depends on the asset type Task assetTask = asset.AssetIndex.FT switch { - FileType.Blocks => RepairAssetTypeBlocks(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), + FileType.Block => RepairAssetTypeBlocks(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), FileType.Audio => RepairOrPatchTypeAudio(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), FileType.Video => RepairAssetTypeVideo(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), _ => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken) @@ -83,7 +83,7 @@ await Parallel.ForEachAsync( // Assign a task depends on the asset type Task assetTask = asset.AssetIndex.FT switch { - FileType.Blocks => RepairAssetTypeBlocks(asset, downloadClient, _httpClient_RepairAssetProgress, token), + FileType.Block => RepairAssetTypeBlocks(asset, downloadClient, _httpClient_RepairAssetProgress, token), FileType.Audio => RepairOrPatchTypeAudio(asset, downloadClient, _httpClient_RepairAssetProgress, token), FileType.Video => RepairAssetTypeVideo(asset, downloadClient, _httpClient_RepairAssetProgress, token), _ => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token) @@ -151,7 +151,7 @@ private async Task RepairAssetTypeGeneric((FilePropertiesRemote AssetIndex, IAss _progressAllCountCurrent++; // Set repair activity status UpdateRepairStatus( - string.Format(asset.AssetIndex.FT == FileType.Blocks ? Lang._GameRepairPage.Status9 : Lang._GameRepairPage.Status8, asset.AssetIndex.FT == FileType.Blocks ? asset.AssetIndex.CRC : asset.AssetIndex.N), + string.Format(asset.AssetIndex.FT == FileType.Block ? Lang._GameRepairPage.Status9 : Lang._GameRepairPage.Status8, asset.AssetIndex.FT == FileType.Block ? asset.AssetIndex.CRC : asset.AssetIndex.N), string.Format(Lang._GameRepairPage.PerProgressSubtitle2, ConverterTool.SummarizeSizeSimple(_progressAllSizeCurrent), ConverterTool.SummarizeSizeSimple(_progressAllSizeTotal)), true); @@ -169,7 +169,7 @@ private async Task RepairAssetTypeGeneric((FilePropertiesRemote AssetIndex, IAss { // Start asset download task await RunDownloadTask(asset.AssetIndex.S, assetPath, assetURL, downloadClient, downloadProgress, token); - LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Blocks ? asset.AssetIndex.CRC : asset.AssetIndex.N)} has been downloaded!", LogType.Default, true); + LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Block ? asset.AssetIndex.CRC : asset.AssetIndex.N)} has been downloaded!", LogType.Default, true); } // Pop repair asset display entry diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Check.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Check.cs index 92e2ac066..08fbc22ea 100644 --- a/CollapseLauncher/Classes/RepairManagement/StarRail/Check.cs +++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Check.cs @@ -19,14 +19,14 @@ internal static string ReplaceStreamingToPersistentPath(string inputPath, string { string parentStreamingRelativePath = string.Format(type switch { - FileType.Blocks => StarRailRepair._assetGameBlocksStreamingPath, + FileType.Block => StarRailRepair._assetGameBlocksStreamingPath, FileType.Audio => StarRailRepair._assetGameAudioStreamingPath, FileType.Video => StarRailRepair._assetGameVideoStreamingPath, _ => string.Empty }, execName); string parentPersistentRelativePath = string.Format(type switch { - FileType.Blocks => StarRailRepair._assetGameBlocksPersistentPath, + FileType.Block => StarRailRepair._assetGameBlocksPersistentPath, FileType.Audio => StarRailRepair._assetGameAudioPersistentPath, FileType.Video => StarRailRepair._assetGameVideoPersistentPath, _ => string.Empty @@ -89,7 +89,7 @@ private async Task Check(List assetIndex, CancellationToke case FileType.Generic: await CheckGenericAssetType(asset, brokenAssetIndex, threadToken); break; - case FileType.Blocks: + case FileType.Block: await CheckAssetType(asset, brokenAssetIndex, threadToken); break; case FileType.Audio: diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs index 8931ea0f8..a97c3c4e3 100644 --- a/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Fetch.cs @@ -184,7 +184,7 @@ private async Task GetPrimaryManifest(DownloadClient downloadClient, Cancellatio { // Deserialize asset index and set it to list AssetIndexV2 parserTool = new AssetIndexV2(); - pkgVersion = new List(parserTool.Deserialize(stream, out DateTime timestamp)); + pkgVersion = parserTool.Deserialize(stream, out DateTime timestamp); LogWriteLine($"Asset index timestamp: {timestamp}", LogType.Default, true); } @@ -238,7 +238,7 @@ private FilePropertiesRemote GetNormalizedFilePropertyTypeBased(string remotePar }, typeAssetRelativeParentPath = string.Format(type switch { - FileType.Blocks => _assetGameBlocksStreamingPath, + FileType.Block => _assetGameBlocksStreamingPath, FileType.Audio => _assetGameAudioStreamingPath, FileType.Video => _assetGameVideoStreamingPath, _ => string.Empty @@ -418,8 +418,8 @@ private void CountAssetIndex(List assetIndex) private FileType ConvertFileTypeEnum(SRAssetType assetType) => assetType switch { - SRAssetType.Asb => FileType.Blocks, - SRAssetType.Block => FileType.Blocks, + SRAssetType.Asb => FileType.Block, + SRAssetType.Block => FileType.Block, SRAssetType.Audio => FileType.Audio, SRAssetType.Video => FileType.Video, _ => FileType.Generic @@ -427,10 +427,10 @@ private void CountAssetIndex(List assetIndex) private RepairAssetType ConvertRepairAssetTypeEnum(FileType assetType) => assetType switch { - FileType.Blocks => RepairAssetType.Block, + FileType.Block => RepairAssetType.Block, FileType.Audio => RepairAssetType.Audio, FileType.Video => RepairAssetType.Video, - _ => RepairAssetType.General + _ => RepairAssetType.Generic }; #endregion } diff --git a/CollapseLauncher/Classes/RepairManagement/StarRail/Repair.cs b/CollapseLauncher/Classes/RepairManagement/StarRail/Repair.cs index 32c1961ba..73647e1f3 100644 --- a/CollapseLauncher/Classes/RepairManagement/StarRail/Repair.cs +++ b/CollapseLauncher/Classes/RepairManagement/StarRail/Repair.cs @@ -58,7 +58,7 @@ await Parallel.ForEachAsync( // Assign a task depends on the asset type Task assetTask = asset.AssetIndex.FT switch { - FileType.Blocks => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), + FileType.Block => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), FileType.Audio => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), FileType.Video => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), _ => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken) @@ -82,7 +82,7 @@ await Parallel.ForEachAsync( // Assign a task depends on the asset type Task assetTask = asset.AssetIndex.FT switch { - FileType.Blocks => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), + FileType.Block => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), FileType.Audio => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), FileType.Video => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), _ => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token) @@ -115,7 +115,7 @@ private async Task RepairAssetTypeGeneric((FilePropertiesRemote AssetIndex, IAss { fileInfo.IsReadOnly = false; fileInfo.Delete(); - LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Blocks ? asset.AssetIndex.CRC : asset.AssetIndex.N)} deleted!", LogType.Default, true); + LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Block ? asset.AssetIndex.CRC : asset.AssetIndex.N)} deleted!", LogType.Default, true); } RemoveHashMarkFile(asset.AssetIndex.N, out _, out _); } @@ -123,7 +123,7 @@ private async Task RepairAssetTypeGeneric((FilePropertiesRemote AssetIndex, IAss { // Start asset download task await RunDownloadTask(asset.AssetIndex.S, asset.AssetIndex.N!, asset.AssetIndex.RN, downloadClient, downloadProgress, token); - LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Blocks ? asset.AssetIndex.CRC : asset.AssetIndex.N)} has been downloaded!", LogType.Default, true); + LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Block ? asset.AssetIndex.CRC : asset.AssetIndex.N)} has been downloaded!", LogType.Default, true); } // Pop repair asset display entry diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs index 9a4165aef..29e02bd92 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Hashing; using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using ZstdSharp.Unsafe; namespace CollapseLauncher { @@ -39,14 +41,17 @@ private async Task Check(List assetIndex, CancellationToke case FileType.Generic: await CheckGenericAssetType(asset, brokenAssetIndex, threadToken); break; - case FileType.Blocks: + case FileType.Block: // await CheckAssetType(asset, brokenAssetIndex, threadToken); + await CheckGenericAssetType(asset, brokenAssetIndex, threadToken); break; case FileType.Audio: // await CheckAssetType(asset, brokenAssetIndex, threadToken); + await CheckGenericAssetType(asset, brokenAssetIndex, threadToken); break; case FileType.Video: // await CheckAssetType(asset, brokenAssetIndex, threadToken); + await CheckGenericAssetType(asset, brokenAssetIndex, threadToken); break; } }); @@ -121,7 +126,7 @@ private async ValueTask CheckGenericAssetType(FilePropertiesRemote asset, List 8 ? await CheckHashAsync(filefs, MD5.Create(), token) : await CheckHashAsync(filefs, new XxHash64(), token); // If local and asset CRC doesn't match, then add the asset if (!IsArrayMatch(localCRC, asset.CRCArray)) @@ -151,10 +156,10 @@ private async ValueTask CheckGenericAssetType(FilePropertiesRemote asset, List assetType switch { - FileType.Blocks => RepairAssetType.Block, + FileType.Block => RepairAssetType.Block, FileType.Audio => RepairAssetType.Audio, FileType.Video => RepairAssetType.Video, - _ => RepairAssetType.General + _ => RepairAssetType.Generic }; } } diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs new file mode 100644 index 000000000..0cfaf1bb4 --- /dev/null +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs @@ -0,0 +1,399 @@ +using Hi3Helper.Data; +using Hi3Helper.EncTool.Parser.AssetIndex; +using Hi3Helper.EncTool.Parser.Sleepy; +using Hi3Helper.Shared.ClassStruct; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; + +#nullable enable +namespace CollapseLauncher +{ + [JsonSerializable(typeof(ZenlessResManifestAsset))] + [JsonSourceGenerationOptions(AllowOutOfOrderMetadataProperties = true, AllowTrailingCommas = true)] + internal partial class ZenlessManifestContext : JsonSerializerContext { } + + internal class ZenlessManifestInterceptStream : Stream + { + private Stream redirectStream; + private Stream innerStream; + private bool isFieldStart; + private bool isFieldEnd; + private byte[] innerBuffer = new byte[16 << 10]; + + internal ZenlessManifestInterceptStream(string? filePath, Stream stream) + { + innerStream = stream; + if (!string.IsNullOrWhiteSpace(filePath)) + { + string? filePathDir = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(filePathDir) && !Directory.Exists(filePathDir)) + { + Directory.CreateDirectory(filePathDir); + } + redirectStream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); + return; + } + redirectStream = Stream.Null; + } + + public override bool CanRead => true; + + public override bool CanSeek => throw new NotImplementedException(); + + public override bool CanWrite => throw new NotImplementedException(); + + public override long Length => throw new NotImplementedException(); + + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public override void Flush() + { + innerStream.Flush(); + redirectStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) => InternalRead(buffer.AsSpan(offset, count)); + + public override int Read(Span buffer) => InternalRead(buffer); + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await InternalReadAsync(buffer.AsMemory(offset, count), cancellationToken); + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + => await InternalReadAsync(buffer, cancellationToken); + + private readonly char[] searchStartValuesUtf16 = "\"files\": [".ToCharArray(); + private readonly byte[] searchStartValuesUtf8 = "\"files\": ["u8.ToArray(); + private readonly char[] searchEndValuesUtf16 = "]\r\n}".ToCharArray(); + private readonly byte[] searchEndValuesUtf8 = "]\r\n}"u8.ToArray(); + + private async ValueTask InternalReadAsync(Memory buffer, CancellationToken token) + { + if (isFieldEnd) + return 0; + + Start: + // - 8 is important to ensure that the EOF detection is working properly + int toRead = Math.Min(innerBuffer.Length - 8, buffer.Length); + int read = await innerStream.ReadAtLeastAsync(innerBuffer.AsMemory(0, toRead), toRead, false, token); + if (read == 0) + return 0; + + await redirectStream.WriteAsync(innerBuffer, 0, read, token); + + int lastIndexOffset = 0; + if (isFieldStart && ((lastIndexOffset = EnsureIsEnd(innerBuffer, read)) > 0)) + { + innerBuffer.AsSpan(0, lastIndexOffset).CopyTo(buffer.Span); + isFieldEnd = true; + return lastIndexOffset; + } + + int offset = 0; + if (!isFieldStart && !(isFieldStart = !((offset = EnsureIsStart(innerBuffer)) < 0))) + goto Start; + + bool isOneGoBufferLoadEnd = read < toRead && isFieldStart && !isFieldEnd; + ReadOnlySpan spanToCopy = isOneGoBufferLoadEnd ? innerBuffer.AsSpan(offset, read - offset).TrimEnd((byte)'}') + : innerBuffer.AsSpan(offset, read - offset); + + spanToCopy.CopyTo(buffer.Span); + return isOneGoBufferLoadEnd ? spanToCopy.Length : read - offset; + } + + private int InternalRead(Span buffer) + { + if (isFieldEnd) + return 0; + + Start: + // - 8 is important to ensure that the EOF detection is working properly + int toRead = Math.Min(innerBuffer.Length - 8, buffer.Length); + int read = innerStream.ReadAtLeast(innerBuffer.AsSpan(0, toRead), toRead, false); + if (read == 0) + return 0; + + redirectStream.Write(innerBuffer, 0, read); + + int lastIndexOffset = 0; + if (isFieldStart && ((lastIndexOffset = EnsureIsEnd(innerBuffer, read)) > 0)) + { + innerBuffer.AsSpan(0, lastIndexOffset).CopyTo(buffer); + isFieldEnd = true; + return lastIndexOffset; + } + + int offset = 0; + if (!isFieldStart && !(isFieldStart = !((offset = EnsureIsStart(innerBuffer)) < 0))) + goto Start; + + innerBuffer.AsSpan(offset, read - offset).CopyTo(buffer); + return read - offset; + } + + private int EnsureIsEnd(Span buffer, int read) + { + ReadOnlySpan bufferAsChars = MemoryMarshal.Cast(buffer); + + int lastIndexOfAnyUtf8 = buffer.LastIndexOf(searchEndValuesUtf8); + if (lastIndexOfAnyUtf8 < searchEndValuesUtf8.Length) + { + int lastIndexOfAnyUtf16 = bufferAsChars.LastIndexOf(searchEndValuesUtf16); + return lastIndexOfAnyUtf16 > 0 ? lastIndexOfAnyUtf16 + 1 : -1; + } + + return lastIndexOfAnyUtf8 + 1; + } + + private int EnsureIsStart(Span buffer) + { + ReadOnlySpan bufferAsChars = MemoryMarshal.Cast(buffer); + + int indexOfAnyUtf8 = buffer.IndexOf(searchStartValuesUtf8); + if (indexOfAnyUtf8 < searchStartValuesUtf8.Length) + { + int indexOfAnyUtf16 = bufferAsChars.IndexOf(searchStartValuesUtf16); + return indexOfAnyUtf16 > 0 ? indexOfAnyUtf16 + (searchStartValuesUtf16.Length - 1) : -1; + } + + return indexOfAnyUtf8 + (searchStartValuesUtf8.Length - 1); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override async ValueTask DisposeAsync() + { + if (innerStream != null) + await innerStream.DisposeAsync(); + + if (redirectStream != null) + await redirectStream.DisposeAsync(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + innerStream?.Dispose(); + redirectStream?.Dispose(); + } + } + } + + internal static class ZenlessRepairExtensions + { + const string StreamingAssetsPath = "StreamingAssets\\"; + const string AssetTypeAudioPath = StreamingAssetsPath + "Audio\\Windows\\"; + const string AssetTypeBlockPath = StreamingAssetsPath + "Blocks\\"; + const string AssetTypeVideoPath = StreamingAssetsPath + "Video\\HD\\"; + + const string PersistentAssetsPath = "Persistent\\"; + const string AssetTypeAudioPersistentPath = PersistentAssetsPath + "Audio\\Windows\\"; + const string AssetTypeBlockPersistentPath = PersistentAssetsPath + "Blocks\\"; + const string AssetTypeVideoPersistentPath = PersistentAssetsPath + "Video\\HD\\"; + + internal static async IAsyncEnumerable MergeAsyncEnumerable(params IAsyncEnumerable[] sources) + { + foreach (IAsyncEnumerable enumerable in sources) + { + await foreach (T? item in enumerable) + { + yield return item; + } + } + } + + internal static async IAsyncEnumerable RegisterSleepyFileInfoToManifest( + this SleepyFileInfoResult fileInfo, + HttpClient httpClient, + List assetIndex, + bool needWriteToLocal, + string persistentPath, + [EnumeratorCancellation] CancellationToken token = default) + { + string manifestFileUrl = ConverterTool.CombineURLFromString(fileInfo.BaseUrl, fileInfo.ReferenceFileInfo.FileName); + using HttpResponseMessage responseMessage = await httpClient.GetAsync(manifestFileUrl, HttpCompletionOption.ResponseHeadersRead, token); + + string filePath = Path.Combine(persistentPath, fileInfo.ReferenceFileInfo.FileName + "_persist"); + + await using Stream responseStream = await responseMessage.Content.ReadAsStreamAsync(token); + await using Stream responseInterceptedStream = new ZenlessManifestInterceptStream(needWriteToLocal ? filePath : null, responseStream); + + IAsyncEnumerable enumerable = JsonSerializer + .DeserializeAsyncEnumerable( + responseInterceptedStream, + ZenlessManifestContext.Default.ZenlessResManifestAsset, + false, + token + ); + + await foreach (ZenlessResManifestAsset? manifest in enumerable) + { + if (manifest == null) + { + continue; + } + + yield return new PkgVersionProperties + { + fileSize = manifest.FileSize, + isForceStoreInPersistent = manifest.IsPersistentFile, + isPatch = manifest.IsPersistentFile, + md5 = Convert.ToHexStringLower(manifest.Xxh64Hash), + remoteName = manifest.FileRelativePath + }; + } + } + + internal static IEnumerable RegisterMainCategorizedAssetsToHashSet(this IEnumerable assetEnumerable, List assetIndex, Dictionary hashSet, string baseLocalPath, string baseUrl) + { + foreach (PkgVersionProperties asset in assetEnumerable) + { + yield return ReturnCategorizedYieldValue(hashSet, assetIndex, asset, baseLocalPath, baseUrl); + } + } + + internal static async IAsyncEnumerable RegisterResCategorizedAssetsToHashSetAsync(this IAsyncEnumerable assetEnumerable, List assetIndex, Dictionary hashSet, string baseLocalPath, string baseUrl) + { + await foreach (PkgVersionProperties asset in assetEnumerable) + { + string baseLocalPathMerged = Path.Combine(baseLocalPath, asset.isPatch ? PersistentAssetsPath : StreamingAssetsPath); + + yield return ReturnCategorizedYieldValue(hashSet, assetIndex, asset, baseLocalPathMerged, baseUrl); + } + } + + private static FilePropertiesRemote? ReturnCategorizedYieldValue(Dictionary hashSet, List assetIndex, PkgVersionProperties asset, string baseLocalPath, string baseUrl) + { + FilePropertiesRemote asRemoteProperty = GetNormalizedFilePropertyTypeBased( + baseUrl, + baseLocalPath, + asset.remoteName, + asset.fileSize, + asset.md5, + FileType.Generic, + asset.isPatch); + + ReadOnlySpan relTypeRelativePath = asRemoteProperty.GetAssetRelativePath(out RepairAssetType assetType); + asRemoteProperty.FT = assetType switch + { + RepairAssetType.Audio => FileType.Audio, + RepairAssetType.Block => FileType.Block, + RepairAssetType.Video => FileType.Video, + _ => FileType.Generic + }; + + if (!relTypeRelativePath.IsEmpty) + { + string relTypePath = assetType switch + { + RepairAssetType.Audio => AssetTypeAudioPath, + RepairAssetType.Block => AssetTypeBlockPath, + RepairAssetType.Video => AssetTypeVideoPath, + _ => throw new NotSupportedException() + }; + + string relTypeRelativePathStr = relTypeRelativePath.ToString(); + if (!hashSet.TryAdd(relTypeRelativePathStr, asRemoteProperty)) + { + FilePropertiesRemote existingValue = hashSet[relTypeRelativePathStr]; + int indexOf = assetIndex.IndexOf(existingValue); + if (indexOf < -1) + return asRemoteProperty; + + assetIndex[indexOf] = asRemoteProperty; + hashSet[relTypeRelativePathStr] = asRemoteProperty; + + return null; + } + } + + return asRemoteProperty; + } + + private static FilePropertiesRemote GetNormalizedFilePropertyTypeBased(string remoteParentURL, + string baseLocalPath, + string remoteRelativePath, + long fileSize, + string hash, + FileType type = FileType.Generic, + bool isPatchApplicable = false) + { + string remoteAbsolutePath = type switch + { + FileType.Generic => ConverterTool.CombineURLFromString(remoteParentURL, remoteRelativePath), + _ => remoteParentURL + }; + string localAbsolutePath = Path.Combine(baseLocalPath, ConverterTool.NormalizePath(remoteRelativePath)); + + return new FilePropertiesRemote + { + FT = type, + CRC = hash, + S = fileSize, + N = localAbsolutePath, + RN = remoteAbsolutePath, + IsPatchApplicable = isPatchApplicable + }; + } + + internal static ReadOnlySpan GetAssetRelativePath(this FilePropertiesRemote asset, out RepairAssetType assetType) + { + assetType = RepairAssetType.Generic; + + int indexOfOffset = -1; + if ((indexOfOffset = asset.N.LastIndexOf(AssetTypeAudioPath, StringComparison.OrdinalIgnoreCase)) >= 0) + { + assetType = RepairAssetType.Audio; + } + else if ((indexOfOffset = asset.N.LastIndexOf(AssetTypeBlockPath, StringComparison.OrdinalIgnoreCase)) >= 0) + { + assetType = RepairAssetType.Block; + } + else if ((indexOfOffset = asset.N.LastIndexOf(AssetTypeVideoPath, StringComparison.OrdinalIgnoreCase)) >= 0) + { + assetType = RepairAssetType.Video; + } + else if ((indexOfOffset = asset.N.LastIndexOf(AssetTypeAudioPersistentPath, StringComparison.OrdinalIgnoreCase)) >= 0) + { + assetType = RepairAssetType.Audio; + } + else if ((indexOfOffset = asset.N.LastIndexOf(AssetTypeBlockPersistentPath, StringComparison.OrdinalIgnoreCase)) >= 0) + { + assetType = RepairAssetType.Block; + } + else if ((indexOfOffset = asset.N.LastIndexOf(AssetTypeVideoPersistentPath, StringComparison.OrdinalIgnoreCase)) >= 0) + { + assetType = RepairAssetType.Video; + } + + if (indexOfOffset >= 0) + { + return asset.N.AsSpan(indexOfOffset); + } + + return ReadOnlySpan.Empty; + } + } +} diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs index deffc2f77..b3247662c 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -1,13 +1,14 @@ using CollapseLauncher.Helper; using CollapseLauncher.Helper.Metadata; using Hi3Helper; -using Hi3Helper.Data; using Hi3Helper.EncTool.Parser.AssetIndex; using Hi3Helper.EncTool.Parser.Sleepy; using Hi3Helper.Http; using Hi3Helper.Shared.ClassStruct; using Hi3Helper.Shared.Region; using System; +using System.Buffers; +using System.Buffers.Text; using System.Collections.Generic; using System.Data; using System.IO; @@ -40,22 +41,34 @@ private async Task Fetch(List assetIndex, CancellationToke // Initialize the new DownloadClient DownloadClient downloadClient = DownloadClient.CreateInstance(client); + // Create a hash set to overwrite local files + Dictionary hashSet = new Dictionary(StringComparer.OrdinalIgnoreCase); + try { - // Get the primary manifest - await GetPrimaryManifest(downloadClient, token, assetIndex); - - // Get the in-game res manifest - await GetResManifest(downloadClient, token, assetIndex); - - // Force-Fetch the Bilibili SDK (if exist :pepehands:) - await FetchBilibiliSDK(token); + // If not in cache mode, then fetch main package + if (!IsCacheUpdateMode) + { + // Get the primary manifest + await GetPrimaryManifest(downloadClient, hashSet, token, assetIndex); + } - // Remove plugin from assetIndex - // Skip the removal for Delta-Patch + // Execute on non-recover main mode if (!IsOnlyRecoverMain) { - EliminatePluginAssetIndex(assetIndex); + // Get the in-game res manifest + await GetResManifest(downloadClient, hashSet, token, assetIndex); + + // Execute plugin things if not in cache mode only + if (!IsCacheUpdateMode) + { + // Force-Fetch the Bilibili SDK (if exist :pepehands:) + await FetchBilibiliSDK(token); + + // Remove plugin from assetIndex + // Skip the removal for Delta-Patch + EliminatePluginAssetIndex(assetIndex); + } } } finally @@ -69,8 +82,13 @@ private async Task Fetch(List assetIndex, CancellationToke #endregion #region PrimaryManifest - private async Task GetPrimaryManifest(DownloadClient downloadClient, CancellationToken token, List assetIndex) + private async Task GetPrimaryManifest(DownloadClient downloadClient, Dictionary hashSet, CancellationToken token, List assetIndex) { + // If it's using cache update mode, then return since we don't need to add manifest + // from pkg_version on cache update mode. + if (IsCacheUpdateMode) + return; + // Initialize pkgVersion list List pkgVersion = new List(); @@ -102,12 +120,18 @@ private async Task GetPrimaryManifest(DownloadClient downloadClient, Cancellatio { // Deserialize asset index and set it to list AssetIndexV2 parserTool = new AssetIndexV2(); - pkgVersion = new List(parserTool.Deserialize(stream, out DateTime timestamp)); - Logger.LogWriteLine($"Asset index timestamp: {timestamp}", LogType.Default, true); + pkgVersion = await Task.Run(() => parserTool.Deserialize(stream, out DateTime timestamp)).ConfigureAwait(false); } // Convert the pkg version list to asset index - ConvertPkgVersionToAssetIndex(pkgVersion, assetIndex); + foreach (FilePropertiesRemote entry in pkgVersion.RegisterMainCategorizedAssetsToHashSet(assetIndex, hashSet, _gamePath, _gameRepoURL)) + { + // If entry is null (means, an existing entry has been overwritten), then next + if (entry == null) + continue; + + assetIndex.Add(entry); + } // Clear the pkg version list pkgVersion.Clear(); @@ -125,38 +149,177 @@ private async Task> FetchMetadata(CancellationToken t #endregion #region ResManifest - private async Task GetResManifest(DownloadClient downloadClient, CancellationToken token, List assetIndex) + private async Task GetResManifest(DownloadClient downloadClient, Dictionary hashSet, CancellationToken token, List assetIndex) { + // Create sleepy property PresetConfig gamePreset = GameVersionManagerCast.GamePreset; - SleepyProperty sleepyProperty = SleepyProperty.Create( - GameVersionManagerCast.GetGameExistingVersion()?.VersionString, - gamePreset.GameDispatchChannelName, - gamePreset.GameDispatchURL, - gamePreset.GameDispatchURLTemplate, - GameSettings?.GeneralData?.SelectedServerName ?? gamePreset.GameDispatchDefaultName, - gamePreset.GameGatewayURLTemplate, - gamePreset.ProtoDispatchKey, - SleepyBuildProperty.Create(GameVersionManagerCast.SleepyIdentity, GameVersionManagerCast.SleepyArea) - ); + HttpClient client = downloadClient.GetHttpClient(); + + // Get sleepy info + SleepyInfo sleepyInfo = await TryGetSleepyInfo( + client, + gamePreset, + GameSettings.GeneralData.SelectedServerName, + gamePreset.GameDispatchDefaultName, + token); + + // Get persistent path + string persistentPath = GameDataPersistentPath; + + // Fetch cache files + if (IsCacheUpdateMode) + { + SleepyFileInfoResult infoKindSilence = sleepyInfo.GetFileInfoResult(FileInfoKind.Silence); + SleepyFileInfoResult infoKindData = sleepyInfo.GetFileInfoResult(FileInfoKind.Data); + + IAsyncEnumerable infoSilenceEnumerable = EnumerateResManifestToAssetIndexAsync( + infoKindSilence.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + infoKindSilence.BaseUrl); + + IAsyncEnumerable infoDataEnumerable = EnumerateResManifestToAssetIndexAsync( + infoKindData.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + infoKindData.BaseUrl); + + await foreach (FilePropertiesRemote asset in ZenlessRepairExtensions + .MergeAsyncEnumerable(infoSilenceEnumerable, infoDataEnumerable)) + { + assetIndex.Add(asset); + } + } + // Fetch repair files + else + { + SleepyFileInfoResult infoKindRes = sleepyInfo.GetFileInfoResult(FileInfoKind.Res); + SleepyFileInfoResult infoKindAudio = sleepyInfo.GetFileInfoResult(FileInfoKind.Audio); + + IAsyncEnumerable infoResEnumerable = EnumerateResManifestToAssetIndexAsync( + infoKindRes.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + infoKindRes.BaseUrl); + + IAsyncEnumerable infoAudioEnumerable = GetOnlyInstalledAudioPack( + EnumerateResManifestToAssetIndexAsync( + infoKindAudio.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + infoKindAudio.BaseUrl) + ); + + await foreach (FilePropertiesRemote asset in ZenlessRepairExtensions + .MergeAsyncEnumerable(infoResEnumerable, infoAudioEnumerable)) + { + assetIndex.Add(asset); + } + } + } + + private async Task TryGetSleepyInfo(HttpClient client, PresetConfig gamePreset, string targetServerName, string fallbackServerName = null, CancellationToken token = default) + { + try + { + // Initialize property + SleepyProperty sleepyProperty = SleepyProperty.Create( + GameVersionManagerCast.GetGameExistingVersion()?.VersionString, + gamePreset.GameDispatchChannelName, + gamePreset.GameDispatchArrayURL[Random.Shared.Next(0, gamePreset.GameDispatchArrayURL.Count - 1)], + gamePreset.GameDispatchURLTemplate, + targetServerName ?? fallbackServerName, + gamePreset.GameGatewayURLTemplate, + gamePreset.ProtoDispatchKey, + SleepyBuildProperty.Create(GameVersionManagerCast.SleepyIdentity, GameVersionManagerCast.SleepyArea) + ); + + // Create sleepy instance + SleepyInfo sleepyInfoReturn = SleepyInfo.CreateSleepyInfo( + client, + GameVersionManagerCast.SleepyInstance, + sleepyProperty + ); + + // Initialize sleepy instance before using + await sleepyInfoReturn.Initialize(token); + + // Return SleepyInfo + return sleepyInfoReturn; + } + catch (TaskCanceledException) { throw; } + catch (OperationCanceledException) { throw; } + catch (Exception) when (!string.IsNullOrEmpty(fallbackServerName)) + { + // If it fails, try to get the SleepyInfo from fallback server name + return await TryGetSleepyInfo(client, gamePreset, fallbackServerName, null, token); + } + } + + private async IAsyncEnumerable GetOnlyInstalledAudioPack(IAsyncEnumerable enumerable) + { + const string WindowsFullPath = "Audio\\Windows\\Full"; + string[] audioList = GetCurrentAudioLangList("Jp"); + SearchValues searchExclude = SearchValues.Create(audioList, StringComparison.OrdinalIgnoreCase); + + await foreach (FilePropertiesRemote asset in enumerable) + { + if (IsAudioFileIncluded(WindowsFullPath, searchExclude, asset)) + yield return asset; + } + } + + private static bool IsAudioFileIncluded(string WindowsFullPath, SearchValues searchInclude, FilePropertiesRemote asset) + { + // If it's not audio file, then return + ReadOnlySpan relPath = asset.GetAssetRelativePath(out RepairAssetType assetType); + if (relPath.IsEmpty || assetType != RepairAssetType.Audio) + return true; + + ReadOnlySpan dirRelPath = Path.GetDirectoryName(relPath); + // If non language audio file, then return + if (dirRelPath.EndsWith(WindowsFullPath)) + return true; + + // Check if non full path, then return + int indexOf = dirRelPath.LastIndexOf(WindowsFullPath); + if (indexOf < 0) + return true; + + // If the index is more than WindowsFullPath.Length (included), then return + ReadOnlySpan lastSequence = Path.GetFileName(dirRelPath); + int indexOfAny = lastSequence.IndexOfAny(searchInclude); + if (indexOfAny == 0) + return true; + + // Otherwise, return false + return false; } #endregion #region Utilities - private void ConvertPkgVersionToAssetIndex(List pkgVersion, List assetIndex) +#nullable enable + private async IAsyncEnumerable EnumerateResManifestToAssetIndexAsync( + IAsyncEnumerable pkgVersion, + List assetIndex, + Dictionary hashSet, + string baseLocalPath, + string baseUrl) { - foreach (PkgVersionProperties entry in pkgVersion) - { - // Add the pkgVersion entry to asset index - FilePropertiesRemote normalizedProperty = GetNormalizedFilePropertyTypeBased( - _gameRepoURL, - entry.remoteName, - entry.fileSize, - entry.md5, - FileType.Generic, - true); - assetIndex.AddSanitize(normalizedProperty); + await foreach (FilePropertiesRemote? entry in pkgVersion.RegisterResCategorizedAssetsToHashSetAsync(assetIndex, hashSet, baseLocalPath, baseUrl)) + { + // If entry is null (means, an existing entry has been overwritten), then next + if (entry == null) + continue; + + yield return entry; } } +#nullable restore private void EliminatePluginAssetIndex(List assetIndex) { @@ -169,39 +332,23 @@ private void EliminatePluginAssetIndex(List assetIndex) }); } - private FilePropertiesRemote GetNormalizedFilePropertyTypeBased(string remoteParentURL, - string remoteRelativePath, - long fileSize, - string hash, - FileType type = FileType.Generic, - bool isPatchApplicable = false, - bool isHasHashMark = false) - { - string remoteAbsolutePath = type switch - { - FileType.Generic => ConverterTool.CombineURLFromString(remoteParentURL, remoteRelativePath), - _ => remoteParentURL - }; - var localAbsolutePath = Path.Combine(_gamePath, ConverterTool.NormalizePath(remoteRelativePath)); - - return new FilePropertiesRemote - { - FT = type, - CRC = hash, - S = fileSize, - N = localAbsolutePath, - RN = remoteAbsolutePath, - IsPatchApplicable = isPatchApplicable, - IsHasHashMark = isHasHashMark, - }; - } - private string[] GetCurrentAudioLangList(string fallbackCurrentLangname) { // Initialize the variable. string audioLangListPath = _gameAudioLangListPath; string audioLangListPathStatic = _gameAudioLangListPathStatic; + string audioLangListPathAlternative = _gameAudioLangListPathAlternate; + string audioLangListPathAlternativeStatic = _gameAudioLangListPathAlternateStatic; + string[] returnValue; + string fallbackCurrentLangnameNative = fallbackCurrentLangname switch + { + "Jp" => "Japanese", + "En" => "English(US)", + "Cn" => "Chinese", + "Kr" => "Korean", + _ => throw new NotSupportedException() + }; // Check if the audioLangListPath is null or the file is not exist, // then create a new one from the fallback value @@ -226,8 +373,33 @@ private string[] GetCurrentAudioLangList(string fallbackCurrentLangname) File.WriteAllLines(audioLangListPathStatic, returnValue); } + string[] returnValueAlternate; + + // Check if the audioLangListPathAlternative is null or the file is not exist, + // then create a new one from the fallback value + if (audioLangListPathAlternative == null || !File.Exists(audioLangListPathAlternativeStatic)) + { + // Try check if the folder is exist. If not, create one. + string audioLangPathDir = Path.GetDirectoryName(audioLangListPathAlternativeStatic); + if (Directory.Exists(audioLangPathDir)) + Directory.CreateDirectory(audioLangPathDir); + + // Assign the default value and write to the file, then return. + returnValueAlternate = new string[] { fallbackCurrentLangnameNative }; + File.WriteAllLines(audioLangListPathAlternativeStatic, returnValueAlternate); + return returnValueAlternate; + } + + // Read all the lines. If empty, then assign the default value and rewrite it + returnValueAlternate = File.ReadAllLines(audioLangListPathAlternativeStatic); + if (returnValueAlternate.Length == 0) + { + returnValueAlternate = new string[] { fallbackCurrentLangnameNative }; + File.WriteAllLines(audioLangListPathAlternativeStatic, returnValueAlternate); + } + // Return the value - return returnValue; + return returnValueAlternate; } private void CountAssetIndex(List assetIndex) diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Repair.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Repair.cs index 4c9a626b2..70e0eccea 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Repair.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Repair.cs @@ -1,18 +1,15 @@ using CollapseLauncher.Helper; +using Hi3Helper; +using Hi3Helper.Data; using Hi3Helper.Http; using Hi3Helper.Shared.ClassStruct; -using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; -using System.Net.Http; +using System.IO; using System.Net; -using System.Text; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Hi3Helper; -using Hi3Helper.Data; -using System.IO; namespace CollapseLauncher { @@ -59,7 +56,7 @@ await Parallel.ForEachAsync( // Assign a task depends on the asset type Task assetTask = asset.AssetIndex.FT switch { - FileType.Blocks => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), + FileType.Block => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), FileType.Audio => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), FileType.Video => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken), _ => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, innerToken) @@ -83,7 +80,7 @@ await Parallel.ForEachAsync( // Assign a task depends on the asset type Task assetTask = asset.AssetIndex.FT switch { - FileType.Blocks => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), + FileType.Block => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), FileType.Audio => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), FileType.Video => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token), _ => RepairAssetTypeGeneric(asset, downloadClient, _httpClient_RepairAssetProgress, token) @@ -116,14 +113,14 @@ private async Task RepairAssetTypeGeneric((FilePropertiesRemote AssetIndex, IAss { fileInfo.IsReadOnly = false; fileInfo.Delete(); - Logger.LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Blocks ? asset.AssetIndex.CRC : asset.AssetIndex.N)} deleted!", LogType.Default, true); + Logger.LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Block ? asset.AssetIndex.CRC : asset.AssetIndex.N)} deleted!", LogType.Default, true); } } else { // Start asset download task await RunDownloadTask(asset.AssetIndex.S, asset.AssetIndex.N!, asset.AssetIndex.RN, downloadClient, downloadProgress, token); - Logger.LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Blocks ? asset.AssetIndex.CRC : asset.AssetIndex.N)} has been downloaded!", LogType.Default, true); + Logger.LogWriteLine($"File [T: {asset.AssetIndex.FT}] {(asset.AssetIndex.FT == FileType.Block ? asset.AssetIndex.CRC : asset.AssetIndex.N)} has been downloaded!", LogType.Default, true); } // Pop repair asset display entry diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs index 1cf2845a4..681daa3e0 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs @@ -3,7 +3,6 @@ using CollapseLauncher.Interfaces; using Hi3Helper; using Hi3Helper.Data; -using Hi3Helper.EncTool.Parser.AssetIndex; using Hi3Helper.Shared.ClassStruct; using Microsoft.UI.Xaml; using System; @@ -21,6 +20,7 @@ internal partial class ZenlessRepair : ProgressBase, IRepa internal const string _assetGameStreamingPath = @"{0}_Data\StreamingAssets"; private bool IsOnlyRecoverMain { get; set; } + private bool IsCacheUpdateMode { get; set; } private string? ExecutableName { get; set; } private List? OriginAssetIndex { get; set; } @@ -65,11 +65,13 @@ private string? _gameAudioLangListPathAlternate protected override string _userAgent { get; set; } = "UnityPlayer/2019.4.40f1 (UnityWebRequest/1.0, libcurl/7.80.0-DEV)"; - public ZenlessRepair(UIElement parentUI, IGameVersionCheck gameVersionManager, ZenlessSettings gameSettings, bool isOnlyRecoverMain = false, string? versionOverride = null) + public ZenlessRepair(UIElement parentUI, IGameVersionCheck gameVersionManager, ZenlessSettings gameSettings, bool isOnlyRecoverMain = false, string? versionOverride = null, bool isCacheUpdateMode = false) : base(parentUI, gameVersionManager, null, "", versionOverride) { // Use IsOnlyRecoverMain for future delta-patch or main game only files IsOnlyRecoverMain = isOnlyRecoverMain; + // We are merging cache functionality with cache update + IsCacheUpdateMode = isCacheUpdateMode; ExecutableName = Path.GetFileNameWithoutExtension(gameVersionManager.GamePreset.GameExecutableName); GameSettings = gameSettings; } @@ -87,7 +89,7 @@ public async Task StartCheckRoutine(bool useFastCheck) return await TryRunExamineThrow(CheckRoutine()); } - public async Task StartRepairRoutine(bool showInteractivePrompt = false, Action actionIfInteractiveCancel = null) + public async Task StartRepairRoutine(bool showInteractivePrompt = false, Action? actionIfInteractiveCancel = null) { if (_assetIndex.Count == 0) throw new InvalidOperationException("There's no broken file being reported! You can't do the repair process!"); diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessResManifest.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessResManifest.cs new file mode 100644 index 000000000..5c2e7291b --- /dev/null +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessResManifest.cs @@ -0,0 +1,35 @@ +using Hi3Helper.EncTool.Parser.Sleepy.JsonConverters; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace CollapseLauncher +{ + internal class ZenlessResManifest + { + [JsonPropertyName("remoteParentDir")] + public string ParentPath { get; set; } + + [JsonPropertyName("files")] + public IAsyncEnumerable AssetList { get; set; } + } + + internal class ZenlessResManifestAsset + { + [JsonPropertyName("remoteName")] + public string FileRelativePath { get; set; } + + [JsonPropertyName("md5")] // "mD5" they said. BROO, IT'S A F**KING XXH64 HASH!!!! + [JsonConverter(typeof(NumberStringToXxh64HashBytesConverter))] // AND THEY STORED IT AS A NUMBER IN A STRING WTFF?????? + public byte[] Xxh64Hash { get; set; } + + [JsonPropertyName("fileSize")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public long FileSize { get; set; } + + [JsonPropertyName("isPatch")] + public bool IsPersistentFile { get; set; } + + [JsonPropertyName("tags")] + public int[] Tags { get; set; } + } +} From bfee12c4ccb27000b2def1bb372a23f49cf574b6 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Thu, 10 Oct 2024 03:06:17 +0700 Subject: [PATCH 07/18] Fix missing Enum adjustment --- .../Classes/Shared/ClassStruct/Class/GameDataStructure.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hi3Helper.Core/Classes/Shared/ClassStruct/Class/GameDataStructure.cs b/Hi3Helper.Core/Classes/Shared/ClassStruct/Class/GameDataStructure.cs index 86cc2e615..da49f1f57 100644 --- a/Hi3Helper.Core/Classes/Shared/ClassStruct/Class/GameDataStructure.cs +++ b/Hi3Helper.Core/Classes/Shared/ClassStruct/Class/GameDataStructure.cs @@ -8,7 +8,7 @@ namespace Hi3Helper.Shared.ClassStruct { [JsonConverter(typeof(JsonStringEnumConverter))] - public enum FileType : byte { Generic, Blocks, Audio, Video, Unused } + public enum FileType : byte { Generic, Block, Audio, Video, Unused } public class FilePropertiesRemote : IAssetIndexSummary { public bool IsUsed { get; set; } From 7ba3e28abb92a2742b6db668db6f56759d98ad0d Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Thu, 10 Oct 2024 03:06:48 +0700 Subject: [PATCH 08/18] Update NuGet-gut + Submodule --- CollapseLauncher/CollapseLauncher.csproj | 2 +- CollapseLauncher/packages.lock.json | 20 +++++++++---------- H.NotifyIcon | 2 +- ...Toolkit.WinUI.Controls.ImageCropper.csproj | 4 ++-- .../ImageCropper/packages.lock.json | 18 ++++++++--------- ...kit.WinUI.Controls.SettingsControls.csproj | 4 ++-- .../SettingsControls/packages.lock.json | 18 ++++++++--------- Hi3Helper.Core/Hi3Helper.Core.csproj | 2 +- Hi3Helper.Core/packages.lock.json | 6 +++--- Hi3Helper.EncTool | 2 +- ImageEx | 2 +- 11 files changed, 40 insertions(+), 40 deletions(-) diff --git a/CollapseLauncher/CollapseLauncher.csproj b/CollapseLauncher/CollapseLauncher.csproj index d15194ef2..9a5e765fb 100644 --- a/CollapseLauncher/CollapseLauncher.csproj +++ b/CollapseLauncher/CollapseLauncher.csproj @@ -113,7 +113,7 @@ - + diff --git a/CollapseLauncher/packages.lock.json b/CollapseLauncher/packages.lock.json index 847cc804d..9065700fc 100644 --- a/CollapseLauncher/packages.lock.json +++ b/CollapseLauncher/packages.lock.json @@ -140,9 +140,9 @@ }, "Microsoft.Windows.CsWinRT": { "type": "Direct", - "requested": "[2.1.3, )", - "resolved": "2.1.3", - "contentHash": "Nl8A4rQ4l2GNj703GvLSbr0Vo++FjxKxU7CIj1pcKz/sN8XSvD4dIvUCYYgD16o2pG4PSSXNgAxfwDUwLGHLPA==" + "requested": "[2.1.5, )", + "resolved": "2.1.5", + "contentHash": "PG0uVrpPTVEhqu70YhGMTyRKZXNgygjIIwdjAmg2hhHkmm6367TafBEdzIU/TgMXy2+x5Lv/Z9MKJehwkQXEvw==" }, "Microsoft.Windows.SDK.BuildTools": { "type": "Direct", @@ -392,7 +392,7 @@ "type": "Project", "dependencies": { "H.NotifyIcon": "[1.0.0, )", - "Microsoft.Web.WebView2": "[1.0.2783-prerelease, )", + "Microsoft.Web.WebView2": "[1.0.2839-prerelease, )", "Microsoft.Windows.SDK.BuildTools": "[10.0.26100.1742, )", "Microsoft.WindowsAppSDK": "[1.6.240923002, )" } @@ -401,7 +401,7 @@ "type": "Project", "dependencies": { "Hi3Helper.EncTool": "[1.0.0, )", - "Microsoft.Windows.CsWinRT": "[2.1.3, )" + "Microsoft.Windows.CsWinRT": "[2.1.5, )" } }, "hi3helper.enctool": { @@ -429,8 +429,8 @@ "CommunityToolkit.WinUI.Extensions": "[8.1.240916, )", "CommunityToolkit.WinUI.Media": "[8.1.240916, )", "Microsoft.Graphics.Win2D": "[1.2.1-experimental2, )", - "Microsoft.Web.WebView2": "[1.0.2783-prerelease, )", - "Microsoft.Windows.CsWinRT": "[2.1.3, )", + "Microsoft.Web.WebView2": "[1.0.2839-prerelease, )", + "Microsoft.Windows.CsWinRT": "[2.1.5, )", "Microsoft.Windows.SDK.BuildTools": "[10.0.26100.1742, )", "Microsoft.WindowsAppSDK": "[1.6.240923002, )" } @@ -439,7 +439,7 @@ "type": "Project", "dependencies": { "CommunityToolkit.WinUI.Extensions": "[8.1.240916, )", - "Microsoft.Web.WebView2": "[1.0.2783-prerelease, )", + "Microsoft.Web.WebView2": "[1.0.2839-prerelease, )", "Microsoft.Windows.SDK.BuildTools": "[10.0.26100.1742, )", "Microsoft.WindowsAppSDK": "[1.6.240923002, )" } @@ -454,8 +454,8 @@ "type": "Project", "dependencies": { "CommunityToolkit.WinUI.Triggers": "[8.1.240916, )", - "Microsoft.Web.WebView2": "[1.0.2783-prerelease, )", - "Microsoft.Windows.CsWinRT": "[2.1.3, )", + "Microsoft.Web.WebView2": "[1.0.2839-prerelease, )", + "Microsoft.Windows.CsWinRT": "[2.1.5, )", "Microsoft.Windows.SDK.BuildTools": "[10.0.26100.1742, )", "Microsoft.WindowsAppSDK": "[1.6.240923002, )" } diff --git a/H.NotifyIcon b/H.NotifyIcon index 81880d59c..82f035bdf 160000 --- a/H.NotifyIcon +++ b/H.NotifyIcon @@ -1 +1 @@ -Subproject commit 81880d59cfce41fe9a0f6bed1fbc13cc973e2bf0 +Subproject commit 82f035bdfc3c4ba95cb32831b1e3f027f171e352 diff --git a/Hi3Helper.CommunityToolkit/ImageCropper/Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper.csproj b/Hi3Helper.CommunityToolkit/ImageCropper/Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper.csproj index fc54b1d12..4403e49e5 100644 --- a/Hi3Helper.CommunityToolkit/ImageCropper/Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper.csproj +++ b/Hi3Helper.CommunityToolkit/ImageCropper/Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper.csproj @@ -34,10 +34,10 @@ - + - + diff --git a/Hi3Helper.CommunityToolkit/ImageCropper/packages.lock.json b/Hi3Helper.CommunityToolkit/ImageCropper/packages.lock.json index f9564fe20..b59a2f7e0 100644 --- a/Hi3Helper.CommunityToolkit/ImageCropper/packages.lock.json +++ b/Hi3Helper.CommunityToolkit/ImageCropper/packages.lock.json @@ -43,15 +43,15 @@ }, "Microsoft.Web.WebView2": { "type": "Direct", - "requested": "[1.0.2783-prerelease, )", - "resolved": "1.0.2783-prerelease", - "contentHash": "sPxd49RLVnOyzplXTvdU9z6NIx0t56MridYoZhw8BOBZt564Ufi168Wfg5G0rIc5EKWmrGihLeZ1OfmjQiqWTw==" + "requested": "[1.0.2839-prerelease, )", + "resolved": "1.0.2839-prerelease", + "contentHash": "3o8szcERAskJmZl/YFSNhUaFL27Ba8MBpD/XSq+MG+vdiU7dmtp6msk8TPB6FuIvbLrzc0meiO21dtX4o2Mzwg==" }, "Microsoft.Windows.CsWinRT": { "type": "Direct", - "requested": "[2.1.3, )", - "resolved": "2.1.3", - "contentHash": "Nl8A4rQ4l2GNj703GvLSbr0Vo++FjxKxU7CIj1pcKz/sN8XSvD4dIvUCYYgD16o2pG4PSSXNgAxfwDUwLGHLPA==" + "requested": "[2.1.5, )", + "resolved": "2.1.5", + "contentHash": "PG0uVrpPTVEhqu70YhGMTyRKZXNgygjIIwdjAmg2hhHkmm6367TafBEdzIU/TgMXy2+x5Lv/Z9MKJehwkQXEvw==" }, "Microsoft.Windows.SDK.BuildTools": { "type": "Direct", @@ -97,9 +97,9 @@ }, "Microsoft.Web.WebView2": { "type": "Direct", - "requested": "[1.0.2783-prerelease, )", - "resolved": "1.0.2783-prerelease", - "contentHash": "sPxd49RLVnOyzplXTvdU9z6NIx0t56MridYoZhw8BOBZt564Ufi168Wfg5G0rIc5EKWmrGihLeZ1OfmjQiqWTw==" + "requested": "[1.0.2839-prerelease, )", + "resolved": "1.0.2839-prerelease", + "contentHash": "3o8szcERAskJmZl/YFSNhUaFL27Ba8MBpD/XSq+MG+vdiU7dmtp6msk8TPB6FuIvbLrzc0meiO21dtX4o2Mzwg==" }, "Microsoft.WindowsAppSDK": { "type": "Direct", diff --git a/Hi3Helper.CommunityToolkit/SettingsControls/Hi3Helper.CommunityToolkit.WinUI.Controls.SettingsControls.csproj b/Hi3Helper.CommunityToolkit/SettingsControls/Hi3Helper.CommunityToolkit.WinUI.Controls.SettingsControls.csproj index 53d7e3e6e..7b6ef89c7 100644 --- a/Hi3Helper.CommunityToolkit/SettingsControls/Hi3Helper.CommunityToolkit.WinUI.Controls.SettingsControls.csproj +++ b/Hi3Helper.CommunityToolkit/SettingsControls/Hi3Helper.CommunityToolkit.WinUI.Controls.SettingsControls.csproj @@ -34,10 +34,10 @@ - + - + diff --git a/Hi3Helper.CommunityToolkit/SettingsControls/packages.lock.json b/Hi3Helper.CommunityToolkit/SettingsControls/packages.lock.json index 28c4df48d..2c32aab90 100644 --- a/Hi3Helper.CommunityToolkit/SettingsControls/packages.lock.json +++ b/Hi3Helper.CommunityToolkit/SettingsControls/packages.lock.json @@ -21,15 +21,15 @@ }, "Microsoft.Web.WebView2": { "type": "Direct", - "requested": "[1.0.2783-prerelease, )", - "resolved": "1.0.2783-prerelease", - "contentHash": "sPxd49RLVnOyzplXTvdU9z6NIx0t56MridYoZhw8BOBZt564Ufi168Wfg5G0rIc5EKWmrGihLeZ1OfmjQiqWTw==" + "requested": "[1.0.2839-prerelease, )", + "resolved": "1.0.2839-prerelease", + "contentHash": "3o8szcERAskJmZl/YFSNhUaFL27Ba8MBpD/XSq+MG+vdiU7dmtp6msk8TPB6FuIvbLrzc0meiO21dtX4o2Mzwg==" }, "Microsoft.Windows.CsWinRT": { "type": "Direct", - "requested": "[2.1.3, )", - "resolved": "2.1.3", - "contentHash": "Nl8A4rQ4l2GNj703GvLSbr0Vo++FjxKxU7CIj1pcKz/sN8XSvD4dIvUCYYgD16o2pG4PSSXNgAxfwDUwLGHLPA==" + "requested": "[2.1.5, )", + "resolved": "2.1.5", + "contentHash": "PG0uVrpPTVEhqu70YhGMTyRKZXNgygjIIwdjAmg2hhHkmm6367TafBEdzIU/TgMXy2+x5Lv/Z9MKJehwkQXEvw==" }, "Microsoft.Windows.SDK.BuildTools": { "type": "Direct", @@ -76,9 +76,9 @@ "net9.0-windows10.0.22621/win-x64": { "Microsoft.Web.WebView2": { "type": "Direct", - "requested": "[1.0.2783-prerelease, )", - "resolved": "1.0.2783-prerelease", - "contentHash": "sPxd49RLVnOyzplXTvdU9z6NIx0t56MridYoZhw8BOBZt564Ufi168Wfg5G0rIc5EKWmrGihLeZ1OfmjQiqWTw==" + "requested": "[1.0.2839-prerelease, )", + "resolved": "1.0.2839-prerelease", + "contentHash": "3o8szcERAskJmZl/YFSNhUaFL27Ba8MBpD/XSq+MG+vdiU7dmtp6msk8TPB6FuIvbLrzc0meiO21dtX4o2Mzwg==" }, "Microsoft.WindowsAppSDK": { "type": "Direct", diff --git a/Hi3Helper.Core/Hi3Helper.Core.csproj b/Hi3Helper.Core/Hi3Helper.Core.csproj index f65319e99..26402b286 100644 --- a/Hi3Helper.Core/Hi3Helper.Core.csproj +++ b/Hi3Helper.Core/Hi3Helper.Core.csproj @@ -46,7 +46,7 @@ - + diff --git a/Hi3Helper.Core/packages.lock.json b/Hi3Helper.Core/packages.lock.json index 4b53cd0a5..f9986f092 100644 --- a/Hi3Helper.Core/packages.lock.json +++ b/Hi3Helper.Core/packages.lock.json @@ -10,9 +10,9 @@ }, "Microsoft.Windows.CsWinRT": { "type": "Direct", - "requested": "[2.1.3, )", - "resolved": "2.1.3", - "contentHash": "Nl8A4rQ4l2GNj703GvLSbr0Vo++FjxKxU7CIj1pcKz/sN8XSvD4dIvUCYYgD16o2pG4PSSXNgAxfwDUwLGHLPA==" + "requested": "[2.1.5, )", + "resolved": "2.1.5", + "contentHash": "PG0uVrpPTVEhqu70YhGMTyRKZXNgygjIIwdjAmg2hhHkmm6367TafBEdzIU/TgMXy2+x5Lv/Z9MKJehwkQXEvw==" }, "Google.Protobuf": { "type": "Transitive", diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool index 2d96954f2..a92442b88 160000 --- a/Hi3Helper.EncTool +++ b/Hi3Helper.EncTool @@ -1 +1 @@ -Subproject commit 2d96954f2357563c649d64b5770270c5aeac7ef7 +Subproject commit a92442b88bd16dcb397a696c91d28cabea41a06c diff --git a/ImageEx b/ImageEx index 71f68044f..b5f35e268 160000 --- a/ImageEx +++ b/ImageEx @@ -1 +1 @@ -Subproject commit 71f68044f41b7bb1e1679d3abb8c5ada15550981 +Subproject commit b5f35e268506e1b76c8734e3b11604661290e78a From 312e6ac7ee7a305af0b14c76923adc2b922fb248 Mon Sep 17 00:00:00 2001 From: Bagus Nur Listiyono Date: Sat, 12 Oct 2024 05:56:22 +0700 Subject: [PATCH 09/18] Early CodeQA --- .../Classes/Helper/Metadata/DataCooker.cs | 40 +++++++++---------- .../Zenless/ZenlessRepair.Check.cs | 5 --- .../Zenless/ZenlessRepair.Extensions.cs | 6 +-- .../Zenless/ZenlessRepair.Fetch.cs | 15 ++++--- .../RepairManagement/Zenless/ZenlessRepair.cs | 5 ++- .../Zenless/ZenlessResManifest.cs | 2 +- 6 files changed, 35 insertions(+), 38 deletions(-) diff --git a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs index 1399931f7..0be291321 100644 --- a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs +++ b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs @@ -206,31 +206,31 @@ private static unsafe int DecompressDataFromZstd(Span outData, int compres int dataWritten; fixed (byte* inputBuffer = &dataRawBuffer[0]) - fixed (byte* outputBuffer = &outData[0]) - { - int decompressedWritten = 0; + fixed (byte* outputBuffer = &outData[0]) + { + int decompressedWritten = 0; - byte[] buffer = new byte[4 << 10]; + byte[] buffer = new byte[4 << 10]; - using UnmanagedMemoryStream inputStream = new UnmanagedMemoryStream(inputBuffer, dataRawBuffer.Length); - using UnmanagedMemoryStream outputStream = new UnmanagedMemoryStream(outputBuffer, outData.Length); - using ZstdDecompressStream decompStream = new ZstdDecompressStream(inputStream); + using UnmanagedMemoryStream inputStream = new UnmanagedMemoryStream(inputBuffer, dataRawBuffer.Length); + using UnmanagedMemoryStream outputStream = new UnmanagedMemoryStream(outputBuffer, outData.Length); + using ZstdDecompressStream decompStream = new ZstdDecompressStream(inputStream); - int read = 0; - while ((read = decompStream.Read(buffer)) > 0) - { - outputStream.Write(buffer, 0, read); - decompressedWritten += read; - } + int read; + while ((read = decompStream.Read(buffer)) > 0) + { + outputStream.Write(buffer, 0, read); + decompressedWritten += read; + } - if (decompressedSize != decompressedWritten) - { - throw new DataMisalignedException("Decompressed data is misaligned!"); - } + if (decompressedSize != decompressedWritten) + { + throw new DataMisalignedException("Decompressed data is misaligned!"); + } - dataWritten = decompressedWritten; - return dataWritten; - } + dataWritten = decompressedWritten; + return dataWritten; + } } } } \ No newline at end of file diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs index 29e02bd92..7f54aad28 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs @@ -9,7 +9,6 @@ using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; -using ZstdSharp.Unsafe; namespace CollapseLauncher { @@ -60,10 +59,6 @@ private async Task Check(List assetIndex, CancellationToke { throw ex.Flatten().InnerExceptions.First(); } - catch (Exception) - { - throw; - } // Re-add the asset index with a broken asset index assetIndex.Clear(); diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs index 0cfaf1bb4..19c67e259 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs @@ -89,7 +89,7 @@ private async ValueTask InternalReadAsync(Memory buffer, Cancellation await redirectStream.WriteAsync(innerBuffer, 0, read, token); - int lastIndexOffset = 0; + int lastIndexOffset; if (isFieldStart && ((lastIndexOffset = EnsureIsEnd(innerBuffer, read)) > 0)) { innerBuffer.AsSpan(0, lastIndexOffset).CopyTo(buffer.Span); @@ -123,7 +123,7 @@ private int InternalRead(Span buffer) redirectStream.Write(innerBuffer, 0, read); - int lastIndexOffset = 0; + int lastIndexOffset; if (isFieldStart && ((lastIndexOffset = EnsureIsEnd(innerBuffer, read)) > 0)) { innerBuffer.AsSpan(0, lastIndexOffset).CopyTo(buffer); @@ -362,7 +362,7 @@ internal static ReadOnlySpan GetAssetRelativePath(this FilePropertiesRemot { assetType = RepairAssetType.Generic; - int indexOfOffset = -1; + int indexOfOffset; if ((indexOfOffset = asset.N.LastIndexOf(AssetTypeAudioPath, StringComparison.OrdinalIgnoreCase)) >= 0) { assetType = RepairAssetType.Audio; diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs index b3247662c..ad174111e 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -8,7 +8,6 @@ using Hi3Helper.Shared.Region; using System; using System.Buffers; -using System.Buffers.Text; using System.Collections.Generic; using System.Data; using System.IO; @@ -140,7 +139,7 @@ private async Task GetPrimaryManifest(DownloadClient downloadClient, Dictionary< private async Task> FetchMetadata(CancellationToken token) { // Set metadata URL - string urlMetadata = string.Format(LauncherConfig.AppGameRepoIndexURLPrefix, GameVersionManagerCast.GamePreset.ProfileName); + string urlMetadata = string.Format(LauncherConfig.AppGameRepoIndexURLPrefix, GameVersionManagerCast!.GamePreset.ProfileName); // Start downloading metadata using FallbackCDNUtil await using BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlMetadata, token); @@ -152,14 +151,14 @@ private async Task> FetchMetadata(CancellationToken t private async Task GetResManifest(DownloadClient downloadClient, Dictionary hashSet, CancellationToken token, List assetIndex) { // Create sleepy property - PresetConfig gamePreset = GameVersionManagerCast.GamePreset; + PresetConfig gamePreset = GameVersionManagerCast!.GamePreset; HttpClient client = downloadClient.GetHttpClient(); // Get sleepy info SleepyInfo sleepyInfo = await TryGetSleepyInfo( client, gamePreset, - GameSettings.GeneralData.SelectedServerName, + GameSettings!.GeneralData.SelectedServerName, gamePreset.GameDispatchDefaultName, token); @@ -228,9 +227,9 @@ private async Task TryGetSleepyInfo(HttpClient client, PresetConfig { // Initialize property SleepyProperty sleepyProperty = SleepyProperty.Create( - GameVersionManagerCast.GetGameExistingVersion()?.VersionString, + GameVersionManagerCast!.GetGameExistingVersion()?.VersionString, gamePreset.GameDispatchChannelName, - gamePreset.GameDispatchArrayURL[Random.Shared.Next(0, gamePreset.GameDispatchArrayURL.Count - 1)], + gamePreset.GameDispatchArrayURL![Random.Shared.Next(0, gamePreset.GameDispatchArrayURL.Count - 1)], gamePreset.GameDispatchURLTemplate, targetServerName ?? fallbackServerName, gamePreset.GameGatewayURLTemplate, @@ -323,11 +322,11 @@ private async IAsyncEnumerable EnumerateResManifestToAsset private void EliminatePluginAssetIndex(List assetIndex) { - _gameVersionManager.GameAPIProp.data.plugins?.ForEach(plugin => + _gameVersionManager.GameAPIProp.data!.plugins?.ForEach(plugin => { assetIndex.RemoveAll(asset => { - return plugin.package.validate?.Exists(validate => validate.path == asset.N) ?? false; + return plugin.package!.validate?.Exists(validate => validate.path == asset.N) ?? false; }); }); } diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs index 681daa3e0..bfda8f6ac 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs @@ -19,10 +19,13 @@ internal partial class ZenlessRepair : ProgressBase, IRepa internal const string _assetGamePersistentPath = @"{0}_Data\Persistent"; internal const string _assetGameStreamingPath = @"{0}_Data\StreamingAssets"; + // ReSharper disable AutoPropertyCanBeMadeGetOnly.Local private bool IsOnlyRecoverMain { get; set; } private bool IsCacheUpdateMode { get; set; } private string? ExecutableName { get; set; } - + // ReSharper restore AutoPropertyCanBeMadeGetOnly.Local + + // ReSharper disable once UnusedAutoPropertyAccessor.Local private List? OriginAssetIndex { get; set; } private GameTypeZenlessVersion? GameVersionManagerCast { get => _gameVersionManager as GameTypeZenlessVersion; } private ZenlessSettings? GameSettings { get; init; } diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessResManifest.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessResManifest.cs index 5c2e7291b..1f9350522 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessResManifest.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessResManifest.cs @@ -20,7 +20,7 @@ internal class ZenlessResManifestAsset [JsonPropertyName("md5")] // "mD5" they said. BROO, IT'S A F**KING XXH64 HASH!!!! [JsonConverter(typeof(NumberStringToXxh64HashBytesConverter))] // AND THEY STORED IT AS A NUMBER IN A STRING WTFF?????? - public byte[] Xxh64Hash { get; set; } + public byte[] Xxh64Hash { get; set; } // classic [JsonPropertyName("fileSize")] [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] From aa26427face2775020705847dc1b2273e974f490 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sun, 13 Oct 2024 22:07:38 +0700 Subject: [PATCH 10/18] Fix 404 error on non-patch res files --- .../Zenless/ZenlessRepair.Extensions.cs | 10 +- .../Zenless/ZenlessRepair.Fetch.cs | 248 ++++++++++-------- 2 files changed, 150 insertions(+), 108 deletions(-) diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs index 19c67e259..fff2a5a50 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs @@ -274,20 +274,20 @@ internal static async IAsyncEnumerable RegisterSleepyFileI } } - internal static async IAsyncEnumerable RegisterResCategorizedAssetsToHashSetAsync(this IAsyncEnumerable assetEnumerable, List assetIndex, Dictionary hashSet, string baseLocalPath, string baseUrl) + internal static async IAsyncEnumerable RegisterResCategorizedAssetsToHashSetAsync(this IAsyncEnumerable assetEnumerable, List assetIndex, Dictionary hashSet, string baseLocalPath, string basePatchUrl, string baseResUrl) { await foreach (PkgVersionProperties asset in assetEnumerable) { string baseLocalPathMerged = Path.Combine(baseLocalPath, asset.isPatch ? PersistentAssetsPath : StreamingAssetsPath); - yield return ReturnCategorizedYieldValue(hashSet, assetIndex, asset, baseLocalPathMerged, baseUrl); + yield return ReturnCategorizedYieldValue(hashSet, assetIndex, asset, baseLocalPathMerged, basePatchUrl, baseResUrl); } } - private static FilePropertiesRemote? ReturnCategorizedYieldValue(Dictionary hashSet, List assetIndex, PkgVersionProperties asset, string baseLocalPath, string baseUrl) + private static FilePropertiesRemote? ReturnCategorizedYieldValue(Dictionary hashSet, List assetIndex, PkgVersionProperties asset, string baseLocalPath, string baseUrl, string? alternativeUrlIfNonPatch = null) { FilePropertiesRemote asRemoteProperty = GetNormalizedFilePropertyTypeBased( - baseUrl, + asset.isPatch || string.IsNullOrEmpty(alternativeUrlIfNonPatch) ? baseUrl : alternativeUrlIfNonPatch!, baseLocalPath, asset.remoteName, asset.fileSize, @@ -315,7 +315,7 @@ internal static async IAsyncEnumerable RegisterSleepyFileI }; string relTypeRelativePathStr = relTypeRelativePath.ToString(); - if (!hashSet.TryAdd(relTypeRelativePathStr, asRemoteProperty)) + if (!hashSet.TryAdd(relTypeRelativePathStr, asRemoteProperty) && asset.isPatch) { FilePropertiesRemote existingValue = hashSet[relTypeRelativePathStr]; int indexOf = assetIndex.IndexOf(existingValue); diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs index ad174111e..b62f88dff 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -1,6 +1,7 @@ using CollapseLauncher.Helper; using CollapseLauncher.Helper.Metadata; using Hi3Helper; +using Hi3Helper.Data; using Hi3Helper.EncTool.Parser.AssetIndex; using Hi3Helper.EncTool.Parser.Sleepy; using Hi3Helper.Http; @@ -28,7 +29,6 @@ private async Task Fetch(List assetIndex, CancellationToke _status.ActivityStatus = Locale.Lang._GameRepairPage.Status2; _status.IsProgressAllIndetermined = true; UpdateStatus(); - StarRailRepairExtension.ClearHashtable(); // Initialize new proxy-aware HttpClient using HttpClient client = new HttpClientBuilder() @@ -43,40 +43,36 @@ private async Task Fetch(List assetIndex, CancellationToke // Create a hash set to overwrite local files Dictionary hashSet = new Dictionary(StringComparer.OrdinalIgnoreCase); - try + // If not in cache mode, then fetch main package + if (!IsCacheUpdateMode) { - // If not in cache mode, then fetch main package - if (!IsCacheUpdateMode) - { - // Get the primary manifest - await GetPrimaryManifest(downloadClient, hashSet, token, assetIndex); - } + // Get the primary manifest + await GetPrimaryManifest(downloadClient, hashSet, token, assetIndex); + } - // Execute on non-recover main mode - if (!IsOnlyRecoverMain) - { - // Get the in-game res manifest - await GetResManifest(downloadClient, hashSet, token, assetIndex); + // Execute on non-recover main mode + if (!IsOnlyRecoverMain) + { + // Get the in-game res manifest + await GetResManifest(downloadClient, hashSet, token, assetIndex, +#if DEBUG + true +#else + false +#endif + ); - // Execute plugin things if not in cache mode only - if (!IsCacheUpdateMode) - { - // Force-Fetch the Bilibili SDK (if exist :pepehands:) - await FetchBilibiliSDK(token); + // Execute plugin things if not in cache mode only + if (!IsCacheUpdateMode) + { + // Force-Fetch the Bilibili SDK (if exist :pepehands:) + await FetchBilibiliSDK(token); - // Remove plugin from assetIndex - // Skip the removal for Delta-Patch - EliminatePluginAssetIndex(assetIndex); - } + // Remove plugin from assetIndex + // Skip the removal for Delta-Patch + EliminatePluginAssetIndex(assetIndex); } } - finally - { - // Clear the hashtable - StarRailRepairExtension.ClearHashtable(); - // Unsubscribe the fetching progress and dispose it and unsubscribe cacheUtil progress to adapter - // _innerGameVersionManager.StarRailMetadataTool.HttpEvent -= _httpClient_FetchAssetProgress; - } } #endregion @@ -115,25 +111,28 @@ private async Task GetPrimaryManifest(DownloadClient downloadClient, Dictionary< // Start downloading asset index using FallbackCDNUtil and return its stream await using BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlIndex, token); - if (stream != null) + await Task.Run(() => { - // Deserialize asset index and set it to list - AssetIndexV2 parserTool = new AssetIndexV2(); - pkgVersion = await Task.Run(() => parserTool.Deserialize(stream, out DateTime timestamp)).ConfigureAwait(false); - } + if (stream != null) + { + // Deserialize asset index and set it to list + AssetIndexV2 parserTool = new AssetIndexV2(); + pkgVersion = parserTool.Deserialize(stream, out DateTime timestamp); + } - // Convert the pkg version list to asset index - foreach (FilePropertiesRemote entry in pkgVersion.RegisterMainCategorizedAssetsToHashSet(assetIndex, hashSet, _gamePath, _gameRepoURL)) - { - // If entry is null (means, an existing entry has been overwritten), then next - if (entry == null) - continue; + // Convert the pkg version list to asset index + foreach (FilePropertiesRemote entry in pkgVersion.RegisterMainCategorizedAssetsToHashSet(assetIndex, hashSet, _gamePath, _gameRepoURL)) + { + // If entry is null (means, an existing entry has been overwritten), then next + if (entry == null) + continue; - assetIndex.Add(entry); - } + assetIndex.Add(entry); + } - // Clear the pkg version list - pkgVersion.Clear(); + // Clear the pkg version list + pkgVersion.Clear(); + }).ConfigureAwait(false); } private async Task> FetchMetadata(CancellationToken token) @@ -148,77 +147,119 @@ private async Task> FetchMetadata(CancellationToken t #endregion #region ResManifest - private async Task GetResManifest(DownloadClient downloadClient, Dictionary hashSet, CancellationToken token, List assetIndex) + private async Task GetResManifest( + DownloadClient downloadClient, + Dictionary hashSet, + CancellationToken token, + List assetIndex, + bool throwIfError) { - // Create sleepy property - PresetConfig gamePreset = GameVersionManagerCast!.GamePreset; - HttpClient client = downloadClient.GetHttpClient(); - - // Get sleepy info - SleepyInfo sleepyInfo = await TryGetSleepyInfo( - client, - gamePreset, - GameSettings!.GeneralData.SelectedServerName, - gamePreset.GameDispatchDefaultName, - token); - - // Get persistent path - string persistentPath = GameDataPersistentPath; - - // Fetch cache files - if (IsCacheUpdateMode) + try { + // Create sleepy property + PresetConfig gamePreset = GameVersionManagerCast!.GamePreset; + HttpClient client = downloadClient.GetHttpClient(); + + // Get sleepy info + SleepyInfo sleepyInfo = await TryGetSleepyInfo( + client, + gamePreset, + GameSettings!.GeneralData.SelectedServerName, + gamePreset.GameDispatchDefaultName, + token); + + // Get persistent path + string persistentPath = GameDataPersistentPath; + + // Get Sleepy Info SleepyFileInfoResult infoKindSilence = sleepyInfo.GetFileInfoResult(FileInfoKind.Silence); SleepyFileInfoResult infoKindData = sleepyInfo.GetFileInfoResult(FileInfoKind.Data); - - IAsyncEnumerable infoSilenceEnumerable = EnumerateResManifestToAssetIndexAsync( - infoKindSilence.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), - assetIndex, - hashSet, - Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), - infoKindSilence.BaseUrl); - - IAsyncEnumerable infoDataEnumerable = EnumerateResManifestToAssetIndexAsync( - infoKindData.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), - assetIndex, - hashSet, - Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), - infoKindData.BaseUrl); - - await foreach (FilePropertiesRemote asset in ZenlessRepairExtensions - .MergeAsyncEnumerable(infoSilenceEnumerable, infoDataEnumerable)) - { - assetIndex.Add(asset); - } - } - // Fetch repair files - else - { SleepyFileInfoResult infoKindRes = sleepyInfo.GetFileInfoResult(FileInfoKind.Res); SleepyFileInfoResult infoKindAudio = sleepyInfo.GetFileInfoResult(FileInfoKind.Audio); + SleepyFileInfoResult infoKindBase = sleepyInfo.GetFileInfoResult(FileInfoKind.Base); - IAsyncEnumerable infoResEnumerable = EnumerateResManifestToAssetIndexAsync( - infoKindRes.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), - assetIndex, - hashSet, - Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), - infoKindRes.BaseUrl); + // Create non-patch URL + string baseResUrl = GetBaseResUrl(infoKindBase.BaseUrl, infoKindBase.RevisionStamp); - IAsyncEnumerable infoAudioEnumerable = GetOnlyInstalledAudioPack( - EnumerateResManifestToAssetIndexAsync( - infoKindAudio.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + // Fetch cache files + if (IsCacheUpdateMode) + { + IAsyncEnumerable infoSilenceEnumerable = EnumerateResManifestToAssetIndexAsync( + infoKindSilence.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), assetIndex, hashSet, Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), - infoKindAudio.BaseUrl) - ); + infoKindSilence.BaseUrl, baseResUrl); + + IAsyncEnumerable infoDataEnumerable = EnumerateResManifestToAssetIndexAsync( + infoKindData.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + infoKindData.BaseUrl, baseResUrl); + + await foreach (FilePropertiesRemote asset in ZenlessRepairExtensions + .MergeAsyncEnumerable(infoSilenceEnumerable, infoDataEnumerable)) + { + assetIndex.Add(asset); + } - await foreach (FilePropertiesRemote asset in ZenlessRepairExtensions - .MergeAsyncEnumerable(infoResEnumerable, infoAudioEnumerable)) + // Create base revision file + string baseRevisionFile = Path.Combine(persistentPath, "base_revision"); + File.WriteAllText(EnsureCreationOfDirectory(baseRevisionFile), infoKindBase.RevisionStamp); + } + // Fetch repair files + else { - assetIndex.Add(asset); + IAsyncEnumerable infoResEnumerable = EnumerateResManifestToAssetIndexAsync( + infoKindRes.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + infoKindRes.BaseUrl, baseResUrl); + + IAsyncEnumerable infoAudioEnumerable = GetOnlyInstalledAudioPack( + EnumerateResManifestToAssetIndexAsync( + infoKindAudio.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + infoKindAudio.BaseUrl, baseResUrl) + ); + + await foreach (FilePropertiesRemote asset in ZenlessRepairExtensions + .MergeAsyncEnumerable(infoResEnumerable, infoAudioEnumerable)) + { + assetIndex.Add(asset); + } } } + catch (Exception ex) + { + if (throwIfError) + throw; + + Logger.LogWriteLine($"An error has occurred while trying to fetch Sleepy's res manifest\r\n{ex}", LogType.Error, true); + } + } + + private string GetBaseResUrl(string baseUrl, string stampRevision) + { + const string startMark = "output_"; + const string endMark = "/client"; + + ReadOnlySpan baseUrlSpan = baseUrl; + + int startMarkOffset = baseUrlSpan.IndexOf(startMark); + int endMarkOffset = baseUrlSpan.IndexOf(endMark); + + if (startMarkOffset < 0 || endMarkOffset < 0) + throw new IndexOutOfRangeException($"Start mark offset or End mark offset was not found! Start: {startMarkOffset} End: {endMarkOffset}"); + + ReadOnlySpan startMarkSpan = baseUrlSpan.Slice(0, startMarkOffset); + ReadOnlySpan endMarkSpan = baseUrlSpan.Slice(endMarkOffset); + + return ConverterTool.CombineURLFromString(startMarkSpan, $"output_{stampRevision}", endMarkSpan.ToString()); } private async Task TryGetSleepyInfo(HttpClient client, PresetConfig gamePreset, string targetServerName, string fallbackServerName = null, CancellationToken token = default) @@ -258,7 +299,7 @@ private async Task TryGetSleepyInfo(HttpClient client, PresetConfig return await TryGetSleepyInfo(client, gamePreset, fallbackServerName, null, token); } } - + private async IAsyncEnumerable GetOnlyInstalledAudioPack(IAsyncEnumerable enumerable) { const string WindowsFullPath = "Audio\\Windows\\Full"; @@ -307,9 +348,10 @@ private async IAsyncEnumerable EnumerateResManifestToAsset List assetIndex, Dictionary hashSet, string baseLocalPath, - string baseUrl) + string basePatchUrl, + string baseResUrl) { - await foreach (FilePropertiesRemote? entry in pkgVersion.RegisterResCategorizedAssetsToHashSetAsync(assetIndex, hashSet, baseLocalPath, baseUrl)) + await foreach (FilePropertiesRemote? entry in pkgVersion.RegisterResCategorizedAssetsToHashSetAsync(assetIndex, hashSet, baseLocalPath, basePatchUrl, baseResUrl)) { // If entry is null (means, an existing entry has been overwritten), then next if (entry == null) From e8b7cfb75fac9e78642105098ea15d684d2913a2 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sun, 13 Oct 2024 22:07:59 +0700 Subject: [PATCH 11/18] Use uninit memalloc on hash buffer --- .../Classes/Interfaces/Class/ProgressBase.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs index dfbc9fa0f..c37f074fa 100644 --- a/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs +++ b/CollapseLauncher/Classes/Interfaces/Class/ProgressBase.cs @@ -1062,8 +1062,11 @@ internal static async ValueTask NaivelyOpenFileStreamAsync(FileInfo #region HashTools protected virtual async ValueTask CheckHashAsync(Stream stream, HashAlgorithm hashProvider, CancellationToken token, bool updateTotalProgress = true) { - // Initialize MD5 instance and assign buffer - byte[] buffer = new byte[_bufferBigLength]; + // Get length based on stream length or at least if bigger, use the default one + int bufferLen = stream is FileStream && _bufferBigLength < stream.Length ? (int)stream.Length : _bufferBigLength; + + // Initialize Xxh64 instance and assign buffer + byte[] buffer = GC.AllocateUninitializedArray(bufferLen); // Do read activity int read; @@ -1094,11 +1097,13 @@ protected virtual async ValueTask CheckHashAsync(Stream stream, HashAlgo return hashProvider.Hash; } - - protected virtual async ValueTask CheckHashAsync(Stream stream, XxHash64 hashProvider, CancellationToken token, bool updateTotalProgress = true) + protected virtual async ValueTask CheckHashAsync(Stream stream, NonCryptographicHashAlgorithm hashProvider, CancellationToken token, bool updateTotalProgress = true) { + // Get length based on stream length or at least if bigger, use the default one + int bufferLen = stream is FileStream && _bufferBigLength < stream.Length ? (int)stream.Length : _bufferBigLength; + // Initialize Xxh64 instance and assign buffer - byte[] buffer = new byte[_bufferBigLength]; + byte[] buffer = GC.AllocateUninitializedArray(bufferLen); // Do read activity int read; From b7f4c733999b3b9053a9347cefc7147dd838787c Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sun, 13 Oct 2024 22:22:06 +0700 Subject: [PATCH 12/18] CodeQA --- .../CachesManagement/Zenless/ZenlessCache.cs | 2 +- .../Zenless/ZenlessRepair.Extensions.cs | 11 ++---- .../Zenless/ZenlessRepair.Fetch.cs | 39 ++++++++++--------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs b/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs index e44956c4b..d51a1e90b 100644 --- a/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs +++ b/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs @@ -14,6 +14,6 @@ public ZenlessCache(UIElement parentUI, IGameVersionCheck gameVersionManager, Ze public ZenlessCache AsBaseType() => this; public async Task StartUpdateRoutine(bool showInteractivePrompt = false) - => await base.StartRepairRoutine(showInteractivePrompt); + => await StartRepairRoutine(showInteractivePrompt); } } diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs index fff2a5a50..97586acea 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs @@ -31,7 +31,7 @@ internal class ZenlessManifestInterceptStream : Stream internal ZenlessManifestInterceptStream(string? filePath, Stream stream) { innerStream = stream; - if (!string.IsNullOrWhiteSpace(filePath)) + if (!string.IsNullOrWhiteSpace(filePath) && stream != null) { string? filePathDir = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(filePathDir) && !Directory.Exists(filePathDir)) @@ -184,11 +184,8 @@ public override void Write(byte[] buffer, int offset, int count) public override async ValueTask DisposeAsync() { - if (innerStream != null) - await innerStream.DisposeAsync(); - - if (redirectStream != null) - await redirectStream.DisposeAsync(); + await innerStream.DisposeAsync(); + await redirectStream.DisposeAsync(); } protected override void Dispose(bool disposing) @@ -287,7 +284,7 @@ internal static async IAsyncEnumerable RegisterSleepyFileI private static FilePropertiesRemote? ReturnCategorizedYieldValue(Dictionary hashSet, List assetIndex, PkgVersionProperties asset, string baseLocalPath, string baseUrl, string? alternativeUrlIfNonPatch = null) { FilePropertiesRemote asRemoteProperty = GetNormalizedFilePropertyTypeBased( - asset.isPatch || string.IsNullOrEmpty(alternativeUrlIfNonPatch) ? baseUrl : alternativeUrlIfNonPatch!, + asset.isPatch || string.IsNullOrEmpty(alternativeUrlIfNonPatch) ? baseUrl : alternativeUrlIfNonPatch, baseLocalPath, asset.remoteName, asset.fileSize, diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs index b62f88dff..88382c16e 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -110,29 +110,32 @@ private async Task GetPrimaryManifest(DownloadClient downloadClient, Dictionary< string urlIndex = string.Format(LauncherConfig.AppGameRepairIndexURLPrefix, _gameVersionManager.GamePreset.ProfileName, _gameVersion.VersionString) + ".binv2"; // Start downloading asset index using FallbackCDNUtil and return its stream - await using BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlIndex, token); - await Task.Run(() => + await using (BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlIndex, token)) { - if (stream != null) + await Task.Run(() => { - // Deserialize asset index and set it to list - AssetIndexV2 parserTool = new AssetIndexV2(); - pkgVersion = parserTool.Deserialize(stream, out DateTime timestamp); - } + if (stream != null) + { + // Deserialize asset index and set it to list + AssetIndexV2 parserTool = new AssetIndexV2(); + pkgVersion = parserTool.Deserialize(stream, out DateTime timestamp); + Logger.LogWriteLine($"Asset index timestamp: {timestamp}", LogType.Default, true); + } - // Convert the pkg version list to asset index - foreach (FilePropertiesRemote entry in pkgVersion.RegisterMainCategorizedAssetsToHashSet(assetIndex, hashSet, _gamePath, _gameRepoURL)) - { - // If entry is null (means, an existing entry has been overwritten), then next - if (entry == null) - continue; + // Convert the pkg version list to asset index + foreach (FilePropertiesRemote entry in pkgVersion.RegisterMainCategorizedAssetsToHashSet(assetIndex, hashSet, _gamePath, _gameRepoURL)) + { + // If entry is null (means, an existing entry has been overwritten), then next + if (entry == null) + continue; - assetIndex.Add(entry); - } + assetIndex.Add(entry); + } - // Clear the pkg version list - pkgVersion.Clear(); - }).ConfigureAwait(false); + // Clear the pkg version list + pkgVersion.Clear(); + }).ConfigureAwait(false); + } } private async Task> FetchMetadata(CancellationToken token) From febd078d946904a9618a9f3d8f8990986d3b06a1 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sun, 13 Oct 2024 22:32:02 +0700 Subject: [PATCH 13/18] CodeQA (again) --- .../RepairManagement/Zenless/ZenlessRepair.Extensions.cs | 2 +- .../RepairManagement/Zenless/ZenlessRepair.Fetch.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs index 97586acea..617adb0c2 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs @@ -31,7 +31,7 @@ internal class ZenlessManifestInterceptStream : Stream internal ZenlessManifestInterceptStream(string? filePath, Stream stream) { innerStream = stream; - if (!string.IsNullOrWhiteSpace(filePath) && stream != null) + if (!string.IsNullOrWhiteSpace(filePath)) { string? filePathDir = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(filePathDir) && !Directory.Exists(filePathDir)) diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs index 88382c16e..40aa871c7 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -110,9 +110,9 @@ private async Task GetPrimaryManifest(DownloadClient downloadClient, Dictionary< string urlIndex = string.Format(LauncherConfig.AppGameRepairIndexURLPrefix, _gameVersionManager.GamePreset.ProfileName, _gameVersion.VersionString) + ".binv2"; // Start downloading asset index using FallbackCDNUtil and return its stream - await using (BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlIndex, token)) + await Task.Run(async () => { - await Task.Run(() => + await using (BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlIndex, token)) { if (stream != null) { @@ -134,8 +134,8 @@ await Task.Run(() => // Clear the pkg version list pkgVersion.Clear(); - }).ConfigureAwait(false); - } + } + }).ConfigureAwait(false); } private async Task> FetchMetadata(CancellationToken token) From c871a775a48fe2a1e6271e2f542ae15f649d9ec0 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sun, 13 Oct 2024 22:32:41 +0700 Subject: [PATCH 14/18] Re-associate sleepy's url query --- .../Zenless/ZenlessRepair.Fetch.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs index 40aa871c7..3cd433279 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -269,14 +269,26 @@ private async Task TryGetSleepyInfo(HttpClient client, PresetConfig { try { + // Get re-associated channel ids + string dispatchUrlTemplate = ('.' + gamePreset.GameDispatchURLTemplate).AssociateGameAndLauncherId( + "channel_id", + "sub_channel_id", + $"{gamePreset.ChannelID}", + $"{gamePreset.SubChannelID}").TrimStart('.'); + string gatewayUrlTemplate = ('.' + gamePreset.GameGatewayURLTemplate).AssociateGameAndLauncherId( + "channel_id", + "sub_channel_id", + $"{gamePreset.ChannelID}", + $"{gamePreset.SubChannelID}").TrimStart('.'); + // Initialize property SleepyProperty sleepyProperty = SleepyProperty.Create( GameVersionManagerCast!.GetGameExistingVersion()?.VersionString, gamePreset.GameDispatchChannelName, gamePreset.GameDispatchArrayURL![Random.Shared.Next(0, gamePreset.GameDispatchArrayURL.Count - 1)], - gamePreset.GameDispatchURLTemplate, + dispatchUrlTemplate, targetServerName ?? fallbackServerName, - gamePreset.GameGatewayURLTemplate, + gatewayUrlTemplate, gamePreset.ProtoDispatchKey, SleepyBuildProperty.Create(GameVersionManagerCast.SleepyIdentity, GameVersionManagerCast.SleepyArea) ); From 7ea6703928e7e5ca48a44de1c985a9669dfe797f Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sun, 13 Oct 2024 22:56:55 +0700 Subject: [PATCH 15/18] Much CodeQA later --- CollapseLauncher/Classes/GamePropertyVault.cs | 80 ++++---- .../Zenless/ZenlessRepair.Extensions.cs | 172 +++++++++--------- .../Zenless/ZenlessRepair.Fetch.cs | 84 ++++----- 3 files changed, 163 insertions(+), 173 deletions(-) diff --git a/CollapseLauncher/Classes/GamePropertyVault.cs b/CollapseLauncher/Classes/GamePropertyVault.cs index 8327cca35..2da50e623 100644 --- a/CollapseLauncher/Classes/GamePropertyVault.cs +++ b/CollapseLauncher/Classes/GamePropertyVault.cs @@ -26,49 +26,53 @@ namespace CollapseLauncher.Statics { internal class GamePresetProperty : IDisposable { - internal GamePresetProperty(UIElement UIElementParent, RegionResourceProp APIResouceProp, string GameName, string GameRegion) + internal GamePresetProperty(UIElement uiElementParent, RegionResourceProp apiResourceProp, string gameName, string gameRegion) { - if (LauncherMetadataHelper.LauncherMetadataConfig != null) + if (LauncherMetadataHelper.LauncherMetadataConfig == null) { - PresetConfig GamePreset = LauncherMetadataHelper.LauncherMetadataConfig[GameName][GameRegion]; + return; + } - _APIResouceProp = APIResouceProp!.Copy(); - switch (GamePreset!.GameType) - { - case GameNameType.Honkai: - _GameVersion = new GameTypeHonkaiVersion(UIElementParent, _APIResouceProp, GameName, GameRegion); - _GameSettings = new HonkaiSettings(_GameVersion); - _GameCache = new HonkaiCache(UIElementParent, _GameVersion); - _GameRepair = new HonkaiRepair(UIElementParent, _GameVersion, _GameCache, _GameSettings); - _GameInstall = new HonkaiInstall(UIElementParent, _GameVersion, _GameCache, _GameSettings); - break; - case GameNameType.StarRail: - _GameVersion = new GameTypeStarRailVersion(UIElementParent, _APIResouceProp, GameName, GameRegion); - _GameSettings = new StarRailSettings(_GameVersion); - _GameCache = new StarRailCache(UIElementParent, _GameVersion); - _GameRepair = new StarRailRepair(UIElementParent, _GameVersion); - _GameInstall = new StarRailInstall(UIElementParent, _GameVersion); - break; - case GameNameType.Genshin: - _GameVersion = new GameTypeGenshinVersion(UIElementParent, _APIResouceProp, GameName, GameRegion); - _GameSettings = new GenshinSettings(_GameVersion); - _GameCache = null; - _GameRepair = new GenshinRepair(UIElementParent, _GameVersion, _GameVersion.GameAPIProp!.data!.game!.latest!.decompressed_path); - _GameInstall = new GenshinInstall(UIElementParent, _GameVersion); - break; - case GameNameType.Zenless: - _GameVersion = new GameTypeZenlessVersion(UIElementParent, _APIResouceProp, GamePreset, GameName, GameRegion); - _GameSettings = new ZenlessSettings(_GameVersion); - _GameCache = new ZenlessCache(UIElementParent, _GameVersion, _GameSettings as ZenlessSettings); - _GameRepair = new ZenlessRepair(UIElementParent, _GameVersion, _GameSettings as ZenlessSettings); - _GameInstall = new ZenlessInstall(UIElementParent, _GameVersion, _GameSettings as ZenlessSettings); - break; - default: - throw new NotSupportedException($"[GamePresetProperty.Ctor] Game type: {GamePreset.GameType} ({GamePreset.ProfileName} - {GamePreset.ZoneName}) is not supported!"); - } + PresetConfig gamePreset = LauncherMetadataHelper.LauncherMetadataConfig[gameName][gameRegion]; - _GamePlaytime = new Playtime(_GameVersion); + _APIResouceProp = apiResourceProp!.Copy(); + switch (gamePreset!.GameType) + { + case GameNameType.Honkai: + _GameVersion = new GameTypeHonkaiVersion(uiElementParent, _APIResouceProp, gameName, gameRegion); + _GameSettings = new HonkaiSettings(_GameVersion); + _GameCache = new HonkaiCache(uiElementParent, _GameVersion); + _GameRepair = new HonkaiRepair(uiElementParent, _GameVersion, _GameCache, _GameSettings); + _GameInstall = new HonkaiInstall(uiElementParent, _GameVersion, _GameCache, _GameSettings); + break; + case GameNameType.StarRail: + _GameVersion = new GameTypeStarRailVersion(uiElementParent, _APIResouceProp, gameName, gameRegion); + _GameSettings = new StarRailSettings(_GameVersion); + _GameCache = new StarRailCache(uiElementParent, _GameVersion); + _GameRepair = new StarRailRepair(uiElementParent, _GameVersion); + _GameInstall = new StarRailInstall(uiElementParent, _GameVersion); + break; + case GameNameType.Genshin: + _GameVersion = new GameTypeGenshinVersion(uiElementParent, _APIResouceProp, gameName, gameRegion); + _GameSettings = new GenshinSettings(_GameVersion); + _GameCache = null; + _GameRepair = new GenshinRepair(uiElementParent, _GameVersion, _GameVersion.GameAPIProp!.data!.game!.latest!.decompressed_path); + _GameInstall = new GenshinInstall(uiElementParent, _GameVersion); + break; + case GameNameType.Zenless: + _GameVersion = new GameTypeZenlessVersion(uiElementParent, _APIResouceProp, gamePreset, gameName, gameRegion); + ZenlessSettings gameSettings = new ZenlessSettings(_GameVersion); + _GameSettings = gameSettings; + _GameCache = new ZenlessCache(uiElementParent, _GameVersion, gameSettings); + _GameRepair = new ZenlessRepair(uiElementParent, _GameVersion, gameSettings); + _GameInstall = new ZenlessInstall(uiElementParent, _GameVersion, gameSettings); + break; + case GameNameType.Unknown: + default: + throw new NotSupportedException($"[GamePresetProperty.Ctor] Game type: {gamePreset.GameType} ({gamePreset.ProfileName} - {gamePreset.ZoneName}) is not supported!"); } + + _GamePlaytime = new Playtime(_GameVersion); } internal RegionResourceProp _APIResouceProp { get; set; } diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs index 617adb0c2..cd5950eed 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -22,15 +23,15 @@ internal partial class ZenlessManifestContext : JsonSerializerContext { } internal class ZenlessManifestInterceptStream : Stream { - private Stream redirectStream; - private Stream innerStream; - private bool isFieldStart; - private bool isFieldEnd; - private byte[] innerBuffer = new byte[16 << 10]; + private readonly Stream _redirectStream; + private readonly Stream _innerStream; + private bool _isFieldStart; + private bool _isFieldEnd; + private readonly byte[] _innerBuffer = new byte[16 << 10]; internal ZenlessManifestInterceptStream(string? filePath, Stream stream) { - innerStream = stream; + _innerStream = stream; if (!string.IsNullOrWhiteSpace(filePath)) { string? filePathDir = Path.GetDirectoryName(filePath); @@ -38,10 +39,10 @@ internal ZenlessManifestInterceptStream(string? filePath, Stream stream) { Directory.CreateDirectory(filePathDir); } - redirectStream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); + _redirectStream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); return; } - redirectStream = Stream.Null; + _redirectStream = Stream.Null; } public override bool CanRead => true; @@ -56,8 +57,8 @@ internal ZenlessManifestInterceptStream(string? filePath, Stream stream) public override void Flush() { - innerStream.Flush(); - redirectStream.Flush(); + _innerStream.Flush(); + _redirectStream.Flush(); } public override int Read(byte[] buffer, int offset, int count) => InternalRead(buffer.AsSpan(offset, count)); @@ -70,40 +71,40 @@ public override async Task ReadAsync(byte[] buffer, int offset, int count, public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => await InternalReadAsync(buffer, cancellationToken); - private readonly char[] searchStartValuesUtf16 = "\"files\": [".ToCharArray(); - private readonly byte[] searchStartValuesUtf8 = "\"files\": ["u8.ToArray(); - private readonly char[] searchEndValuesUtf16 = "]\r\n}".ToCharArray(); - private readonly byte[] searchEndValuesUtf8 = "]\r\n}"u8.ToArray(); + private readonly char[] _searchStartValuesUtf16 = "\"files\": [".ToCharArray(); + private readonly byte[] _searchStartValuesUtf8 = "\"files\": ["u8.ToArray(); + private readonly char[] _searchEndValuesUtf16 = "]\r\n}".ToCharArray(); + private readonly byte[] _searchEndValuesUtf8 = "]\r\n}"u8.ToArray(); private async ValueTask InternalReadAsync(Memory buffer, CancellationToken token) { - if (isFieldEnd) + if (_isFieldEnd) return 0; Start: // - 8 is important to ensure that the EOF detection is working properly - int toRead = Math.Min(innerBuffer.Length - 8, buffer.Length); - int read = await innerStream.ReadAtLeastAsync(innerBuffer.AsMemory(0, toRead), toRead, false, token); + int toRead = Math.Min(_innerBuffer.Length - 8, buffer.Length); + int read = await _innerStream.ReadAtLeastAsync(_innerBuffer.AsMemory(0, toRead), toRead, false, token); if (read == 0) return 0; - await redirectStream.WriteAsync(innerBuffer, 0, read, token); + await _redirectStream.WriteAsync(_innerBuffer, 0, read, token); int lastIndexOffset; - if (isFieldStart && ((lastIndexOffset = EnsureIsEnd(innerBuffer, read)) > 0)) + if (_isFieldStart && (lastIndexOffset = EnsureIsEnd(_innerBuffer)) > 0) { - innerBuffer.AsSpan(0, lastIndexOffset).CopyTo(buffer.Span); - isFieldEnd = true; + _innerBuffer.AsSpan(0, lastIndexOffset).CopyTo(buffer.Span); + _isFieldEnd = true; return lastIndexOffset; } int offset = 0; - if (!isFieldStart && !(isFieldStart = !((offset = EnsureIsStart(innerBuffer)) < 0))) + if (!_isFieldStart && !(_isFieldStart = !((offset = EnsureIsStart(_innerBuffer)) < 0))) goto Start; - bool isOneGoBufferLoadEnd = read < toRead && isFieldStart && !isFieldEnd; - ReadOnlySpan spanToCopy = isOneGoBufferLoadEnd ? innerBuffer.AsSpan(offset, read - offset).TrimEnd((byte)'}') - : innerBuffer.AsSpan(offset, read - offset); + bool isOneGoBufferLoadEnd = read < toRead && _isFieldStart && !_isFieldEnd; + ReadOnlySpan spanToCopy = isOneGoBufferLoadEnd ? _innerBuffer.AsSpan(offset, read - offset).TrimEnd((byte)'}') + : _innerBuffer.AsSpan(offset, read - offset); spanToCopy.CopyTo(buffer.Span); return isOneGoBufferLoadEnd ? spanToCopy.Length : read - offset; @@ -111,60 +112,62 @@ private async ValueTask InternalReadAsync(Memory buffer, Cancellation private int InternalRead(Span buffer) { - if (isFieldEnd) + if (_isFieldEnd) return 0; Start: // - 8 is important to ensure that the EOF detection is working properly - int toRead = Math.Min(innerBuffer.Length - 8, buffer.Length); - int read = innerStream.ReadAtLeast(innerBuffer.AsSpan(0, toRead), toRead, false); + int toRead = Math.Min(_innerBuffer.Length - 8, buffer.Length); + int read = _innerStream.ReadAtLeast(_innerBuffer.AsSpan(0, toRead), toRead, false); if (read == 0) return 0; - redirectStream.Write(innerBuffer, 0, read); + _redirectStream.Write(_innerBuffer, 0, read); int lastIndexOffset; - if (isFieldStart && ((lastIndexOffset = EnsureIsEnd(innerBuffer, read)) > 0)) + if (_isFieldStart && (lastIndexOffset = EnsureIsEnd(_innerBuffer)) > 0) { - innerBuffer.AsSpan(0, lastIndexOffset).CopyTo(buffer); - isFieldEnd = true; + _innerBuffer.AsSpan(0, lastIndexOffset).CopyTo(buffer); + _isFieldEnd = true; return lastIndexOffset; } int offset = 0; - if (!isFieldStart && !(isFieldStart = !((offset = EnsureIsStart(innerBuffer)) < 0))) + if (!_isFieldStart && !(_isFieldStart = !((offset = EnsureIsStart(_innerBuffer)) < 0))) goto Start; - innerBuffer.AsSpan(offset, read - offset).CopyTo(buffer); + _innerBuffer.AsSpan(offset, read - offset).CopyTo(buffer); return read - offset; } - private int EnsureIsEnd(Span buffer, int read) + private int EnsureIsEnd(Span buffer) { ReadOnlySpan bufferAsChars = MemoryMarshal.Cast(buffer); - int lastIndexOfAnyUtf8 = buffer.LastIndexOf(searchEndValuesUtf8); - if (lastIndexOfAnyUtf8 < searchEndValuesUtf8.Length) + int lastIndexOfAnyUtf8 = buffer.LastIndexOf(_searchEndValuesUtf8); + if (lastIndexOfAnyUtf8 >= _searchEndValuesUtf8.Length) { - int lastIndexOfAnyUtf16 = bufferAsChars.LastIndexOf(searchEndValuesUtf16); - return lastIndexOfAnyUtf16 > 0 ? lastIndexOfAnyUtf16 + 1 : -1; + return lastIndexOfAnyUtf8 + 1; } - return lastIndexOfAnyUtf8 + 1; + int lastIndexOfAnyUtf16 = bufferAsChars.LastIndexOf(_searchEndValuesUtf16); + return lastIndexOfAnyUtf16 > 0 ? lastIndexOfAnyUtf16 + 1 : -1; + } private int EnsureIsStart(Span buffer) { ReadOnlySpan bufferAsChars = MemoryMarshal.Cast(buffer); - int indexOfAnyUtf8 = buffer.IndexOf(searchStartValuesUtf8); - if (indexOfAnyUtf8 < searchStartValuesUtf8.Length) + int indexOfAnyUtf8 = buffer.IndexOf(_searchStartValuesUtf8); + if (indexOfAnyUtf8 >= _searchStartValuesUtf8.Length) { - int indexOfAnyUtf16 = bufferAsChars.IndexOf(searchStartValuesUtf16); - return indexOfAnyUtf16 > 0 ? indexOfAnyUtf16 + (searchStartValuesUtf16.Length - 1) : -1; + return indexOfAnyUtf8 + (_searchStartValuesUtf8.Length - 1); } - return indexOfAnyUtf8 + (searchStartValuesUtf8.Length - 1); + int indexOfAnyUtf16 = bufferAsChars.IndexOf(_searchStartValuesUtf16); + return indexOfAnyUtf16 > 0 ? indexOfAnyUtf16 + (_searchStartValuesUtf16.Length - 1) : -1; + } public override long Seek(long offset, SeekOrigin origin) @@ -184,31 +187,33 @@ public override void Write(byte[] buffer, int offset, int count) public override async ValueTask DisposeAsync() { - await innerStream.DisposeAsync(); - await redirectStream.DisposeAsync(); + await _innerStream.DisposeAsync(); + await _redirectStream.DisposeAsync(); } protected override void Dispose(bool disposing) { - if (disposing) + if (!disposing) { - innerStream?.Dispose(); - redirectStream?.Dispose(); + return; } + + _innerStream.Dispose(); + _redirectStream.Dispose(); } } internal static class ZenlessRepairExtensions { - const string StreamingAssetsPath = "StreamingAssets\\"; - const string AssetTypeAudioPath = StreamingAssetsPath + "Audio\\Windows\\"; - const string AssetTypeBlockPath = StreamingAssetsPath + "Blocks\\"; - const string AssetTypeVideoPath = StreamingAssetsPath + "Video\\HD\\"; + private const string StreamingAssetsPath = @"StreamingAssets\"; + private const string AssetTypeAudioPath = StreamingAssetsPath + @"Audio\Windows\"; + private const string AssetTypeBlockPath = StreamingAssetsPath + @"Blocks\"; + private const string AssetTypeVideoPath = StreamingAssetsPath + @"Video\HD\"; - const string PersistentAssetsPath = "Persistent\\"; - const string AssetTypeAudioPersistentPath = PersistentAssetsPath + "Audio\\Windows\\"; - const string AssetTypeBlockPersistentPath = PersistentAssetsPath + "Blocks\\"; - const string AssetTypeVideoPersistentPath = PersistentAssetsPath + "Video\\HD\\"; + private const string PersistentAssetsPath = @"Persistent\"; + private const string AssetTypeAudioPersistentPath = PersistentAssetsPath + @"Audio\Windows\"; + private const string AssetTypeBlockPersistentPath = PersistentAssetsPath + @"Blocks\"; + private const string AssetTypeVideoPersistentPath = PersistentAssetsPath + @"Video\HD\"; internal static async IAsyncEnumerable MergeAsyncEnumerable(params IAsyncEnumerable[] sources) { @@ -264,12 +269,7 @@ internal static async IAsyncEnumerable RegisterSleepyFileI } internal static IEnumerable RegisterMainCategorizedAssetsToHashSet(this IEnumerable assetEnumerable, List assetIndex, Dictionary hashSet, string baseLocalPath, string baseUrl) - { - foreach (PkgVersionProperties asset in assetEnumerable) - { - yield return ReturnCategorizedYieldValue(hashSet, assetIndex, asset, baseLocalPath, baseUrl); - } - } + => assetEnumerable.Select(asset => ReturnCategorizedYieldValue(hashSet, assetIndex, asset, baseLocalPath, baseUrl)); internal static async IAsyncEnumerable RegisterResCategorizedAssetsToHashSetAsync(this IAsyncEnumerable assetEnumerable, List assetIndex, Dictionary hashSet, string baseLocalPath, string basePatchUrl, string baseResUrl) { @@ -301,32 +301,27 @@ internal static async IAsyncEnumerable RegisterSleepyFileI _ => FileType.Generic }; - if (!relTypeRelativePath.IsEmpty) + if (relTypeRelativePath.IsEmpty) { - string relTypePath = assetType switch - { - RepairAssetType.Audio => AssetTypeAudioPath, - RepairAssetType.Block => AssetTypeBlockPath, - RepairAssetType.Video => AssetTypeVideoPath, - _ => throw new NotSupportedException() - }; + return asRemoteProperty; + } - string relTypeRelativePathStr = relTypeRelativePath.ToString(); - if (!hashSet.TryAdd(relTypeRelativePathStr, asRemoteProperty) && asset.isPatch) - { - FilePropertiesRemote existingValue = hashSet[relTypeRelativePathStr]; - int indexOf = assetIndex.IndexOf(existingValue); - if (indexOf < -1) - return asRemoteProperty; + string relTypeRelativePathStr = relTypeRelativePath.ToString(); + if (hashSet.TryAdd(relTypeRelativePathStr, asRemoteProperty) || !asset.isPatch) + { + return asRemoteProperty; + } - assetIndex[indexOf] = asRemoteProperty; - hashSet[relTypeRelativePathStr] = asRemoteProperty; + FilePropertiesRemote existingValue = hashSet[relTypeRelativePathStr]; + int indexOf = assetIndex.IndexOf(existingValue); + if (indexOf < -1) + return asRemoteProperty; - return null; - } - } + assetIndex[indexOf] = asRemoteProperty; + hashSet[relTypeRelativePathStr] = asRemoteProperty; + + return null; - return asRemoteProperty; } private static FilePropertiesRemote GetNormalizedFilePropertyTypeBased(string remoteParentURL, @@ -385,12 +380,7 @@ internal static ReadOnlySpan GetAssetRelativePath(this FilePropertiesRemot assetType = RepairAssetType.Video; } - if (indexOfOffset >= 0) - { - return asset.N.AsSpan(indexOfOffset); - } - - return ReadOnlySpan.Empty; + return indexOfOffset >= 0 ? asset.N.AsSpan(indexOfOffset) : ReadOnlySpan.Empty; } } } diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs index 3cd433279..f39df63fd 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -47,7 +47,7 @@ private async Task Fetch(List assetIndex, CancellationToke if (!IsCacheUpdateMode) { // Get the primary manifest - await GetPrimaryManifest(downloadClient, hashSet, token, assetIndex); + await GetPrimaryManifest(hashSet, token, assetIndex); } // Execute on non-recover main mode @@ -77,7 +77,7 @@ await GetResManifest(downloadClient, hashSet, token, assetIndex, #endregion #region PrimaryManifest - private async Task GetPrimaryManifest(DownloadClient downloadClient, Dictionary hashSet, CancellationToken token, List assetIndex) + private async Task GetPrimaryManifest(Dictionary hashSet, CancellationToken token, List assetIndex) { // If it's using cache update mode, then return since we don't need to add manifest // from pkg_version on cache update mode. @@ -103,7 +103,7 @@ private async Task GetPrimaryManifest(DownloadClient downloadClient, Dictionary< _gameRepoURL = value; } // If the base._isVersionOverride is true, then throw. This sanity check is required if the delta patch is being performed. - catch when (base._isVersionOverride) { throw; } + catch when (_isVersionOverride) { throw; } // Fetch the asset index from CDN // Set asset index URL @@ -112,30 +112,28 @@ private async Task GetPrimaryManifest(DownloadClient downloadClient, Dictionary< // Start downloading asset index using FallbackCDNUtil and return its stream await Task.Run(async () => { - await using (BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlIndex, token)) + await using BridgedNetworkStream stream = await FallbackCDNUtil.TryGetCDNFallbackStream(urlIndex, token); + if (stream != null) { - if (stream != null) - { - // Deserialize asset index and set it to list - AssetIndexV2 parserTool = new AssetIndexV2(); - pkgVersion = parserTool.Deserialize(stream, out DateTime timestamp); - Logger.LogWriteLine($"Asset index timestamp: {timestamp}", LogType.Default, true); - } - - // Convert the pkg version list to asset index - foreach (FilePropertiesRemote entry in pkgVersion.RegisterMainCategorizedAssetsToHashSet(assetIndex, hashSet, _gamePath, _gameRepoURL)) - { - // If entry is null (means, an existing entry has been overwritten), then next - if (entry == null) - continue; + // Deserialize asset index and set it to list + AssetIndexV2 parserTool = new AssetIndexV2(); + pkgVersion = parserTool.Deserialize(stream, out DateTime timestamp); + Logger.LogWriteLine($"Asset index timestamp: {timestamp}", LogType.Default, true); + } - assetIndex.Add(entry); - } + // Convert the pkg version list to asset index + foreach (FilePropertiesRemote entry in pkgVersion.RegisterMainCategorizedAssetsToHashSet(assetIndex, hashSet, _gamePath, _gameRepoURL)) + { + // If entry is null (means, an existing entry has been overwritten), then next + if (entry == null) + continue; - // Clear the pkg version list - pkgVersion.Clear(); + assetIndex.Add(entry); } - }).ConfigureAwait(false); + + // Clear the pkg version list + pkgVersion.Clear(); + }, token).ConfigureAwait(false); } private async Task> FetchMetadata(CancellationToken token) @@ -191,25 +189,25 @@ private async Task GetResManifest( infoKindSilence.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), assetIndex, hashSet, - Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + Path.Combine(_gamePath, $@"{ExecutableName}_Data\"), infoKindSilence.BaseUrl, baseResUrl); IAsyncEnumerable infoDataEnumerable = EnumerateResManifestToAssetIndexAsync( infoKindData.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), assetIndex, hashSet, - Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + Path.Combine(_gamePath, $@"{ExecutableName}_Data\"), infoKindData.BaseUrl, baseResUrl); await foreach (FilePropertiesRemote asset in ZenlessRepairExtensions - .MergeAsyncEnumerable(infoSilenceEnumerable, infoDataEnumerable)) + .MergeAsyncEnumerable(infoSilenceEnumerable, infoDataEnumerable).WithCancellation(token)) { assetIndex.Add(asset); } // Create base revision file string baseRevisionFile = Path.Combine(persistentPath, "base_revision"); - File.WriteAllText(EnsureCreationOfDirectory(baseRevisionFile), infoKindBase.RevisionStamp); + await File.WriteAllTextAsync(EnsureCreationOfDirectory(baseRevisionFile), infoKindBase.RevisionStamp, token); } // Fetch repair files else @@ -218,7 +216,7 @@ private async Task GetResManifest( infoKindRes.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), assetIndex, hashSet, - Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + Path.Combine(_gamePath, $@"{ExecutableName}_Data\"), infoKindRes.BaseUrl, baseResUrl); IAsyncEnumerable infoAudioEnumerable = GetOnlyInstalledAudioPack( @@ -226,12 +224,12 @@ private async Task GetResManifest( infoKindAudio.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), assetIndex, hashSet, - Path.Combine(_gamePath, string.Format(@"{0}_Data\", ExecutableName)), + Path.Combine(_gamePath, $@"{ExecutableName}_Data\"), infoKindAudio.BaseUrl, baseResUrl) ); await foreach (FilePropertiesRemote asset in ZenlessRepairExtensions - .MergeAsyncEnumerable(infoResEnumerable, infoAudioEnumerable)) + .MergeAsyncEnumerable(infoResEnumerable, infoAudioEnumerable).WithCancellation(token)) { assetIndex.Add(asset); } @@ -259,8 +257,8 @@ private string GetBaseResUrl(string baseUrl, string stampRevision) if (startMarkOffset < 0 || endMarkOffset < 0) throw new IndexOutOfRangeException($"Start mark offset or End mark offset was not found! Start: {startMarkOffset} End: {endMarkOffset}"); - ReadOnlySpan startMarkSpan = baseUrlSpan.Slice(0, startMarkOffset); - ReadOnlySpan endMarkSpan = baseUrlSpan.Slice(endMarkOffset); + ReadOnlySpan startMarkSpan = baseUrlSpan[..startMarkOffset]; + ReadOnlySpan endMarkSpan = baseUrlSpan[endMarkOffset..]; return ConverterTool.CombineURLFromString(startMarkSpan, $"output_{stampRevision}", endMarkSpan.ToString()); } @@ -274,12 +272,12 @@ private async Task TryGetSleepyInfo(HttpClient client, PresetConfig "channel_id", "sub_channel_id", $"{gamePreset.ChannelID}", - $"{gamePreset.SubChannelID}").TrimStart('.'); - string gatewayUrlTemplate = ('.' + gamePreset.GameGatewayURLTemplate).AssociateGameAndLauncherId( + $"{gamePreset.SubChannelID}")!.TrimStart('.'); + string gatewayUrlTemplate = ('.' + gamePreset.GameGatewayURLTemplate)!.AssociateGameAndLauncherId( "channel_id", "sub_channel_id", $"{gamePreset.ChannelID}", - $"{gamePreset.SubChannelID}").TrimStart('.'); + $"{gamePreset.SubChannelID}")!.TrimStart('.'); // Initialize property SleepyProperty sleepyProperty = SleepyProperty.Create( @@ -317,18 +315,18 @@ private async Task TryGetSleepyInfo(HttpClient client, PresetConfig private async IAsyncEnumerable GetOnlyInstalledAudioPack(IAsyncEnumerable enumerable) { - const string WindowsFullPath = "Audio\\Windows\\Full"; + const string windowsFullPath = @"Audio\Windows\Full"; string[] audioList = GetCurrentAudioLangList("Jp"); SearchValues searchExclude = SearchValues.Create(audioList, StringComparison.OrdinalIgnoreCase); await foreach (FilePropertiesRemote asset in enumerable) { - if (IsAudioFileIncluded(WindowsFullPath, searchExclude, asset)) + if (IsAudioFileIncluded(windowsFullPath, searchExclude, asset)) yield return asset; } } - private static bool IsAudioFileIncluded(string WindowsFullPath, SearchValues searchInclude, FilePropertiesRemote asset) + private static bool IsAudioFileIncluded(string windowsFullPath, SearchValues searchInclude, FilePropertiesRemote asset) { // If it's not audio file, then return ReadOnlySpan relPath = asset.GetAssetRelativePath(out RepairAssetType assetType); @@ -337,22 +335,20 @@ private static bool IsAudioFileIncluded(string WindowsFullPath, SearchValues dirRelPath = Path.GetDirectoryName(relPath); // If non language audio file, then return - if (dirRelPath.EndsWith(WindowsFullPath)) + if (dirRelPath.EndsWith(windowsFullPath)) return true; - // Check if non full path, then return - int indexOf = dirRelPath.LastIndexOf(WindowsFullPath); + // Check if non-full path, then return + int indexOf = dirRelPath.LastIndexOf(windowsFullPath); if (indexOf < 0) return true; // If the index is more than WindowsFullPath.Length (included), then return ReadOnlySpan lastSequence = Path.GetFileName(dirRelPath); int indexOfAny = lastSequence.IndexOfAny(searchInclude); - if (indexOfAny == 0) - return true; // Otherwise, return false - return false; + return indexOfAny == 0; } #endregion From acda9592f2948511f40f67f2ffdbf7e34de66b92 Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sun, 13 Oct 2024 23:18:51 +0700 Subject: [PATCH 16/18] Add delta-patch support --- .../Zenless/ZenlessInstall.cs | 56 ++++++++++++------- .../RepairManagement/Zenless/ZenlessRepair.cs | 1 - 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.cs b/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.cs index c84e19190..40ad1759c 100644 --- a/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.cs +++ b/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.cs @@ -24,7 +24,8 @@ namespace CollapseLauncher.InstallManager.Zenless internal class ZenlessInstall : InstallManagerBase { #region Private Properties - private ZenlessSettings? ZenlessSettings { get; set; } + private ZenlessSettings? ZenlessSettings { get; } + private ZenlessRepair? ZenlessGameRepairManager { get; set; } #endregion #region Override Properties @@ -55,20 +56,6 @@ protected override string? _gameAudioLangListPath } } - private string? _gameAudioLangListPathAlternate - { - get - { - // If the persistent folder is not exist, then return null - if (!Directory.Exists(_gameDataPersistentPath)) - return null; - - // Get the audio lang path index - string audioLangPath = _gameAudioLangListPathAlternateStatic; - return File.Exists(audioLangPath) ? audioLangPath : null; - } - } - protected override string _gameAudioLangListPathStatic => Path.Combine(_gameDataPersistentPath, "audio_lang_launcher"); private string _gameAudioLangListPathAlternateStatic => @@ -82,10 +69,44 @@ public ZenlessInstall(UIElement parentUI, IGameVersionCheck GameVersionManager, } #region Override Methods - StartPackageInstallationInner + + public override async ValueTask StartPackageVerification(List gamePackage) + { + IsRunning = true; + + // Get the delta patch confirmation if the property is not null + if (_gameDeltaPatchProperty == null) + return await base.StartPackageVerification(gamePackage); + + // If the confirm is 1 (verified) or -1 (cancelled), then return the code + int deltaPatchConfirm = await ConfirmDeltaPatchDialog(_gameDeltaPatchProperty, + ZenlessGameRepairManager = GetGameRepairInstance(_gameDeltaPatchProperty.SourceVer) as ZenlessRepair); + if (deltaPatchConfirm is -1 or 1) + { + return deltaPatchConfirm; + } + + // If no delta patch is happening as deltaPatchConfirm returns 0 (normal update), then do the base verification + return await base.StartPackageVerification(gamePackage); + } + + protected override IRepair GetGameRepairInstance(string? versionString) => + new ZenlessRepair(_parentUI, + _gameVersionManager, ZenlessSettings!, true, + versionString); + protected override async Task StartPackageInstallationInner(List? gamePackage = null, bool isOnlyInstallPackage = false, bool doNotDeleteZipExplicit = false) { + // If the delta patch is performed, then return + if (!isOnlyInstallPackage && await StartDeltaPatch(ZenlessGameRepairManager, false, true)) + { + // Update the audio package list after delta patch has been initiated + WriteAudioLangList(_gameDeltaPatchPreReqList); + return; + } + // Run the base installation process await base.StartPackageInstallationInner(gamePackage, isOnlyInstallPackage, doNotDeleteZipExplicit); @@ -184,7 +205,6 @@ private string GetLanguageStringByLocaleCodeAlternate(string localeCode) #endregion #region Override Methods - UninstallGame - protected override UninstallGameProperty AssignUninstallFolders() { return new UninstallGameProperty @@ -199,8 +219,6 @@ protected override UninstallGameProperty AssignUninstallFolders() foldersToKeepInData = ["ScreenShots"] }; } - #endregion } -} -#nullable restore \ No newline at end of file +} \ No newline at end of file diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs index bfda8f6ac..aa89166e1 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs @@ -31,7 +31,6 @@ internal partial class ZenlessRepair : ProgressBase, IRepa private ZenlessSettings? GameSettings { get; init; } private string GameDataPersistentPath { get => Path.Combine(_gamePath, string.Format(_assetGamePersistentPath, ExecutableName)); } - private string GameDataStreamingPath { get => Path.Combine(_gamePath, string.Format(_assetGameStreamingPath, ExecutableName)); } protected string? _gameAudioLangListPath { From 2b82f825000427a3d8353ba152fb40c92e38ff2e Mon Sep 17 00:00:00 2001 From: Kemal Setya Adhi Date: Sat, 19 Oct 2024 20:33:13 +0700 Subject: [PATCH 17/18] CodeQA --- .../GameManagement/GameVersion/Zenless/VersionCheck.cs | 3 +-- CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs | 9 ++------- .../RepairManagement/Zenless/ZenlessRepair.Check.cs | 2 +- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs index 8d07d328c..fbbb8aa0e 100644 --- a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs +++ b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs @@ -54,8 +54,7 @@ private void InitializeSleepy(PresetConfig gamePreset) } // Try serve a dinner and if it fails, then GET OUT! - bool isServed = DataCooker.IsServeV3Data(keyUtf8Base64); - if (!isServed) + if (!DataCooker.IsServeV3Data(keyUtf8Base64)) goto QuitFail; // Enjoy the meal (i guess?) diff --git a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs index 0be291321..e97fdb341 100644 --- a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs +++ b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs @@ -179,7 +179,6 @@ internal static void ServeV3Data(ReadOnlySpan data, Span private static int DecompressDataFromBrotli(Span outData, int compressedSize, int decompressedSize, ReadOnlySpan dataRawBuffer) { - int dataWritten; BrotliDecoder decoder = new BrotliDecoder(); int offset = 0; @@ -197,14 +196,11 @@ private static int DecompressDataFromBrotli(Span outData, int compressedSi throw new DataMisalignedException("Decompressed data is misaligned!"); } - dataWritten = decompressedWritten; - return dataWritten; + return decompressedWritten; } private static unsafe int DecompressDataFromZstd(Span outData, int compressedSize, int decompressedSize, ReadOnlySpan dataRawBuffer) { - int dataWritten; - fixed (byte* inputBuffer = &dataRawBuffer[0]) fixed (byte* outputBuffer = &outData[0]) { @@ -228,8 +224,7 @@ private static unsafe int DecompressDataFromZstd(Span outData, int compres throw new DataMisalignedException("Decompressed data is misaligned!"); } - dataWritten = decompressedWritten; - return dataWritten; + return decompressedWritten; } } } diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs index 7f54aad28..e880973c6 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Check.cs @@ -80,7 +80,7 @@ private async ValueTask CheckGenericAssetType(FilePropertiesRemote asset, List Date: Sat, 19 Oct 2024 20:33:41 +0700 Subject: [PATCH 18/18] Adding fallback to read pkg_version directly if MetaRepo one is missing --- .../Zenless/ZenlessRepair.Extensions.cs | 28 +++++++++++++++- .../Zenless/ZenlessRepair.Fetch.cs | 33 +++++++++++++++++-- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs index cd5950eed..c9f509283 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs @@ -1,4 +1,5 @@ -using Hi3Helper.Data; +using Hi3Helper; +using Hi3Helper.Data; using Hi3Helper.EncTool.Parser.AssetIndex; using Hi3Helper.EncTool.Parser.Sleepy; using Hi3Helper.Shared.ClassStruct; @@ -271,6 +272,15 @@ internal static async IAsyncEnumerable RegisterSleepyFileI internal static IEnumerable RegisterMainCategorizedAssetsToHashSet(this IEnumerable assetEnumerable, List assetIndex, Dictionary hashSet, string baseLocalPath, string baseUrl) => assetEnumerable.Select(asset => ReturnCategorizedYieldValue(hashSet, assetIndex, asset, baseLocalPath, baseUrl)); + internal static async IAsyncEnumerable RegisterMainCategorizedAssetsToHashSetAsync(this IAsyncEnumerable assetEnumerable, List assetIndex, Dictionary hashSet, string baseLocalPath, string baseUrl, [EnumeratorCancellation] CancellationToken token = default) + + { + await foreach (PkgVersionProperties asset in assetEnumerable.WithCancellation(token)) + { + yield return ReturnCategorizedYieldValue(hashSet, assetIndex, asset, baseLocalPath, baseUrl); + } + } + internal static async IAsyncEnumerable RegisterResCategorizedAssetsToHashSetAsync(this IAsyncEnumerable assetEnumerable, List assetIndex, Dictionary hashSet, string baseLocalPath, string basePatchUrl, string baseResUrl) { await foreach (PkgVersionProperties asset in assetEnumerable) @@ -382,5 +392,21 @@ internal static ReadOnlySpan GetAssetRelativePath(this FilePropertiesRemot return indexOfOffset >= 0 ? asset.N.AsSpan(indexOfOffset) : ReadOnlySpan.Empty; } + + internal static async IAsyncEnumerable EnumerateStreamToPkgVersionPropertiesAsync( + this Stream stream, + [EnumeratorCancellation] CancellationToken token = default) + { + using TextReader reader = new StreamReader(stream); + string? currentLine; + while (!string.IsNullOrEmpty(currentLine = await reader.ReadLineAsync(token))) + { + PkgVersionProperties? property = currentLine.Deserialize(CoreLibraryJSONContext.Default.PkgVersionProperties); + if (property == null) + continue; + + yield return property; + } + } } } diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs index f39df63fd..d9174c526 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -94,9 +94,38 @@ private async Task GetPrimaryManifest(Dictionary h Dictionary repoMetadata = await FetchMetadata(token); // Check for manifest. If it doesn't exist, then throw and warn the user - if (!(repoMetadata.TryGetValue(_gameVersion.VersionString, out var value))) + 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!"); + // If version override is on, then throw + if (_isVersionOverride) + throw new VersionNotFoundException($"Manifest for {_gameVersionManager.GamePreset.ZoneName} (version: {_gameVersion.VersionString}) doesn't exist! Please contact @neon-nyan or open an issue for this!"); + + // Otherwise, fallback to the launcher's pkg_version + RegionResourceVersion latestZipApi = GameVersionManagerCast?.GetGameLatestZip(GameInstallStateEnum.Installed).FirstOrDefault(); + string latestZipApiUrl = latestZipApi?.decompressed_path; + + // Throw if latest zip api URL returns null + if (string.IsNullOrEmpty(latestZipApiUrl)) + throw new NullReferenceException("Cannot find latest zip api url while failing back to pkg_version"); + + // Assign the URL based on the version + _gameRepoURL = latestZipApiUrl; + + // Combine pkg_version url + latestZipApiUrl = ConverterTool.CombineURLFromString(latestZipApiUrl, "pkg_version"); + + // Read pkg_version stream response + await using Stream stream = await FallbackCDNUtil.GetHttpStreamFromResponse(latestZipApiUrl, token); + await foreach (FilePropertiesRemote asset in stream + .EnumerateStreamToPkgVersionPropertiesAsync(token) + .RegisterMainCategorizedAssetsToHashSetAsync(assetIndex, hashSet, _gamePath, _gameRepoURL, token)) + { + // If entry is null (means, an existing entry has been overwritten), then next + if (asset == null) + continue; + + assetIndex.Add(asset); + } } // Assign the URL based on the version