diff --git a/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs b/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs index 17fc990e8..0e7cae51b 100644 --- a/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs +++ b/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs @@ -142,9 +142,7 @@ internal static FileInfo EnsureCreationOfDirectory(this FileInfo filePath) try { - if (directoryInfo is { Exists: false }) - directoryInfo.Create(); - + directoryInfo?.Create(); return filePath; } finally diff --git a/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.Sophon.cs b/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.Sophon.cs index 9f65f7d6f..a0ca1d281 100644 --- a/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.Sophon.cs +++ b/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.Sophon.cs @@ -555,7 +555,7 @@ private async Task ConfirmAdditionalInstallDataPackageFiles( .Where(x => matchingFieldsList.Contains(x.MatchingField, StringComparer.OrdinalIgnoreCase)) .Sum(x => { - var firstTag = x.ChunkInfo; + SophonManifestChunkInfo? firstTag = x.ChunkInfo; return firstTag?.CompressedSize ?? 0; }); @@ -968,7 +968,9 @@ private ValueTask RunSophonAssetDownloadThread(HttpClient client, } // Get the target and temp file info - FileInfo existingFileInfo = new FileInfo(filePath).EnsureNoReadOnly(); + FileInfo existingFileInfo = new FileInfo(filePath) + .EnsureCreationOfDirectory() + .EnsureNoReadOnly(); return asset.WriteToStreamAsync(client, assetSize => existingFileInfo.Open(new FileStreamOptions @@ -1001,9 +1003,13 @@ private ValueTask RunSophonAssetUpdateThread(HttpClient client, // Get the target and temp file info FileInfo existingFileInfo = - new FileInfo(filePath).EnsureNoReadOnly(out bool isExistingFileInfoExist); + new FileInfo(filePath) + .EnsureCreationOfDirectory() + .EnsureNoReadOnly(out bool isExistingFileInfoExist); FileInfo sophonFileInfo = - new FileInfo(filePath + "_tempSophon").EnsureNoReadOnly(out bool isSophonFileInfoExist); + new FileInfo(filePath + "_tempSophon") + .EnsureCreationOfDirectory() + .EnsureNoReadOnly(out bool isSophonFileInfoExist); // Use "_tempSophon" if file is new or if "_tempSophon" file exist. Otherwise use original file if exist if (!isExistingFileInfoExist || isSophonFileInfoExist diff --git a/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.SophonPatch.cs b/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.SophonPatch.cs index 3d9dfa397..b4a88c650 100644 --- a/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.SophonPatch.cs +++ b/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.SophonPatch.cs @@ -11,7 +11,7 @@ using CollapseLauncher.Extension; using CollapseLauncher.Helper; using CollapseLauncher.Helper.Metadata; -using CollapseLauncher.Interfaces; +using CollapseLauncher.Helper.StreamUtility; using Hi3Helper; using Hi3Helper.Data; using Hi3Helper.Plugin.Core.Management; @@ -25,8 +25,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.IO.Hashing; using System.Linq; using System.Net.Http; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; @@ -51,15 +53,14 @@ protected virtual async Task AlterStartPatchUpdateSophon(HttpClient httpCl nameof(GameVersionManager.GamePreset.LauncherBizName)); // Get GameVersionManager and GamePreset - IGameVersion gameVersion = GameVersionManager; - PresetConfig gamePreset = gameVersion.GamePreset; + PresetConfig gamePreset = GameVersionManager.GamePreset; // Gt current and future version - GameVersion? requestedVersionFrom = gameVersion.GetGameExistingVersion() ?? + GameVersion? requestedVersionFrom = GameVersionManager.GetGameExistingVersion() ?? throw new NullReferenceException("Cannot get previous/current version of the game"); GameVersion? requestedVersionTo = (isPreloadMode ? - gameVersion.GetGameVersionApiPreload() : - gameVersion.GetGameVersionApi()) ?? + GameVersionManager.GetGameVersionApiPreload() : + GameVersionManager.GetGameVersionApi()) ?? throw new NullReferenceException("Cannot get next/future version of the game"); // Assign branch properties @@ -168,13 +169,13 @@ protected virtual async Task ConfirmAdditionalPatchDataPackageFiles(SophonChunkM .Where(x => matchingFieldsList.Contains(x.MatchingField, StringComparer.OrdinalIgnoreCase)) .Sum(x => { - var firstTag = x.DiffTaggedInfo.FirstOrDefault(y => y.Key == currentVersion).Value; + SophonManifestChunkInfo? firstTag = x.DiffTaggedInfo.FirstOrDefault(y => y.Key == currentVersion).Value; return firstTag?.CompressedSize ?? 0; }); long sizeAdditionalToDownload = otherManifestIdentity .Sum(x => { - var firstTag = x.DiffTaggedInfo.FirstOrDefault(y => y.Key == currentVersion).Value; + SophonManifestChunkInfo? firstTag = x.DiffTaggedInfo.FirstOrDefault(y => y.Key == currentVersion).Value; return firstTag?.CompressedSize ?? 0; }); @@ -191,7 +192,7 @@ protected virtual async Task ConfirmAdditionalPatchDataPackageFiles(SophonChunkM } } - matchingFieldsList.AddRange(otherManifestIdentity.Select(identity => identity.MatchingField)); + matchingFieldsList.AddRange(otherManifestIdentity.Select(identity => identity.MatchingField ?? "")); return; string GetFileDetails() @@ -205,12 +206,12 @@ string GetFileDetails() long chunkCount = 0; // ReSharper disable once ConvertToUsingDeclaration - using (FileStream fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write)) + using (FileStream fileStream = new(filePath, FileMode.Create, FileAccess.Write)) { - using StreamWriter writer = new StreamWriter(fileStream); - foreach (var field in otherManifestIdentity) + using StreamWriter writer = new(fileStream); + foreach (SophonManifestPatchIdentity field in otherManifestIdentity) { - var fieldInfo = field.DiffTaggedInfo.FirstOrDefault(x => x.Key == currentVersion).Value; + SophonManifestChunkInfo? fieldInfo = field.DiffTaggedInfo.FirstOrDefault(x => x.Key == currentVersion).Value; if (fieldInfo == null) { continue; @@ -560,29 +561,67 @@ async ValueTask ImplDownload(Tuple> ct SophonPatchAsset patchAsset = ctx.Item1; Dictionary downloadedDict = ctx.Item2; - using (dictionaryLock.EnterScope()) + try { - _ = downloadedDict.TryAdd(patchAsset.PatchNameSource, 0); - downloadedDict[patchAsset.PatchNameSource]++; + UpdateCurrentDownloadStatus(); + // Check if target file has already been patched so the launcher won't redownload everything. + if (!isPreloadMode && patchAsset.PatchMethod != SophonPatchMethod.Remove) + { + FileInfo fileInfo = new FileInfo(Path.Combine(GamePath, patchAsset.TargetFilePath)) + .EnsureNoReadOnly() + .StripAlternateDataStream(); + + if (fileInfo.Exists && + fileInfo.Length == patchAsset.TargetFileSize) + { + byte[] remoteHashBytes = HexTool.HexToBytesUnsafe(patchAsset.TargetFileHash); + byte[] localHashBytes = remoteHashBytes.Length > 8 + ? await GetCryptoHashAsync(fileInfo, null, false, false, innerToken) + : await GetHashAsync(fileInfo, false, false, innerToken); + + // Try to reverse hash bytes in case the returned bytes are going to be Big-endian. + if (!localHashBytes.SequenceEqual(remoteHashBytes)) + { + Array.Reverse(localHashBytes); + } + + // Now compare. If the hash is already equal (means the target file is already downloaded), + // then skip from downloading the patch. + if (localHashBytes.SequenceEqual(remoteHashBytes)) + { + long patchSize = patchAsset.PatchSize; + UpdateSophonFileTotalProgress(patchSize); + UpdateSophonFileDownloadProgress(patchSize, patchSize); + return; + } + } + } + + await patchAsset.DownloadPatchAsync(httpClient, + GamePath, + patchOutputDir, + true, + read => + { + UpdateSophonFileTotalProgress(read); + UpdateSophonFileDownloadProgress(read, read); + }, + downloadLimiter, + innerToken); } + finally + { + using (dictionaryLock.EnterScope()) + { + _ = downloadedDict.TryAdd(patchAsset.PatchNameSource, 0); + downloadedDict[patchAsset.PatchNameSource]++; + } - UpdateCurrentDownloadStatus(); - await patchAsset.DownloadPatchAsync(httpClient, - GamePath, - patchOutputDir, - true, - read => - { - UpdateSophonFileTotalProgress(read); - UpdateSophonFileDownloadProgress(read, read); - }, - downloadLimiter, - innerToken); - - Logger.LogWriteLine($"Downloaded patch file for: {patchAsset.TargetFilePath}", - LogType.Debug, - true); - Interlocked.Increment(ref ProgressAllCountCurrent); + Logger.LogWriteLine($"Downloaded patch file for: {patchAsset.TargetFilePath}", + LogType.Debug, + true); + Interlocked.Increment(ref ProgressAllCountCurrent); + } } async ValueTask ImplPatchUpdate(Tuple> ctx, CancellationToken innerToken) diff --git a/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.cs b/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.cs index 78c0674d7..e8a960e7c 100644 --- a/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.cs +++ b/CollapseLauncher/Classes/InstallManagement/Base/InstallManagerBase.cs @@ -1926,6 +1926,7 @@ protected virtual string GetLanguageStringByID(int id) ) => langString switch { "Chinese" => "zh-cn", + "Chinese(PRC)" => "zh-cn", "English" => "en-us", "English(US)" => "en-us", "Korean" => "ko-kr", diff --git a/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs b/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs index 17d0ad7c3..c867ee2fb 100644 --- a/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs +++ b/CollapseLauncher/Classes/InstallManagement/Zenless/ZenlessInstall.SophonPatch.cs @@ -1,5 +1,6 @@ using Hi3Helper.Sophon; using Hi3Helper.Sophon.Infos; +using Hi3Helper.Sophon.Structs; using System; using System.Buffers; using System.Collections.Generic; @@ -29,47 +30,16 @@ public override async Task FilterAssetList( Func itemPathSelector, CancellationToken token) { - HashSet exceptMatchFieldHashSet = await GetExceptMatchFieldHashSet(token); + HashSet exceptMatchFieldHashSet = await GetExceptMatchFieldHashSet(token); if (exceptMatchFieldHashSet.Count == 0) { return; } - FilterSophonAsset(itemList, Selector, exceptMatchFieldHashSet); - return; - - string? Selector(T item) - { - // For Game Update - if (item is SophonPatchAsset asset) - { - if (asset.MainAssetInfo is not { } assetSelected) - { - return null; - } - - token.ThrowIfCancellationRequested(); - ref SophonChunksInfo chunkInfo = ref Unsafe.IsNullRef(ref assetSelected) - ? ref Unsafe.NullRef() - : ref GetChunkAssetChunksInfo(assetSelected); - - if (Unsafe.IsNullRef(ref chunkInfo)) - { - chunkInfo = ref GetChunkAssetChunksInfoAlt(assetSelected); - } - - return Unsafe.IsNullRef(ref chunkInfo) - ? asset.MainAssetInfo.AssetName - : chunkInfo.ChunksBaseUrl; - } - - // TODO: For Game Repair handle - - return null; - } + FilterSophonAsset(itemList, exceptMatchFieldHashSet); } - private async Task> GetExceptMatchFieldHashSet(CancellationToken token) + private async Task> GetExceptMatchFieldHashSet(CancellationToken token) { string gameExecDataName = Path.GetFileNameWithoutExtension(GameVersionManager.GamePreset.GameExecutableName) ?? "ZenlessZoneZero"; @@ -82,38 +52,20 @@ private async Task> GetExceptMatchFieldHashSet(CancellationToken to return []; } - string exceptMatchFieldContent = await File.ReadAllTextAsync(gameExceptMatchFieldFile, token); - HashSet exceptMatchFieldHashSet = CreateExceptMatchFieldHashSet(exceptMatchFieldContent); + string exceptMatchFieldContent = await File.ReadAllTextAsync(gameExceptMatchFieldFile, token); + HashSet exceptMatchFieldHashSet = CreateExceptMatchFieldHashSet(exceptMatchFieldContent); return exceptMatchFieldHashSet; } // ReSharper disable once IdentifierTypo - private static void FilterSophonAsset(List itemList, Func assetSelector, HashSet exceptMatchFieldHashSet) + private static void FilterSophonAsset(List itemList, HashSet exceptMatchFieldHashSet) { - const string separators = "/\\"; - scoped Span urlPathRanges = stackalloc Range[32]; - List filteredList = []; foreach (T asset in itemList) { - string? assetPath = assetSelector(asset); - if (assetPath == null) - { - filteredList.Add(asset); - continue; - } - - ReadOnlySpan manifestUrl = assetPath; - int rangeLen = manifestUrl.SplitAny(urlPathRanges, separators, SplitOptions); - if (rangeLen <= 0) - { - continue; - } - - ReadOnlySpan manifestStr = manifestUrl[urlPathRanges[rangeLen - 1]]; - if (int.TryParse(manifestStr, null, out int lookupNumber) && - exceptMatchFieldHashSet.Contains(lookupNumber)) + if (asset is SophonIdentifiableProperty { MatchingField: { } assetMatchingField } && + exceptMatchFieldHashSet.Contains(assetMatchingField)) { continue; } @@ -130,11 +82,10 @@ private static void FilterSophonAsset(List itemList, Func asse itemList.AddRange(filteredList); } - internal static HashSet CreateExceptMatchFieldHashSet(string exceptMatchFieldContent) - where T : ISpanParsable + internal static HashSet CreateExceptMatchFieldHashSet(string exceptMatchFieldContent) { const string lineFeedSeparators = "\r\n"; - HashSet hashSetReturn = []; + HashSet hashSetReturn = new(StringComparer.OrdinalIgnoreCase); scoped Span contentLineRange = stackalloc Range[2]; ReadOnlySpan contentSpan = exceptMatchFieldContent.AsSpan(); @@ -157,10 +108,7 @@ internal static HashSet CreateExceptMatchFieldHashSet(string exceptMatchFi } ReadOnlySpan contentMatch = contentSpan[contentMatchRange].Trim(separatorsChars); - if (T.TryParse(contentMatch, null, out T? result)) - { - hashSetReturn.Add(result); - } + hashSetReturn.Add(contentMatch.ToString()); } return hashSetReturn; diff --git a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs index 225aed9dc..ea0c0d3f9 100644 --- a/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs +++ b/CollapseLauncher/Classes/RepairManagement/Zenless/ZenlessRepair.Fetch.cs @@ -107,7 +107,7 @@ private void FilterExcludedAssets(List assetList) } string exceptMatchFieldContent = File.ReadAllText(gameExceptMatchFieldFile); - HashSet exceptMatchFieldHashSet = ZenlessInstall.CreateExceptMatchFieldHashSet(exceptMatchFieldContent); + HashSet exceptMatchFieldHashSet = ZenlessInstall.CreateExceptMatchFieldHashSet(exceptMatchFieldContent); List filteredList = []; foreach (FilePropertiesRemote asset in assetList) @@ -120,7 +120,7 @@ private void FilterExcludedAssets(List assetList) continue; } - bool isExceptionFound = zenlessResAsset.PackageMatchingIds.Any(exceptMatchFieldHashSet.Contains); + bool isExceptionFound = zenlessResAsset.PackageMatchingIds.Any(x => exceptMatchFieldHashSet.Contains($"{x}")); if (isExceptionFound) { continue; diff --git a/Hi3Helper.Sophon b/Hi3Helper.Sophon index 080d077fc..612fb1fee 160000 --- a/Hi3Helper.Sophon +++ b/Hi3Helper.Sophon @@ -1 +1 @@ -Subproject commit 080d077fcef87d5c591aca5c7bacef0198c4e8d1 +Subproject commit 612fb1fee1fc67ca1ab429d85df016b54d0e5a15