diff --git a/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs b/CollapseLauncher/Classes/CachesManagement/Zenless/ZenlessCache.cs new file mode 100644 index 000000000..d51a1e90b --- /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 StartRepairRoutine(showInteractivePrompt); + } +} diff --git a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs index f1ce6bb50..fbbb8aa0e 100644 --- a/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs +++ b/CollapseLauncher/Classes/GameManagement/GameVersion/Zenless/VersionCheck.cs @@ -1,17 +1,124 @@ +using CollapseLauncher.Helper.Metadata; +using Hi3Helper; 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; namespace CollapseLauncher.GameVersioning { internal sealed class GameTypeZenlessVersion : GameVersionBase { #region Properties + internal RSA SleepyInstance { get; set; } + internal string SleepyIdentity { get; set; } + internal string SleepyArea { 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! + if (!DataCooker.IsServeV3Data(keyUtf8Base64)) + 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); + + // 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 felt food poisoned since last night's dinner, 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) + { +#if !DEBUG + config.IsRepairEnabled = false; + config.IsCacheUpdateEnabled = false; +#endif + } + } + #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 49ba591ef..d70587556 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, GameName, GameRegion); - _GameSettings = new ZenlessSettings(_GameVersion); - _GameCache = null; - _GameRepair = null; - _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!"); - } - - _GamePlaytime = new Playtime(_GameVersion, _GameSettings); + PresetConfig gamePreset = LauncherMetadataHelper.LauncherMetadataConfig[gameName][gameRegion]; + + _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, _GameSettings); } internal RegionResourceProp _APIResouceProp { get; set; } diff --git a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs index 6a9c49256..e97fdb341 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,56 @@ internal static void ServeV3Data(ReadOnlySpan data, Span } } } + + private static int DecompressDataFromBrotli(Span outData, int compressedSize, int decompressedSize, ReadOnlySpan dataRawBuffer) + { + 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!"); + } + + return decompressedWritten; + } + + private static unsafe int DecompressDataFromZstd(Span outData, int compressedSize, int decompressedSize, ReadOnlySpan dataRawBuffer) + { + 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; + while ((read = decompStream.Read(buffer)) > 0) + { + outputStream.Write(buffer, 0, read); + decompressedWritten += read; + } + + if (decompressedSize != decompressedWritten) + { + throw new DataMisalignedException("Decompressed data is misaligned!"); + } + + return decompressedWritten; + } + } } } \ No newline at end of file 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/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/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/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/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..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; @@ -1093,6 +1096,40 @@ protected virtual async ValueTask CheckHashAsync(Stream stream, HashAlgo // Return computed hash byte return hashProvider.Hash; } + + 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 = GC.AllocateUninitializedArray(bufferLen); + + // 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 new file mode 100644 index 000000000..e880973c6 --- /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.IO.Hashing; +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.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; + } + }); + } + catch (AggregateException ex) + { + throw ex.Flatten().InnerExceptions.First(); + } + + // 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 + 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 = asset.CRCArray.Length > 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)) + { + _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.Block => RepairAssetType.Block, + FileType.Audio => RepairAssetType.Audio, + FileType.Video => RepairAssetType.Video, + _ => 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..c9f509283 --- /dev/null +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Extensions.cs @@ -0,0 +1,412 @@ +using Hi3Helper; +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.Linq; +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 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; + 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; + if (_isFieldStart && (lastIndexOffset = EnsureIsEnd(_innerBuffer)) > 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; + if (_isFieldStart && (lastIndexOffset = EnsureIsEnd(_innerBuffer)) > 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) + { + ReadOnlySpan bufferAsChars = MemoryMarshal.Cast(buffer); + + int lastIndexOfAnyUtf8 = buffer.LastIndexOf(_searchEndValuesUtf8); + if (lastIndexOfAnyUtf8 >= _searchEndValuesUtf8.Length) + { + 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) + { + 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) + { + 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() + { + await _innerStream.DisposeAsync(); + await _redirectStream.DisposeAsync(); + } + + protected override void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + + _innerStream.Dispose(); + _redirectStream.Dispose(); + } + } + + internal static class ZenlessRepairExtensions + { + 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\"; + + 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) + { + 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) + => 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) + { + string baseLocalPathMerged = Path.Combine(baseLocalPath, asset.isPatch ? PersistentAssetsPath : StreamingAssetsPath); + + yield return ReturnCategorizedYieldValue(hashSet, assetIndex, asset, baseLocalPathMerged, basePatchUrl, baseResUrl); + } + } + + 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, + 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) + { + return asRemoteProperty; + } + + string relTypeRelativePathStr = relTypeRelativePath.ToString(); + if (hashSet.TryAdd(relTypeRelativePathStr, asRemoteProperty) || !asset.isPatch) + { + return asRemoteProperty; + } + + FilePropertiesRemote existingValue = hashSet[relTypeRelativePathStr]; + int indexOf = assetIndex.IndexOf(existingValue); + if (indexOf < -1) + return asRemoteProperty; + + assetIndex[indexOf] = asRemoteProperty; + hashSet[relTypeRelativePathStr] = asRemoteProperty; + + return null; + + } + + 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; + 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; + } + + 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 new file mode 100644 index 000000000..d9174c526 --- /dev/null +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -0,0 +1,496 @@ +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.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(); + + // 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); + + // Create a hash set to overwrite local files + Dictionary hashSet = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // If not in cache mode, then fetch main package + if (!IsCacheUpdateMode) + { + // Get the primary manifest + await GetPrimaryManifest(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); + + // Remove plugin from assetIndex + // Skip the removal for Delta-Patch + EliminatePluginAssetIndex(assetIndex); + } + } + } + #endregion + + #region PrimaryManifest + 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. + if (IsCacheUpdateMode) + return; + + // 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)) + { + // 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 + _gameRepoURL = value; + } + // If the base._isVersionOverride is true, then throw. This sanity check is required if the delta patch is being performed. + catch when (_isVersionOverride) { throw; } + + // Fetch the asset index from CDN + // Set asset index URL + string urlIndex = string.Format(LauncherConfig.AppGameRepairIndexURLPrefix, _gameVersionManager.GamePreset.ProfileName, _gameVersion.VersionString) + ".binv2"; + + // Start downloading asset index using FallbackCDNUtil and return its stream + await Task.Run(async () => + { + 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 = 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; + + assetIndex.Add(entry); + } + + // Clear the pkg version list + pkgVersion.Clear(); + }, token).ConfigureAwait(false); + } + + 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, + Dictionary hashSet, + CancellationToken token, + List assetIndex, + bool throwIfError) + { + 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); + SleepyFileInfoResult infoKindRes = sleepyInfo.GetFileInfoResult(FileInfoKind.Res); + SleepyFileInfoResult infoKindAudio = sleepyInfo.GetFileInfoResult(FileInfoKind.Audio); + SleepyFileInfoResult infoKindBase = sleepyInfo.GetFileInfoResult(FileInfoKind.Base); + + // Create non-patch URL + string baseResUrl = GetBaseResUrl(infoKindBase.BaseUrl, infoKindBase.RevisionStamp); + + // Fetch cache files + if (IsCacheUpdateMode) + { + IAsyncEnumerable infoSilenceEnumerable = EnumerateResManifestToAssetIndexAsync( + infoKindSilence.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, $@"{ExecutableName}_Data\"), + infoKindSilence.BaseUrl, baseResUrl); + + IAsyncEnumerable infoDataEnumerable = EnumerateResManifestToAssetIndexAsync( + infoKindData.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, $@"{ExecutableName}_Data\"), + infoKindData.BaseUrl, baseResUrl); + + await foreach (FilePropertiesRemote asset in ZenlessRepairExtensions + .MergeAsyncEnumerable(infoSilenceEnumerable, infoDataEnumerable).WithCancellation(token)) + { + assetIndex.Add(asset); + } + + // Create base revision file + string baseRevisionFile = Path.Combine(persistentPath, "base_revision"); + await File.WriteAllTextAsync(EnsureCreationOfDirectory(baseRevisionFile), infoKindBase.RevisionStamp, token); + } + // Fetch repair files + else + { + IAsyncEnumerable infoResEnumerable = EnumerateResManifestToAssetIndexAsync( + infoKindRes.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, $@"{ExecutableName}_Data\"), + infoKindRes.BaseUrl, baseResUrl); + + IAsyncEnumerable infoAudioEnumerable = GetOnlyInstalledAudioPack( + EnumerateResManifestToAssetIndexAsync( + infoKindAudio.RegisterSleepyFileInfoToManifest(client, assetIndex, true, persistentPath, token), + assetIndex, + hashSet, + Path.Combine(_gamePath, $@"{ExecutableName}_Data\"), + infoKindAudio.BaseUrl, baseResUrl) + ); + + await foreach (FilePropertiesRemote asset in ZenlessRepairExtensions + .MergeAsyncEnumerable(infoResEnumerable, infoAudioEnumerable).WithCancellation(token)) + { + 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[..startMarkOffset]; + ReadOnlySpan endMarkSpan = baseUrlSpan[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) + { + 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)], + dispatchUrlTemplate, + targetServerName ?? fallbackServerName, + gatewayUrlTemplate, + 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); + + // Otherwise, return false + return indexOfAny == 0; + } + #endregion + + #region Utilities +#nullable enable + private async IAsyncEnumerable EnumerateResManifestToAssetIndexAsync( + IAsyncEnumerable pkgVersion, + List assetIndex, + Dictionary hashSet, + string baseLocalPath, + string basePatchUrl, + string baseResUrl) + { + 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) + continue; + + yield return entry; + } + } +#nullable restore + + 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 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 + 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); + } + + 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 returnValueAlternate; + } + + 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..70e0eccea --- /dev/null +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Repair.cs @@ -0,0 +1,131 @@ +using CollapseLauncher.Helper; +using Hi3Helper; +using Hi3Helper.Data; +using Hi3Helper.Http; +using Hi3Helper.Shared.ClassStruct; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +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.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) + }; + + // 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.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) + }; + + // 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.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.Block ? 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..aa89166e1 --- /dev/null +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.cs @@ -0,0 +1,168 @@ +using CollapseLauncher.GameSettings.Zenless; +using CollapseLauncher.GameVersioning; +using CollapseLauncher.Interfaces; +using Hi3Helper; +using Hi3Helper.Data; +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"; + + // 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; } + + private string GameDataPersistentPath { get => Path.Combine(_gamePath, string.Format(_assetGamePersistentPath, 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, 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; + } + #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/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessResManifest.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessResManifest.cs new file mode 100644 index 000000000..1f9350522 --- /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; } // classic + + [JsonPropertyName("fileSize")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public long FileSize { get; set; } + + [JsonPropertyName("isPatch")] + public bool IsPersistentFile { get; set; } + + [JsonPropertyName("tags")] + public int[] Tags { get; set; } + } +} diff --git a/CollapseLauncher/packages.lock.json b/CollapseLauncher/packages.lock.json index e036ae055..df2d4ca49 100644 --- a/CollapseLauncher/packages.lock.json +++ b/CollapseLauncher/packages.lock.json @@ -415,7 +415,7 @@ "dependencies": { "Google.Protobuf": "[3.28.2, )", "Hi3Helper.Http": "[2.0.0, )", - "System.IO.Hashing": "[9.0.0-rc.1.24431.7, )" + "System.IO.Hashing": "[9.0.0-rc.2.24473.5, )" } }, "hi3helper.http": { @@ -435,7 +435,7 @@ "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.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, )" @@ -460,7 +460,7 @@ "type": "Project", "dependencies": { "CommunityToolkit.WinUI.Triggers": "[8.1.240916, )", - "Microsoft.Web.WebView2": "[1.0.2783-prerelease, )", + "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/Hi3Helper.CommunityToolkit/ImageCropper/Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper.csproj b/Hi3Helper.CommunityToolkit/ImageCropper/Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper.csproj index 29511abdf..72f887392 100644 --- a/Hi3Helper.CommunityToolkit/ImageCropper/Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper.csproj +++ b/Hi3Helper.CommunityToolkit/ImageCropper/Hi3Helper.CommunityToolkit.WinUI.Controls.ImageCropper.csproj @@ -34,7 +34,7 @@ - + diff --git a/Hi3Helper.CommunityToolkit/ImageCropper/packages.lock.json b/Hi3Helper.CommunityToolkit/ImageCropper/packages.lock.json index bbf4825d1..b59a2f7e0 100644 --- a/Hi3Helper.CommunityToolkit/ImageCropper/packages.lock.json +++ b/Hi3Helper.CommunityToolkit/ImageCropper/packages.lock.json @@ -43,9 +43,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.Windows.CsWinRT": { "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 4a2b0d3e0..7240ced86 100644 --- a/Hi3Helper.CommunityToolkit/SettingsControls/Hi3Helper.CommunityToolkit.WinUI.Controls.SettingsControls.csproj +++ b/Hi3Helper.CommunityToolkit/SettingsControls/Hi3Helper.CommunityToolkit.WinUI.Controls.SettingsControls.csproj @@ -34,7 +34,7 @@ - + diff --git a/Hi3Helper.CommunityToolkit/SettingsControls/packages.lock.json b/Hi3Helper.CommunityToolkit/SettingsControls/packages.lock.json index cd3418c61..2c32aab90 100644 --- a/Hi3Helper.CommunityToolkit/SettingsControls/packages.lock.json +++ b/Hi3Helper.CommunityToolkit/SettingsControls/packages.lock.json @@ -21,9 +21,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.Windows.CsWinRT": { "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/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; } diff --git a/Hi3Helper.Core/packages.lock.json b/Hi3Helper.Core/packages.lock.json index 4a7380f45..f9986f092 100644 --- a/Hi3Helper.Core/packages.lock.json +++ b/Hi3Helper.Core/packages.lock.json @@ -21,15 +21,15 @@ }, "System.IO.Hashing": { "type": "Transitive", - "resolved": "9.0.0-rc.1.24431.7", - "contentHash": "giMiDNlne8WcLG7rsEfrETaukDUTBPn8a97QRo9LYR71O0Rb4SJ6TbfGdQ9oiBYldVPSAbSQANb5ThLZOo408Q==" + "resolved": "9.0.0-rc.2.24473.5", + "contentHash": "BWkkIwLhG75+RnyBHGQd0Vrti8wqIBeNiAmHQrb6UiFny6qEQ+z6r61bFAOubw8dbp5S8nQrTS/wjJtpolkYTA==" }, "hi3helper.enctool": { "type": "Project", "dependencies": { "Google.Protobuf": "[3.28.2, )", "Hi3Helper.Http": "[2.0.0, )", - "System.IO.Hashing": "[9.0.0-rc.1.24431.7, )" + "System.IO.Hashing": "[9.0.0-rc.2.24473.5, )" } }, "hi3helper.http": { diff --git a/Hi3Helper.EncTool b/Hi3Helper.EncTool index d692d38e9..c48eae7f8 160000 --- a/Hi3Helper.EncTool +++ b/Hi3Helper.EncTool @@ -1 +1 @@ -Subproject commit d692d38e9770e75a2b11c1cee5253f9be128517f +Subproject commit c48eae7f824c427d8f83cdd3ac475e976eacc66f