From ddb541c8fae7a5e5e5513d7132f8cb2c935f5d1e Mon Sep 17 00:00:00 2001 From: iks-rain Date: Mon, 9 Feb 2026 16:55:14 +0800 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=95=B4?= =?UTF-8?q?=E5=90=88=E5=8C=85Override=E8=B5=84=E6=BA=90=E7=9A=84=E5=8D=95/?= =?UTF-8?q?=E5=A4=9A=E7=BA=BF=E7=A8=8B=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Installer/Modpack/ModPackUtils.cs | 129 ++++++++++++++++++ .../Extensions/ZipArchiveExtension.cs | 7 + 2 files changed, 136 insertions(+) create mode 100644 MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs new file mode 100644 index 0000000..2f7458e --- /dev/null +++ b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs @@ -0,0 +1,129 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO.Compression; +using MinecraftLaunch.Extensions; + +namespace MinecraftLaunch.Components.Installer.Modpack; + +internal static class ModPackUtils +{ + public const char ZipPathSeparator = '/'; + public static Task ExtractModpackAsync( + [NotNull] string srcZipPath, + [NotNull] string overridesPrefix, + [NotNull] string independentAndFullWorkingPath, + bool enableParallelAcceleration, + CancellationToken cancelToken = default) + { + + + cancelToken.ThrowIfCancellationRequested(); + if (enableParallelAcceleration) + return ParallelAccelerationAsync(srcZipPath, overridesPrefix, independentAndFullWorkingPath, cancelToken); + return SingleThreadAsync(srcZipPath, overridesPrefix, independentAndFullWorkingPath, cancelToken); + + // 修正路径 + static string RemoveOverridesPrefix( + string source, + string overridesPrefix) + { + if (overridesPrefix.EndsWith('/')) + { + return source[overridesPrefix.Length..]; + } + + // 补充/ + return source[(overridesPrefix.Length + 1)..]; + } + + // 判断是否需要排除 + static bool IsShouldExtract( + ZipArchiveEntry entry, + string overridesPrefix) + { + // 排除目录 + if (entry.FullName.EndsWith(ZipPathSeparator)) return false; + // 排除非 /文件 + if (!entry.FullName.StartsWith(overridesPrefix, StringComparison.OrdinalIgnoreCase)) return false; + return true; + } + + // 多线程 + static async Task ParallelAccelerationAsync( + string srcZipPath, + string overridesPrefix, + string independentAndFullWorkingPath, + CancellationToken cancelToken) + { + // 从-1开始即第一次递增后为0 + const int startOffset = -1; + //Init + var parallelCount = Environment.ProcessorCount * 2; + var taskThreads = new Task[parallelCount]; + var zips = new ZipArchive[parallelCount]; + var targetOffset = startOffset; + var concurrentOffset = startOffset; + for (var i = 0; i < parallelCount; i++) + { + zips[i] = ZipFile.OpenRead(srcZipPath); + // 首次循环读取长度 + if (i is 0) targetOffset = zips[0].Entries.Count; + // 拷贝tid用于不同实例闭包 + var taskThreadId = i; + taskThreads[i] = Task.Run(ExtractManyJob, cancelToken); + continue; + + // main job + async Task ExtractManyJob() + { + while (true) + { + cancelToken.ThrowIfCancellationRequested(); + var entryIndex = Interlocked.Increment(ref concurrentOffset); + // entries已全部复制 + // targetOffset不会被修改 + // ReSharper disable once AccessToModifiedClosure + if (entryIndex >= targetOffset) return; + var entry = zips[taskThreadId].Entries[entryIndex]; + if (!IsShouldExtract(entry, overridesPrefix)) continue; + // 获取路径 + var dstPath = Path.Combine(independentAndFullWorkingPath, + RemoveOverridesPrefix(entry.FullName, overridesPrefix)); + // 复制 + await entry.ExtractToFileAsync(dstPath); + } + } + } + + // 确保资源释放,Task只会在await时重新抛出 + try + { + await Task.WhenAll(taskThreads); + } + finally + { + foreach (var zip in zips) zip.Dispose(); + } + } + + // 单线程 + static async Task SingleThreadAsync( + string srcZipPath, + string overridesPrefix, + string independentAndFullWorkingPath, + CancellationToken cancelToken) + { + using var zip = ZipFile.OpenRead(srcZipPath); + foreach (var item in zip.Entries) + { + cancelToken.ThrowIfCancellationRequested(); + if (!IsShouldExtract(item, overridesPrefix)) continue; + await item.ExtractToFileAsync( + Path.Combine( + independentAndFullWorkingPath, + RemoveOverridesPrefix(item.FullName, overridesPrefix)) + ); + } + } + } + +} \ No newline at end of file diff --git a/MinecraftLaunch/Extensions/ZipArchiveExtension.cs b/MinecraftLaunch/Extensions/ZipArchiveExtension.cs index 181a846..2a4eccc 100644 --- a/MinecraftLaunch/Extensions/ZipArchiveExtension.cs +++ b/MinecraftLaunch/Extensions/ZipArchiveExtension.cs @@ -19,4 +19,11 @@ public static void ExtractTo(this ZipArchiveEntry zipArchiveEntry, string destin zipArchiveEntry.ExtractToFile(destinationFile, true); } + + public static async Task ExtractToFileAsync(this ZipArchiveEntry zipArchiveEntry, string destinationFile) + { + await using var dst = File.OpenWrite(destinationFile); + await using var src = zipArchiveEntry.Open(); + await src.CopyToAsync(dst); + } } \ No newline at end of file From d7bedcdbd0757413edc261a6dab9eb8cbce2ea4b Mon Sep 17 00:00:00 2001 From: iks-rain Date: Mon, 9 Feb 2026 17:33:34 +0800 Subject: [PATCH 02/12] style --- .../Installer/Modpack/ModPackUtils.cs | 192 ++++++++---------- 1 file changed, 90 insertions(+), 102 deletions(-) diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs index 2f7458e..f003ef2 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Compression; +using System.Net.Sockets; using MinecraftLaunch.Extensions; namespace MinecraftLaunch.Components.Installer.Modpack; @@ -7,123 +8,110 @@ namespace MinecraftLaunch.Components.Installer.Modpack; internal static class ModPackUtils { public const char ZipPathSeparator = '/'; - public static Task ExtractModpackAsync( - [NotNull] string srcZipPath, - [NotNull] string overridesPrefix, - [NotNull] string independentAndFullWorkingPath, - bool enableParallelAcceleration, + + + public static async Task ExtractParallelAccelerationAsync( + string srcZipPath, + string overridesPrefix, + string independentAndFullWorkingPath, CancellationToken cancelToken = default) { - - - cancelToken.ThrowIfCancellationRequested(); - if (enableParallelAcceleration) - return ParallelAccelerationAsync(srcZipPath, overridesPrefix, independentAndFullWorkingPath, cancelToken); - return SingleThreadAsync(srcZipPath, overridesPrefix, independentAndFullWorkingPath, cancelToken); - - // 修正路径 - static string RemoveOverridesPrefix( - string source, - string overridesPrefix) + // 从-1开始即第一次递增后为0 + const int startOffset = -1; + //Init + var parallelCount = Environment.ProcessorCount * 2; + var taskThreads = new Task[parallelCount]; + var zips = new ZipArchive[parallelCount]; + var targetOffset = startOffset; + var concurrentOffset = startOffset; + for (var i = 0; i < parallelCount; i++) { - if (overridesPrefix.EndsWith('/')) + zips[i] = ZipFile.OpenRead(srcZipPath); + // 首次循环读取长度 + if (i is 0) targetOffset = zips[0].Entries.Count; + // 拷贝tid用于不同实例闭包 + var taskThreadId = i; + taskThreads[i] = Task.Run(ExtractManyJob, cancelToken); + continue; + + // main job + async Task ExtractManyJob() { - return source[overridesPrefix.Length..]; + while (true) + { + cancelToken.ThrowIfCancellationRequested(); + var entryIndex = Interlocked.Increment(ref concurrentOffset); + // entries已全部复制 + // targetOffset不会被修改 + // ReSharper disable once AccessToModifiedClosure + if (entryIndex >= targetOffset) return; + var entry = zips[taskThreadId].Entries[entryIndex]; + if (!IsShouldExtract(entry, overridesPrefix)) continue; + // 获取路径 + var dstPath = Path.Combine(independentAndFullWorkingPath, + RemoveOverridesPrefix(entry.FullName, overridesPrefix)); + // 复制 + await entry.ExtractToFileAsync(dstPath); + } } - - // 补充/ - return source[(overridesPrefix.Length + 1)..]; } - // 判断是否需要排除 - static bool IsShouldExtract( - ZipArchiveEntry entry, - string overridesPrefix) + // 确保资源释放,Task只会在await时重新抛出 + try { - // 排除目录 - if (entry.FullName.EndsWith(ZipPathSeparator)) return false; - // 排除非 /文件 - if (!entry.FullName.StartsWith(overridesPrefix, StringComparison.OrdinalIgnoreCase)) return false; - return true; + await Task.WhenAll(taskThreads); } + finally + { + foreach (var zip in zips) zip.Dispose(); + } + } - // 多线程 - static async Task ParallelAccelerationAsync( - string srcZipPath, - string overridesPrefix, - string independentAndFullWorkingPath, - CancellationToken cancelToken) + public static async Task ExtractSingleThreadAsync( + string srcZipPath, + string overridesPrefix, + string independentAndFullWorkingPath, + CancellationToken cancelToken = default) + { + using var zip = ZipFile.OpenRead(srcZipPath); + foreach (var item in zip.Entries) { - // 从-1开始即第一次递增后为0 - const int startOffset = -1; - //Init - var parallelCount = Environment.ProcessorCount * 2; - var taskThreads = new Task[parallelCount]; - var zips = new ZipArchive[parallelCount]; - var targetOffset = startOffset; - var concurrentOffset = startOffset; - for (var i = 0; i < parallelCount; i++) - { - zips[i] = ZipFile.OpenRead(srcZipPath); - // 首次循环读取长度 - if (i is 0) targetOffset = zips[0].Entries.Count; - // 拷贝tid用于不同实例闭包 - var taskThreadId = i; - taskThreads[i] = Task.Run(ExtractManyJob, cancelToken); - continue; + cancelToken.ThrowIfCancellationRequested(); + if (!IsShouldExtract(item, overridesPrefix)) continue; + await item.ExtractToFileAsync( + Path.Combine( + independentAndFullWorkingPath, + RemoveOverridesPrefix(item.FullName, overridesPrefix)) + ); + } + } - // main job - async Task ExtractManyJob() - { - while (true) - { - cancelToken.ThrowIfCancellationRequested(); - var entryIndex = Interlocked.Increment(ref concurrentOffset); - // entries已全部复制 - // targetOffset不会被修改 - // ReSharper disable once AccessToModifiedClosure - if (entryIndex >= targetOffset) return; - var entry = zips[taskThreadId].Entries[entryIndex]; - if (!IsShouldExtract(entry, overridesPrefix)) continue; - // 获取路径 - var dstPath = Path.Combine(independentAndFullWorkingPath, - RemoveOverridesPrefix(entry.FullName, overridesPrefix)); - // 复制 - await entry.ExtractToFileAsync(dstPath); - } - } - } + #region Util - // 确保资源释放,Task只会在await时重新抛出 - try - { - await Task.WhenAll(taskThreads); - } - finally - { - foreach (var zip in zips) zip.Dispose(); - } - } + private static bool IsShouldExtract( + ZipArchiveEntry entry, + string overridesPrefix) + { + // 排除目录 + if (entry.FullName.EndsWith(ZipPathSeparator)) return false; + // 排除非 /文件 + if (!entry.FullName.StartsWith(overridesPrefix, StringComparison.OrdinalIgnoreCase)) return false; + return true; + } - // 单线程 - static async Task SingleThreadAsync( - string srcZipPath, - string overridesPrefix, - string independentAndFullWorkingPath, - CancellationToken cancelToken) + // 修正路径 + private static string RemoveOverridesPrefix( + string source, + string overridesPrefix) + { + if (overridesPrefix.EndsWith('/')) { - using var zip = ZipFile.OpenRead(srcZipPath); - foreach (var item in zip.Entries) - { - cancelToken.ThrowIfCancellationRequested(); - if (!IsShouldExtract(item, overridesPrefix)) continue; - await item.ExtractToFileAsync( - Path.Combine( - independentAndFullWorkingPath, - RemoveOverridesPrefix(item.FullName, overridesPrefix)) - ); - } + return source[overridesPrefix.Length..]; } + + // 补充/ + return source[(overridesPrefix.Length + 1)..]; } - + + #endregion } \ No newline at end of file From ca2d330bbf30c0b8ffbd79ee4cfc5c9db5aa8e69 Mon Sep 17 00:00:00 2001 From: iks-rain Date: Mon, 9 Feb 2026 17:43:21 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat(ModPack):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs index f003ef2..6153cf6 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs @@ -14,6 +14,7 @@ public static async Task ExtractParallelAccelerationAsync( string srcZipPath, string overridesPrefix, string independentAndFullWorkingPath, + Action whenEachEntryCompleted = null, CancellationToken cancelToken = default) { // 从-1开始即第一次递增后为0 @@ -52,6 +53,7 @@ async Task ExtractManyJob() RemoveOverridesPrefix(entry.FullName, overridesPrefix)); // 复制 await entry.ExtractToFileAsync(dstPath); + whenEachEntryCompleted?.Invoke(zips[taskThreadId]); } } } @@ -71,6 +73,7 @@ public static async Task ExtractSingleThreadAsync( string srcZipPath, string overridesPrefix, string independentAndFullWorkingPath, + Action whenEachEntryCompleted = null, CancellationToken cancelToken = default) { using var zip = ZipFile.OpenRead(srcZipPath); @@ -83,6 +86,7 @@ await item.ExtractToFileAsync( independentAndFullWorkingPath, RemoveOverridesPrefix(item.FullName, overridesPrefix)) ); + whenEachEntryCompleted?.Invoke(zip); } } From 9b057251882858010f2750960262de41ce6833e7 Mon Sep 17 00:00:00 2001 From: iks-rain Date: Mon, 9 Feb 2026 18:54:33 +0800 Subject: [PATCH 04/12] =?UTF-8?q?fix(ModPack):MIT=E6=8E=88=E6=9D=83.NET?= =?UTF-8?q?=E6=A0=87=E5=87=86=E5=BA=93ExtractAsync=E9=83=A8=E5=88=86?= =?UTF-8?q?=E4=BB=A3=E7=A0=81,=E8=AF=B7=E6=8D=A2.NET10=E5=A5=BD=E5=90=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Installer/Modpack/ModPackUtils.cs | 21 ++++----- .../Extensions/ZipArchiveExtension.cs | 45 +++++++++++++++++-- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs index 6153cf6..6df19fd 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs @@ -14,8 +14,8 @@ public static async Task ExtractParallelAccelerationAsync( string srcZipPath, string overridesPrefix, string independentAndFullWorkingPath, - Action whenEachEntryCompleted = null, - CancellationToken cancelToken = default) + /*执行线程不保证*/Action whenEachEntryCompleted = null, + CancellationToken cancellationToken = default) { // 从-1开始即第一次递增后为0 const int startOffset = -1; @@ -32,7 +32,7 @@ public static async Task ExtractParallelAccelerationAsync( if (i is 0) targetOffset = zips[0].Entries.Count; // 拷贝tid用于不同实例闭包 var taskThreadId = i; - taskThreads[i] = Task.Run(ExtractManyJob, cancelToken); + taskThreads[i] = Task.Run(ExtractManyJob, cancellationToken); continue; // main job @@ -40,7 +40,7 @@ async Task ExtractManyJob() { while (true) { - cancelToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); var entryIndex = Interlocked.Increment(ref concurrentOffset); // entries已全部复制 // targetOffset不会被修改 @@ -52,7 +52,7 @@ async Task ExtractManyJob() var dstPath = Path.Combine(independentAndFullWorkingPath, RemoveOverridesPrefix(entry.FullName, overridesPrefix)); // 复制 - await entry.ExtractToFileAsync(dstPath); + await entry.ExtractToFileAsync(dstPath,true,cancellationToken).ConfigureAwait(false); whenEachEntryCompleted?.Invoke(zips[taskThreadId]); } } @@ -73,19 +73,20 @@ public static async Task ExtractSingleThreadAsync( string srcZipPath, string overridesPrefix, string independentAndFullWorkingPath, - Action whenEachEntryCompleted = null, - CancellationToken cancelToken = default) + /*执行线程不保证*/Action whenEachEntryCompleted = null, + CancellationToken cancellationToken = default) { using var zip = ZipFile.OpenRead(srcZipPath); foreach (var item in zip.Entries) { - cancelToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); if (!IsShouldExtract(item, overridesPrefix)) continue; await item.ExtractToFileAsync( Path.Combine( independentAndFullWorkingPath, - RemoveOverridesPrefix(item.FullName, overridesPrefix)) - ); + RemoveOverridesPrefix(item.FullName, overridesPrefix)), + overwrite:true,cancellationToken + ).ConfigureAwait(false); whenEachEntryCompleted?.Invoke(zip); } } diff --git a/MinecraftLaunch/Extensions/ZipArchiveExtension.cs b/MinecraftLaunch/Extensions/ZipArchiveExtension.cs index 2a4eccc..fdb8851 100644 --- a/MinecraftLaunch/Extensions/ZipArchiveExtension.cs +++ b/MinecraftLaunch/Extensions/ZipArchiveExtension.cs @@ -20,10 +20,47 @@ public static void ExtractTo(this ZipArchiveEntry zipArchiveEntry, string destin zipArchiveEntry.ExtractToFile(destinationFile, true); } - public static async Task ExtractToFileAsync(this ZipArchiveEntry zipArchiveEntry, string destinationFile) + // Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + // -> The .NET Foundation licenses this function to us under the MIT license. + // 使用了.net标准库代码 + private static void ExtractToFileInitialize(ZipArchiveEntry source, string destinationFileName, bool overwrite, out FileStreamOptions fileStreamOptions) { - await using var dst = File.OpenWrite(destinationFile); - await using var src = zipArchiveEntry.Open(); - await src.CopyToAsync(dst); + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(destinationFileName); + + fileStreamOptions = new() + { + Access = FileAccess.Write, + Mode = overwrite ? FileMode.Create : FileMode.CreateNew, + Share = FileShare.None, + BufferSize = 16384 + }; + + const UnixFileMode OwnershipPermissions = + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + + // Restore Unix permissions. + // For security, limit to ownership permissions, and respect umask (through UnixCreateMode). + // We don't apply UnixFileMode.None because .zip files created on Windows and .zip files created + // with previous versions of .NET don't include permissions. + UnixFileMode mode = (UnixFileMode)(source.ExternalAttributes >> 16) & OwnershipPermissions; + if (mode != UnixFileMode.None && !OperatingSystem.IsWindows()) + { + fileStreamOptions.UnixCreateMode = mode; + } + } + // Licensed to the .NET Foundation under one or more agreements. + // The .NET Foundation licenses this file to you under the MIT license. + internal static async Task ExtractToFileAsync(this ZipArchiveEntry zipArchiveEntry, string destinationFile,bool overwrite,CancellationToken cts) + { + cts.ThrowIfCancellationRequested(); + ExtractToFileInitialize(zipArchiveEntry, destinationFile, overwrite, out var fileStreamOptions); + await using var dst = new FileStream(destinationFile, fileStreamOptions); + await using var src = zipArchiveEntry.Open(); // OpenAsync真搬不了吧(),等xilu速速换NET10单目标即可 + await src.CopyToAsync(dst, cts).ConfigureAwait(false); + File.SetLastWriteTime(destinationFile,DateTime.Now); } } \ No newline at end of file From 06ecebf673c98d7ffb2538a1952bdfd2933a1b50 Mon Sep 17 00:00:00 2001 From: iks-rain Date: Mon, 9 Feb 2026 19:25:43 +0800 Subject: [PATCH 05/12] =?UTF-8?q?transfer:=20ModrinthModpackInstaller.cs?= =?UTF-8?q?=20`ExtractModpackAsync`=E6=94=B9=E4=B8=BA=E4=BD=BF=E7=94=A8Mod?= =?UTF-8?q?PackUtils=E4=B8=AD=E7=9A=84=E5=8D=95=E7=BA=BF=E7=A8=8B=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E6=8F=90=E5=8F=96(=E5=A4=9A=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E6=9C=AA=E6=B5=8B=E8=AF=95,=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E5=8D=95=E7=BA=BF=E7=A8=8B=E4=B8=8E=E5=8E=9F=E6=9C=AC?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E4=BF=9D=E6=8C=81=E4=B8=80=E8=87=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modpack/ModrinthModpackInstaller.cs | 49 +++++++------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs index dff6125..962f4aa 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs @@ -111,39 +111,28 @@ private Task DownloadModsAsync(IEnumerable } private async Task ExtractModpackAsync(CancellationToken cancellationToken) { - var zipArchive = ZipFile.OpenRead(ModpackPath); - var entries = zipArchive?.Entries; - ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, entries.Count, 0); + + ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, 0, 0); // 此处未开始解析,返回0 const string decompressPrefix = "overrides"; - string woringPath = Minecraft.ToWorkingPath(true); - int count = 0; - var tasks = entries.Select(x => Task.Run(() => { - lock (zipArchive) { - ReportProgress(InstallStep.ExtractModpack, - ((double)Interlocked.Increment(ref count) / (double)entries.Count).ToPercentage(0.85d, 1.0d), - TaskStatus.Running, entries.Count, count); - - if (!x.FullName.StartsWith(decompressPrefix)) - return; - - var subPath = x.FullName[(decompressPrefix.Length + 1)..]; - if (string.IsNullOrEmpty(subPath)) - return; - - var filePath = new FileInfo(Path.Combine(woringPath, subPath)); - if (x.FullName.EndsWith('/')) { - filePath.Directory.Create(); - return; - } - - x.ExtractTo(filePath.FullName); - } - }, cancellationToken)); - - await Task.WhenAll(tasks); - zipArchive.Dispose(); + var count = 0; + await ModPackUtils.ExtractSingleThreadAsync( + srcZipPath: ModpackPath, + overridesPrefix: decompressPrefix, + independentAndFullWorkingPath: Minecraft.ToWorkingPath(true), + whenEachEntryCompleted: ReportEntryExtractingProgress, + cancellationToken: cancellationToken); + return; + + + void ReportEntryExtractingProgress(ZipArchive zipArchive) => + ReportProgress( + step: InstallStep.ExtractModpack, + progress: (Interlocked.Increment(ref count) / (double)zipArchive.Entries.Count).ToPercentage(0.85d, 1.0d), + status: TaskStatus.Running, + totalCount: zipArchive.Entries.Count, + finshedCount: count); } #endregion From 9c00a3658eea6c900b1e4c3b2fccae013fec567d Mon Sep 17 00:00:00 2001 From: iks-rain Date: Mon, 9 Feb 2026 19:27:04 +0800 Subject: [PATCH 06/12] =?UTF-8?q?transfer:=20McbbsModpackInstaller.cs=20`E?= =?UTF-8?q?xtractModpackAsync`=E6=94=B9=E4=B8=BA=E4=BD=BF=E7=94=A8ModPackU?= =?UTF-8?q?tils=E4=B8=AD=E7=9A=84=E5=8D=95=E7=BA=BF=E7=A8=8B=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modpack/McbbsModpackInstaller.cs | 51 ++++++++----------- 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs index d1f754b..e5f05bb 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/McbbsModpackInstaller.cs @@ -75,39 +75,28 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates private async Task ExtractModpackAsync(CancellationToken cancellationToken) { - var zipArchive = ZipFile.OpenRead(ModpackPath); - var entries = zipArchive?.Entries; - ReportProgress(InstallStep.ExtractModpack, 0.10d, TaskStatus.Running, entries.Count, 0); + + ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, 0, 0); // 此处未开始解析,返回0 const string decompressPrefix = "overrides"; - string woringPath = Minecraft.ToWorkingPath(true); - - int count = 0; - var tasks = entries.Select(x => Task.Run(() => { - lock (zipArchive) { - ReportProgress(InstallStep.ExtractModpack, - ((double)Interlocked.Increment(ref count) / (double)entries.Count).ToPercentage(0.1d, 1.0d), - TaskStatus.Running, entries.Count, count); - - if (!x.FullName.StartsWith(decompressPrefix)) - return; - - var subPath = x.FullName[(decompressPrefix.Length + 1)..]; - if (string.IsNullOrEmpty(subPath)) - return; - - var filePath = new FileInfo(Path.Combine(woringPath, subPath)); - if (x.FullName.EndsWith('/')) { - filePath.Directory.Create(); - return; - } - - x.ExtractTo(filePath.FullName); - } - }, cancellationToken)); - - await Task.WhenAll(tasks); - zipArchive.Dispose(); + + var count = 0; + await ModPackUtils.ExtractSingleThreadAsync( + srcZipPath: ModpackPath, + overridesPrefix: decompressPrefix, + independentAndFullWorkingPath: Minecraft.ToWorkingPath(true), + whenEachEntryCompleted: ReportEntryExtractingProgress, + cancellationToken: cancellationToken); + return; + + + void ReportEntryExtractingProgress(ZipArchive zipArchive) => + ReportProgress( + step: InstallStep.ExtractModpack, + progress: (Interlocked.Increment(ref count) / (double)zipArchive.Entries.Count).ToPercentage(0.85d, 1.0d), + status: TaskStatus.Running, + totalCount: zipArchive.Entries.Count, + finshedCount: count); } #endregion From 99c2ca1ebd836b6a02ab6af49c64b7d6f79b63ae Mon Sep 17 00:00:00 2001 From: iks-rain Date: Mon, 9 Feb 2026 19:28:17 +0800 Subject: [PATCH 07/12] =?UTF-8?q?transfer:=20CurseforgeModpackInstaller.cs?= =?UTF-8?q?=20`ExtractModpackAsync`=E6=94=B9=E4=B8=BA=E4=BD=BF=E7=94=A8Mod?= =?UTF-8?q?PackUtils=E4=B8=AD=E7=9A=84=E5=8D=95=E7=BA=BF=E7=A8=8B=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E6=8F=90=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modpack/CurseforgeModpackInstaller.cs | 49 +++++++------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs index e321c0b..2ed5c7f 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/CurseforgeModpackInstaller.cs @@ -193,36 +193,25 @@ private async Task DownloadModsAsync(IEnumerable asyncUrls, Cancellation } private async Task ExtractModpackAsync(CancellationToken cancellationToken) { - var zipArchive = ZipFile.OpenRead(ModpackPath); - var entries = zipArchive?.Entries; - ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, entries.Count, 0); - - int count = 0; - var tasks = entries.Select(x => Task.Run(() => { - lock (zipArchive) { - ReportProgress(InstallStep.ExtractModpack, - ((double)Interlocked.Increment(ref count) / (double)entries.Count).ToPercentage(0.85d, 1.0d), - TaskStatus.Running, entries.Count, count); - - if (!Entry.IsOverride || - !x.FullName.StartsWith(Entry.Overrides, StringComparison.OrdinalIgnoreCase)) return; - - var subPath = x.FullName[(Entry.Overrides.Length + 1)..]; - if (string.IsNullOrEmpty(subPath)) - return; - - var filePath = new FileInfo(Path.Combine(Path.GetFullPath(Minecraft.ToWorkingPath(true)), subPath)); - if (x.FullName.EndsWith('/')) { - Directory.CreateDirectory(filePath.FullName); - return; - } - - x.ExtractTo(filePath.FullName); - } - }, cancellationToken)); - - await Task.WhenAll(tasks); - zipArchive.Dispose(); + + ReportProgress(InstallStep.ExtractModpack, 0.85d, TaskStatus.Running, 0, 0); // 此处未开始解析,返回0 + + var count = 0; + await ModPackUtils.ExtractSingleThreadAsync( + srcZipPath: ModpackPath, + overridesPrefix: Entry.Overrides, + independentAndFullWorkingPath: Minecraft.ToWorkingPath(true), + whenEachEntryCompleted: ReportEntryExtractingProgress, + cancellationToken: cancellationToken); + return; + + void ReportEntryExtractingProgress(ZipArchive zipArchive) => + ReportProgress( + step: InstallStep.ExtractModpack, + progress: (Interlocked.Increment(ref count) / (double)zipArchive.Entries.Count).ToPercentage(0.85d, 1.0d), + status: TaskStatus.Running, + totalCount: zipArchive.Entries.Count, + finshedCount: count); } #endregion From 22aad59143c3fbbb792252da97f7c8294fedc0ff Mon Sep 17 00:00:00 2001 From: iks-rain Date: Mon, 9 Feb 2026 21:05:38 +0800 Subject: [PATCH 08/12] =?UTF-8?q?delapi(ModPack):=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=A4=9A=E7=BA=BF=E7=A8=8B=E7=89=88=E6=9C=AC,=E7=BB=8F?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=97=A0=E6=98=8E=E6=98=BE=E6=8F=90=E5=8D=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Installer/Modpack/ModPackUtils.cs | 61 +------------------ 1 file changed, 1 insertion(+), 60 deletions(-) diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs index 6df19fd..c27e18c 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs @@ -10,65 +10,6 @@ internal static class ModPackUtils public const char ZipPathSeparator = '/'; - public static async Task ExtractParallelAccelerationAsync( - string srcZipPath, - string overridesPrefix, - string independentAndFullWorkingPath, - /*执行线程不保证*/Action whenEachEntryCompleted = null, - CancellationToken cancellationToken = default) - { - // 从-1开始即第一次递增后为0 - const int startOffset = -1; - //Init - var parallelCount = Environment.ProcessorCount * 2; - var taskThreads = new Task[parallelCount]; - var zips = new ZipArchive[parallelCount]; - var targetOffset = startOffset; - var concurrentOffset = startOffset; - for (var i = 0; i < parallelCount; i++) - { - zips[i] = ZipFile.OpenRead(srcZipPath); - // 首次循环读取长度 - if (i is 0) targetOffset = zips[0].Entries.Count; - // 拷贝tid用于不同实例闭包 - var taskThreadId = i; - taskThreads[i] = Task.Run(ExtractManyJob, cancellationToken); - continue; - - // main job - async Task ExtractManyJob() - { - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - var entryIndex = Interlocked.Increment(ref concurrentOffset); - // entries已全部复制 - // targetOffset不会被修改 - // ReSharper disable once AccessToModifiedClosure - if (entryIndex >= targetOffset) return; - var entry = zips[taskThreadId].Entries[entryIndex]; - if (!IsShouldExtract(entry, overridesPrefix)) continue; - // 获取路径 - var dstPath = Path.Combine(independentAndFullWorkingPath, - RemoveOverridesPrefix(entry.FullName, overridesPrefix)); - // 复制 - await entry.ExtractToFileAsync(dstPath,true,cancellationToken).ConfigureAwait(false); - whenEachEntryCompleted?.Invoke(zips[taskThreadId]); - } - } - } - - // 确保资源释放,Task只会在await时重新抛出 - try - { - await Task.WhenAll(taskThreads); - } - finally - { - foreach (var zip in zips) zip.Dispose(); - } - } - public static async Task ExtractSingleThreadAsync( string srcZipPath, string overridesPrefix, @@ -119,4 +60,4 @@ private static string RemoveOverridesPrefix( } #endregion -} \ No newline at end of file +} From 4c8fa9fb562c0b643e730db3fc64e923ebcb4ddf Mon Sep 17 00:00:00 2001 From: iks-rain Date: Mon, 9 Feb 2026 21:21:01 +0800 Subject: [PATCH 09/12] =?UTF-8?q?fix(ModPack):=E4=BF=AE=E5=A4=8D=E8=8B=A5?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E5=8E=9F=E5=85=88=E4=B8=8D=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E5=8D=B3=E6=97=A0=E6=B3=95=E5=AE=8C=E6=88=90=E6=8F=90=E5=8F=96?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Installer/Modpack/ModPackUtils.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs index c27e18c..8cf32d8 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs @@ -21,11 +21,19 @@ public static async Task ExtractSingleThreadAsync( foreach (var item in zip.Entries) { cancellationToken.ThrowIfCancellationRequested(); + // 排除非 /文件 + if (!item.FullName.StartsWith(overridesPrefix, StringComparison.OrdinalIgnoreCase)) continue; + var targetPath = Path.Combine( + independentAndFullWorkingPath, + RemoveOverridesPrefix(item.FullName, overridesPrefix)); + if (item.FullName.EndsWith(ZipPathSeparator)) + { + Directory.CreateDirectory(targetPath); + continue; + } if (!IsShouldExtract(item, overridesPrefix)) continue; await item.ExtractToFileAsync( - Path.Combine( - independentAndFullWorkingPath, - RemoveOverridesPrefix(item.FullName, overridesPrefix)), + targetPath, overwrite:true,cancellationToken ).ConfigureAwait(false); whenEachEntryCompleted?.Invoke(zip); From 2db8ac760b703546483d0d0851490fc3eeb63ebc Mon Sep 17 00:00:00 2001 From: iks-rain Date: Mon, 9 Feb 2026 23:55:04 +0800 Subject: [PATCH 10/12] =?UTF-8?q?perf:=20ModrinthModpackInstaller.cs=20Par?= =?UTF-8?q?seModFiles=E4=BF=AE=E6=94=B9,=E5=87=8F=E5=B0=91=E9=94=81?= =?UTF-8?q?=E5=BC=80=E9=94=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modpack/ModrinthModpackInstaller.cs | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs index 962f4aa..02717a3 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs @@ -70,30 +70,37 @@ public override async Task InstallAsync(CancellationToken cancel #region Privates - private IEnumerable ParseModFiles(CancellationToken cancellationToken) { - int totalCount = Entry.Files.Count(); - ReportProgress(InstallStep.ParseDownloadUrls, 0.1d, TaskStatus.Running, totalCount, 0); - - int count = 0; - string versionPath = Minecraft.ToWorkingPath(true); - foreach (var file in Entry.Files.AsParallel()) { + private IEnumerable ParseModFiles(CancellationToken cancellationToken) + { + const double minProgress = 0.1d; + const double maxProgress = 0.45d; + var fileArray = Entry.Files.ToArray(); + var constTotalCount = fileArray.Length; + ReportProgress( + step: InstallStep.ParseDownloadUrls, + progress: 0.1d, + status: TaskStatus.Running, + totalCount: constTotalCount, + finshedCount: 0); + double count = 0; + var versionPath = Minecraft.ToWorkingPath(true); + //不对Parallel进行Foreach,直接不Parallel + return fileArray.Select(fileItem => + { cancellationToken.ThrowIfCancellationRequested(); - - lock (Entry) { - double progress = (double)Interlocked.Increment(ref count) / (double)totalCount; - ReportProgress(InstallStep.ParseDownloadUrls, progress.ToPercentage(0.1d, 0.45d), - TaskStatus.Running, totalCount, count); - } - - if (!file.Downloads.Any()) - continue; - - if (string.IsNullOrEmpty(file.Path)) - continue; - - var filePath = Path.Combine(versionPath, file.Path); - yield return new DownloadRequest(file.Downloads.First(), filePath); - } + // 非多线程且同个闭包可见性好,无需原子操作 + ReportProgress( + step: InstallStep.ParseDownloadUrls, + // ReSharper disable once AccessToModifiedClosure + progress: (++count / constTotalCount).ToPercentage(minProgress, maxProgress), + status: TaskStatus.Running, + totalCount: constTotalCount, + finshedCount: 0); + if (!fileItem.Downloads.Any()) return null; + if (string.IsNullOrEmpty(fileItem.Path)) return null; + var filePath = Path.Combine(versionPath, fileItem.Path); + return new DownloadRequest(fileItem.Downloads.First(), filePath); + }).Where(static x => x is not null); } private Task DownloadModsAsync(IEnumerable downloadRequests, CancellationToken cancellationToken) { From afb712fe48b928d10e24108c863f723fe197bbe9 Mon Sep 17 00:00:00 2001 From: iks-rain Date: Tue, 10 Feb 2026 00:52:59 +0800 Subject: [PATCH 11/12] =?UTF-8?q?perf:=20=E4=BD=BF=E7=94=A8trygetvalue?= =?UTF-8?q?=E5=87=8F=E5=B0=91=E4=B8=80=E6=AC=A1=E6=9F=A5=E8=A1=A8=E6=93=8D?= =?UTF-8?q?=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modpack/ModrinthModpackInstaller.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs b/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs index 02717a3..efc36e0 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModrinthModpackInstaller.cs @@ -25,18 +25,18 @@ public static ModrinthModpackInstallEntry ParseModpackInstallEntry(string modpac } public static async Task ParseModLoaderEntryAsync(ModrinthModpackInstallEntry modpack, CancellationToken cancellationToken = default) { - if (modpack.Dependencies.ContainsKey("fabric-loader")) + if (modpack.Dependencies.TryGetValue("fabric-loader", out var modpackDependency1)) return (await FabricInstaller.EnumerableFabricAsync(modpack.McVersion, cancellationToken: cancellationToken)) - .First(x => x.BuildVersion.Equals(modpack.Dependencies["fabric-loader"])); - else if (modpack.Dependencies.ContainsKey("quilt-loader")) + .First(x => x.BuildVersion.Equals(modpackDependency1)); + else if (modpack.Dependencies.TryGetValue("quilt-loader", out var dependency1)) return (await QuiltInstaller.EnumerableQuiltAsync(modpack.McVersion, cancellationToken)) - .First(x => x.BuildVersion.Equals(modpack.Dependencies["quilt-loader"])); - else if (modpack.Dependencies.ContainsKey("forge")) + .First(x => x.BuildVersion.Equals(dependency1)); + else if (modpack.Dependencies.TryGetValue("forge", out var modpackDependency)) return (await ForgeInstaller.EnumerableForgeAsync(modpack.McVersion, false, cancellationToken)) - .First(x => x.ForgeVersion.Equals(modpack.Dependencies["forge"])); - else if (modpack.Dependencies.ContainsKey("neoforge")) + .First(x => x.ForgeVersion.Equals(modpackDependency)); + else if (modpack.Dependencies.TryGetValue("neoforge", out var dependency)) return (await ForgeInstaller.EnumerableForgeAsync(modpack.McVersion, true, cancellationToken)) - .First(x => x.ForgeVersion.Equals(modpack.Dependencies["neoforge"])); + .First(x => x.ForgeVersion.Equals(dependency)); else throw new NotSupportedException(); } From 2cb2c326846946607af4b3ffc80434c2dae8b267 Mon Sep 17 00:00:00 2001 From: iks-rain Date: Wed, 11 Feb 2026 14:48:27 +0800 Subject: [PATCH 12/12] =?UTF-8?q?async:=20=E6=8F=90=E5=8D=87=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E6=80=A7,=E5=A3=B0=E8=AF=B7=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=89=8D=E7=9A=84=E5=8F=96=E6=B6=88=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs index 8cf32d8..c2c1694 100644 --- a/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs +++ b/MinecraftLaunch/Components/Installer/Modpack/ModPackUtils.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO.Compression; using System.Net.Sockets; using MinecraftLaunch.Extensions; @@ -17,6 +18,8 @@ public static async Task ExtractSingleThreadAsync( /*执行线程不保证*/Action whenEachEntryCompleted = null, CancellationToken cancellationToken = default) { + Debug.Assert(srcZipPath is not null || overridesPrefix is not null || independentAndFullWorkingPath is not null); + cancellationToken.ThrowIfCancellationRequested(); using var zip = ZipFile.OpenRead(srcZipPath); foreach (var item in zip.Entries) {