From 7781ee299f3559c33ee1b8821b1ef8f56a1647c0 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Thu, 6 Aug 2026 17:37:58 +0800 Subject: [PATCH 01/19] ref:Modpack --- .../Modpack/CurseForgeManifestTest.cs | 80 +++++++++++ .../Minecraft/Modpack/FakeModpackArchive.cs | 35 +++++ .../Modpack/ModpackArchiveDetectorTest.cs | 125 ++++++++++++++++++ .../Minecraft/Modpack/ModrinthManifestTest.cs | 86 ++++++++++++ .../Minecraft/Modpack/CurseForgeManifest.cs | 120 +++++++++++++++++ .../Modpack/IModpackArchiveReader.cs | 27 ++++ .../Modpack/ModpackArchiveDetector.cs | 92 +++++++++++++ PCL.Core/Minecraft/Modpack/ModpackFormat.cs | 48 +++++++ .../Minecraft/Modpack/ModrinthManifest.cs | 121 +++++++++++++++++ PCL.Core/Utils/RegexPatterns.cs | 11 ++ 10 files changed, 745 insertions(+) create mode 100644 PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs create mode 100644 PCL.Core.Test/Minecraft/Modpack/FakeModpackArchive.cs create mode 100644 PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs create mode 100644 PCL.Core.Test/Minecraft/Modpack/ModrinthManifestTest.cs create mode 100644 PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs create mode 100644 PCL.Core/Minecraft/Modpack/IModpackArchiveReader.cs create mode 100644 PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs create mode 100644 PCL.Core/Minecraft/Modpack/ModpackFormat.cs create mode 100644 PCL.Core/Minecraft/Modpack/ModrinthManifest.cs diff --git a/PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs b/PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs new file mode 100644 index 0000000000..fe92799d2e --- /dev/null +++ b/PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs @@ -0,0 +1,80 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PCL.Core.Minecraft.Modpack; +using PCL.Core.Utils; + +namespace PCL.Core.Test.Minecraft.Modpack; + +[TestClass] +public class CurseForgeManifestTest +{ + [TestMethod] + public void ParsesCompleteManifest() + { + const string json = """ + { + "manifestType": "minecraftModpack", + "manifestVersion": 1, + "name": "示例整合包", + "version": "1.0.0", + "author": "作者名", + "minecraft": { + "version": "1.20.1", + "modLoaders": [ { "id": "forge-47.2.0", "primary": true } ] + }, + "files": [ + { "projectID": 238222, "fileID": 5246076, "required": true }, + { "projectID": 123456, "fileID": 789012, "required": false } + ], + "overrides": "overrides" + } + """; + var manifest = CurseForgeManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.AreEqual("minecraftModpack", manifest.ManifestType); + Assert.AreEqual(1, manifest.ManifestVersion); + Assert.AreEqual("示例整合包", manifest.Name); + Assert.AreEqual("1.0.0", manifest.Version); + Assert.AreEqual("作者名", manifest.Author); + Assert.AreEqual("1.20.1", manifest.Minecraft?.Version); + Assert.IsNotNull(manifest.Minecraft?.ModLoaders); + Assert.AreEqual(1, manifest.Minecraft.ModLoaders.Count); + Assert.AreEqual("forge-47.2.0", manifest.Minecraft.ModLoaders[0].Id); + Assert.IsTrue(manifest.Minecraft.ModLoaders[0].Primary); + Assert.IsNotNull(manifest.Files); + Assert.AreEqual(2, manifest.Files.Count); + Assert.AreEqual(238222, manifest.Files[0].ProjectId); + Assert.AreEqual(5246076, manifest.Files[0].FileId); + Assert.IsFalse(manifest.Files[0].IsOptional); + Assert.IsTrue(manifest.Files[1].IsOptional); + Assert.AreEqual("overrides", manifest.Overrides); + } + + [TestMethod] + public void MissingRequiredDefaultsToRequired() + { + const string json = """{"files":[{"projectID":1,"fileID":2}]}"""; + var manifest = CurseForgeManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.IsNotNull(manifest.Files); + Assert.IsFalse(manifest.Files[0].IsOptional); + } + + [TestMethod] + public void MissingOptionalFieldsStayNull() + { + const string json = """{"name":"Test"}"""; + var manifest = CurseForgeManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.AreEqual("Test", manifest.Name); + Assert.IsNull(manifest.Minecraft); + Assert.IsNull(manifest.Files); + Assert.IsNull(manifest.Overrides); + Assert.IsNull(manifest.Version); + } + + [TestMethod] + public void ParseNullReturnsNull() + { + Assert.IsNull(CurseForgeManifest.Parse(null)); + } +} diff --git a/PCL.Core.Test/Minecraft/Modpack/FakeModpackArchive.cs b/PCL.Core.Test/Minecraft/Modpack/FakeModpackArchive.cs new file mode 100644 index 0000000000..ead66136a5 --- /dev/null +++ b/PCL.Core.Test/Minecraft/Modpack/FakeModpackArchive.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using PCL.Core.Minecraft.Modpack; + +namespace PCL.Core.Test.Minecraft.Modpack; + +/// +/// 内存中的 实现,用于格式识别与清单解析的单元测试。 +/// +internal sealed class FakeModpackArchive : IModpackArchiveReader +{ + private readonly Dictionary _entries; + + public FakeModpackArchive(params KeyValuePair[] entries) + { + _entries = entries.ToDictionary(entry => entry.Key, entry => entry.Value); + } + + public FakeModpackArchive(params string[] entryNames) + { + _entries = entryNames.ToDictionary(name => name, _ => ""); + } + + public IEnumerable EntryNames => _entries.Keys; + + public bool EntryExists(string entryName) + { + return _entries.ContainsKey(entryName); + } + + public string ReadEntryText(string entryName) + { + return _entries[entryName]; + } +} diff --git a/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs b/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs new file mode 100644 index 0000000000..9929e9717e --- /dev/null +++ b/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PCL.Core.Minecraft.Modpack; + +namespace PCL.Core.Test.Minecraft.Modpack; + +[TestClass] +public class ModpackArchiveDetectorTest +{ + private const string ManifestWithoutAddons = + """{"manifestType":"minecraftModpack","name":"Test","minecraft":{"version":"1.20.1"}}"""; + + private const string ManifestWithAddons = + """{"manifestType":"minecraftModpack","name":"Test","addons":[{"id":"game","version":"1.20.1"}]}"""; + + [TestMethod] + public void DetectsCurseForgeAtRoot() + { + var archive = new FakeModpackArchive( + new KeyValuePair("manifest.json", ManifestWithoutAddons), + new KeyValuePair("overrides/mods/Test.jar", "")); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.CurseForge, result.Format); + Assert.AreEqual("", result.ArchiveBaseFolder); + } + + [TestMethod] + public void DetectsCurseForgeInSubfolder() + { + var archive = new FakeModpackArchive( + new KeyValuePair("MyPack/manifest.json", ManifestWithoutAddons), + new KeyValuePair("MyPack/overrides/mods/Test.jar", "")); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.CurseForge, result.Format); + Assert.AreEqual("MyPack/", result.ArchiveBaseFolder); + } + + [TestMethod] + public void DetectsModrinthAtRoot() + { + var archive = new FakeModpackArchive("modrinth.index.json", "overrides/mods/Test.jar"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.Modrinth, result.Format); + Assert.AreEqual("", result.ArchiveBaseFolder); + } + + [TestMethod] + public void DetectsMultiMcAtRoot() + { + var archive = new FakeModpackArchive("mmc-pack.json", "instance.cfg", "patches/net.minecraft.json", + ".minecraft/mods/Test.jar"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.MultiMc, result.Format); + Assert.AreEqual("", result.ArchiveBaseFolder); + } + + [TestMethod] + public void DetectsMcbbsByPackmeta() + { + var archive = new FakeModpackArchive("mcbbs.packmeta", "manifest.json"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.Mcbbs, result.Format); + } + + [TestMethod] + public void DetectsMcbbsByManifestWithAddons() + { + var archive = new FakeModpackArchive( + new KeyValuePair("manifest.json", ManifestWithAddons)); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.Mcbbs, result.Format); + } + + [TestMethod] + public void DetectsHmclAtRoot() + { + var archive = new FakeModpackArchive("modpack.json", "minecraft/mods/Test.jar"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.Hmcl, result.Format); + } + + [TestMethod] + public void DetectsLauncherPackByNestedZipAtRoot() + { + var archive = new FakeModpackArchive("modpack.zip"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.LauncherPack, result.Format); + } + + [TestMethod] + public void DetectsLauncherPackByNestedMrpackInSubfolder() + { + var archive = new FakeModpackArchive("SomeFolder/modpack.mrpack", "SomeFolder/README.txt"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.LauncherPack, result.Format); + Assert.AreEqual("SomeFolder/", result.ArchiveBaseFolder); + } + + [TestMethod] + public void DetectsLazyPackByMinecraftVersions() + { + var archive = new FakeModpackArchive(".minecraft/versions/1.20.1/1.20.1.json", + ".minecraft/mods/Test.jar", "README.txt"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.LazyPack, result.Format); + } + + [TestMethod] + public void ReturnsUnknownForUnrecognizedArchive() + { + var archive = new FakeModpackArchive("README.txt", "data/something.dat"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.Unknown, result.Format); + } + + [TestMethod] + public void RootManifestTakesPriorityOverSubfolderFiles() + { + var archive = new FakeModpackArchive( + new KeyValuePair("manifest.json", ManifestWithoutAddons), + new KeyValuePair("SubFolder/modrinth.index.json", "")); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.CurseForge, result.Format); + } +} diff --git a/PCL.Core.Test/Minecraft/Modpack/ModrinthManifestTest.cs b/PCL.Core.Test/Minecraft/Modpack/ModrinthManifestTest.cs new file mode 100644 index 0000000000..c40da6aca4 --- /dev/null +++ b/PCL.Core.Test/Minecraft/Modpack/ModrinthManifestTest.cs @@ -0,0 +1,86 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PCL.Core.Minecraft.Modpack; +using PCL.Core.Utils; + +namespace PCL.Core.Test.Minecraft.Modpack; + +[TestClass] +public class ModrinthManifestTest +{ + [TestMethod] + public void ParsesCompleteManifest() + { + const string json = """ + { + "formatVersion": 1, + "game": "minecraft", + "versionId": "1.0.0", + "name": "示例整合包", + "summary": "简短描述", + "dependencies": { + "minecraft": "1.21.1", + "fabric-loader": "0.16.5" + }, + "files": [ + { + "path": "mods/示例模组.jar", + "hashes": { "sha1": "abcd", "sha512": "efgh" }, + "env": { "client": "required", "server": "unsupported" }, + "downloads": [ "https://cdn.modrinth.com/示例模组.jar" ], + "fileSize": 1234567 + } + ] + } + """; + var manifest = ModrinthManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.AreEqual(1, manifest.FormatVersion); + Assert.AreEqual("minecraft", manifest.Game); + Assert.AreEqual("1.0.0", manifest.VersionId); + Assert.AreEqual("示例整合包", manifest.Name); + Assert.AreEqual("简短描述", manifest.Summary); + Assert.IsNotNull(manifest.Dependencies); + Assert.AreEqual("1.21.1", manifest.Dependencies["minecraft"]); + Assert.AreEqual("0.16.5", manifest.Dependencies["fabric-loader"]); + Assert.IsNotNull(manifest.Files); + Assert.AreEqual(1, manifest.Files.Count); + var file = manifest.Files[0]; + Assert.AreEqual("mods/示例模组.jar", file.Path); + Assert.AreEqual("abcd", file.Hashes?.Sha1); + Assert.AreEqual("efgh", file.Hashes?.Sha512); + Assert.AreEqual("required", file.Env?.Client); + Assert.AreEqual("unsupported", file.Env?.Server); + Assert.IsNotNull(file.Downloads); + Assert.AreEqual(1, file.Downloads.Count); + Assert.AreEqual(1234567, file.FileSize); + } + + [TestMethod] + public void MissingOptionalFieldsStayNull() + { + const string json = """{"formatVersion":1,"name":"Test"}"""; + var manifest = ModrinthManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.AreEqual("Test", manifest.Name); + Assert.IsNull(manifest.Dependencies); + Assert.IsNull(manifest.Files); + Assert.IsNull(manifest.Summary); + } + + [TestMethod] + public void CaseInsensitiveDependencyKeysArePreserved() + { + const string json = """{"dependencies":{"Minecraft":"1.20.1","FORGE":"47.2.0"}}"""; + var manifest = ModrinthManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.IsNotNull(manifest.Dependencies); + Assert.AreEqual("1.20.1", manifest.Dependencies["Minecraft"]); + Assert.AreEqual("47.2.0", manifest.Dependencies["FORGE"]); + } + + [TestMethod] + public void ParseNullReturnsNull() + { + Assert.IsNull(ModrinthManifest.Parse(null)); + } +} diff --git a/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs b/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs new file mode 100644 index 0000000000..40072876be --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using PCL.Core.Utils; + +namespace PCL.Core.Minecraft.Modpack; + +/// +/// CurseForge 格式整合包的清单(manifest.json)模型。 +/// 使用 的宽松反序列化读取;缺失的字段保持为 null/空,由安装流程负责校验。 +/// +public sealed class CurseForgeManifest +{ + /// + /// 清单类型,固定为 "minecraftModpack"。 + /// + public string? ManifestType { get; init; } + + /// + /// 清单版本号,当前固定为 1。 + /// + public int ManifestVersion { get; init; } + + /// + /// 整合包名称。 + /// + public string? Name { get; init; } + + /// + /// 整合包版本号。 + /// + public string? Version { get; init; } + + /// + /// 作者名称(可选)。 + /// + public string? Author { get; init; } + + /// + /// 游戏相关信息;缺失时表示清单未声明游戏版本。 + /// + public CurseForgeMinecraft? Minecraft { get; init; } + + /// + /// 需要下载的模组清单;缺失或为空表示没有需要联网下载的文件。 + /// + public List? Files { get; init; } + + /// + /// overrides 目录名;缺失时表示整合包未声明覆写目录(与历史行为一致,不默认补全)。 + /// + public string? Overrides { get; init; } + + /// + /// 宽松解析 CurseForge 清单。 + /// + /// 清单 JSON 节点。 + /// 解析后的清单;传入 null 时返回 null。 + public static CurseForgeManifest? Parse(JsonNode? node) + { + return node.ToObject(); + } +} + +/// +/// CurseForge 清单中的游戏信息。 +/// +public sealed class CurseForgeMinecraft +{ + /// + /// Minecraft 版本号,如 "1.20.1"。 + /// + public string? Version { get; init; } + + /// + /// 加载器列表,如 forge-47.2.0fabric-0.15.11。 + /// + public List? ModLoaders { get; init; } +} + +/// +/// CurseForge 清单中的加载器声明。 +/// +public sealed class CurseForgeModLoader +{ + /// + /// 加载器 ID,格式为 <loader>-<version>。 + /// + public string? Id { get; init; } + + /// + /// 是否为主加载器。 + /// + public bool Primary { get; init; } +} + +/// +/// CurseForge 清单中需要下载的文件项。 +/// +public sealed class CurseForgeFile +{ + /// + /// CurseForge 项目 ID;缺失时表示无法解析该文件项。 + /// + public int? ProjectId { get; init; } + + /// + /// CurseForge 文件 ID;缺失时表示无法解析该文件项。 + /// + public int? FileId { get; init; } + + /// + /// 是否为必需文件;缺失时视为必需。 + /// + public bool? Required { get; init; } + + /// + /// 是否为可选文件(required 显式为 false)。 + /// + public bool IsOptional => Required is false; +} diff --git a/PCL.Core/Minecraft/Modpack/IModpackArchiveReader.cs b/PCL.Core/Minecraft/Modpack/IModpackArchiveReader.cs new file mode 100644 index 0000000000..2f53ad27b5 --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/IModpackArchiveReader.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace PCL.Core.Minecraft.Modpack; + +/// +/// 整合包压缩包条目的最小读取抽象,供格式识别与清单解析使用。 +/// 条目名统一使用 / 分隔的完整相对路径(与 zip 条目命名一致)。 +/// +public interface IModpackArchiveReader +{ + /// + /// 压缩包内的全部条目名。 + /// + IEnumerable EntryNames { get; } + + /// + /// 判断压缩包内是否存在指定条目。 + /// + /// 条目名,如 modrinth.index.json。 + bool EntryExists(string entryName); + + /// + /// 以文本方式读取指定条目的全部内容。 + /// + /// 条目名。 + string ReadEntryText(string entryName); +} diff --git a/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs b/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs new file mode 100644 index 0000000000..1977038007 --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs @@ -0,0 +1,92 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using PCL.Core.Utils; + +namespace PCL.Core.Minecraft.Modpack; + +/// +/// 整合包格式识别结果。 +/// +/// 识别出的格式。 +/// 清单所在的压缩包内基础目录(以 / 结尾;根目录时为空字符串)。 +public readonly record struct ModpackDetection(ModpackFormat Format, string ArchiveBaseFolder); + +/// +/// 在 上识别整合包的格式,与压缩包或文件夹来源无关。 +/// +public static class ModpackArchiveDetector +{ + /// + /// 识别整合包格式。优先检查根目录的关键文件,其次检查一层子目录中的关键文件, + /// 最后尝试识别懒人包(.minecraft 完整实例)结构。 + /// + public static ModpackDetection Detect(IModpackArchiveReader archive) + { + // 根目录关键文件 + if (archive.EntryExists("mcbbs.packmeta")) + return new ModpackDetection(ModpackFormat.Mcbbs, ""); + if (archive.EntryExists("mmc-pack.json")) + return new ModpackDetection(ModpackFormat.MultiMc, ""); + if (archive.EntryExists("modrinth.index.json")) + return new ModpackDetection(ModpackFormat.Modrinth, ""); + if (archive.EntryExists("manifest.json")) + return new ModpackDetection( + _HasAddons(archive.ReadEntryText("manifest.json")) ? ModpackFormat.Mcbbs : ModpackFormat.CurseForge, ""); + if (archive.EntryExists("modpack.json")) + return new ModpackDetection(ModpackFormat.Hmcl, ""); + if (archive.EntryExists("modpack.zip") || archive.EntryExists("modpack.mrpack")) + return new ModpackDetection(ModpackFormat.LauncherPack, ""); + + // 一层子目录中的关键文件 + foreach (var entryName in archive.EntryNames) + { + var parts = entryName.Split("/"); + if (parts.Length != 2) + continue; + var baseFolder = parts[0] + "/"; + switch (parts[1]) + { + case "mcbbs.packmeta": + return new ModpackDetection(ModpackFormat.Mcbbs, baseFolder); + case "mmc-pack.json": + return new ModpackDetection(ModpackFormat.MultiMc, baseFolder); + case "modrinth.index.json": + return new ModpackDetection(ModpackFormat.Modrinth, baseFolder); + case "manifest.json": + // 历史行为:一层子目录内带 addons 的 manifest 视为 MCBBS,且基础目录固定为 overrides/。 + if (_HasAddons(archive.ReadEntryText(entryName))) + return new ModpackDetection(ModpackFormat.Mcbbs, "overrides/"); + return new ModpackDetection(ModpackFormat.CurseForge, baseFolder); + case "modpack.json": + return new ModpackDetection(ModpackFormat.Hmcl, baseFolder); + case "modpack.zip": + case "modpack.mrpack": + return new ModpackDetection(ModpackFormat.LauncherPack, baseFolder); + } + } + + // 懒人包:存在 .minecraft/versions/<版本>/<版本>.json 结构 + foreach (var entryName in archive.EntryNames) + if (RegexPatterns.ModpackLazyInstance.Match(entryName).Success) + return new ModpackDetection(ModpackFormat.LazyPack, ""); + + return new ModpackDetection(ModpackFormat.Unknown, ""); + } + + /// + /// 判断 manifest.json 是否包含 addons 字段(带 addons 视为 MCBBS 格式)。 + /// 清单无法解析时按不含 addons 处理,交由对应格式的安装流程抛出更具体的错误。 + /// + private static bool _HasAddons(string jsonText) + { + try + { + var node = JsonNode.Parse(jsonText); + return node?["addons"] is not null; + } + catch (JsonException) + { + return false; + } + } +} diff --git a/PCL.Core/Minecraft/Modpack/ModpackFormat.cs b/PCL.Core/Minecraft/Modpack/ModpackFormat.cs new file mode 100644 index 0000000000..95f1b18582 --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/ModpackFormat.cs @@ -0,0 +1,48 @@ +namespace PCL.Core.Minecraft.Modpack; + +/// +/// 整合包压缩包的可识别格式。 +/// +public enum ModpackFormat +{ + /// + /// 未能识别出任何已知格式。 + /// + Unknown, + + /// + /// CurseForge 格式,清单文件为 manifest.json(不含 addons 字段)。 + /// + CurseForge, + + /// + /// Modrinth 格式,清单文件为 modrinth.index.json。 + /// + Modrinth, + + /// + /// MultiMC 格式,含 mmc-pack.jsoninstance.cfg。 + /// + MultiMc, + + /// + /// MCBBS 格式,清单文件为 mcbbs.packmeta,或带 addons 字段的 manifest.json。 + /// + Mcbbs, + + /// + /// HMCL 格式,清单文件为 modpack.json。 + /// + Hmcl, + + /// + /// 带启动器或嵌套整合包的压缩包:内含 modpack.zip / modpack.mrpack, + /// 或一层子目录内包含任意格式的整合包。 + /// + LauncherPack, + + /// + /// 懒人包:内含 .minecraft/versions/<版本>/<版本>.json 完整实例目录的压缩包。 + /// + LazyPack +} diff --git a/PCL.Core/Minecraft/Modpack/ModrinthManifest.cs b/PCL.Core/Minecraft/Modpack/ModrinthManifest.cs new file mode 100644 index 0000000000..021a4bc43a --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/ModrinthManifest.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using PCL.Core.Utils; + +namespace PCL.Core.Minecraft.Modpack; + +/// +/// Modrinth 格式整合包的清单(modrinth.index.json)模型。 +/// 使用 的宽松反序列化读取;缺失的字段保持为 null/空,由安装流程负责校验。 +/// +public sealed class ModrinthManifest +{ + /// + /// 清单格式版本号,固定为 1。 + /// + public int FormatVersion { get; init; } + + /// + /// 目标游戏,固定为 "minecraft"。 + /// + public string? Game { get; init; } + + /// + /// 该整合包版本的唯一标识符。 + /// + public string? VersionId { get; init; } + + /// + /// 整合包名称。 + /// + public string? Name { get; init; } + + /// + /// 简短描述(可选)。 + /// + public string? Summary { get; init; } + + /// + /// 依赖声明,键如 minecraftfabric-loaderforgeneoforgequilt-loader; + /// 缺失时表示清单未声明依赖。 + /// + public Dictionary? Dependencies { get; init; } + + /// + /// 需要下载的文件列表;缺失或为空表示没有需要联网下载的文件。 + /// + public List? Files { get; init; } + + /// + /// 宽松解析 Modrinth 清单。 + /// + /// 清单 JSON 节点。 + /// 解析后的清单;传入 null 时返回 null。 + public static ModrinthManifest? Parse(JsonNode? node) + { + return node.ToObject(); + } +} + +/// +/// Modrinth 清单中的下载文件项。 +/// +public sealed class ModrinthFile +{ + /// + /// 文件在 Minecraft 实例中的相对路径,如 mods/MyMod.jar。不允许包含 .. 或以盘符开头。 + /// + public string? Path { get; init; } + + /// + /// 文件哈希校验信息。 + /// + public ModrinthHashes? Hashes { get; init; } + + /// + /// 客户端/服务端适用性声明。 + /// + public ModrinthEnv? Env { get; init; } + + /// + /// 文件下载 URL 列表(通常只有一个)。 + /// + public List? Downloads { get; init; } + + /// + /// 文件大小(字节,可选)。 + /// + public long FileSize { get; init; } +} + +/// +/// Modrinth 文件哈希校验信息。 +/// +public sealed class ModrinthHashes +{ + /// + /// SHA1 哈希值。 + /// + public string? Sha1 { get; init; } + + /// + /// SHA512 哈希值。 + /// + public string? Sha512 { get; init; } +} + +/// +/// Modrinth 文件的环境适用性声明。 +/// +public sealed class ModrinthEnv +{ + /// + /// 客户端适用性,取值为 required / optional / unsupported。 + /// + public string? Client { get; init; } + + /// + /// 服务端适用性,取值为 required / optional / unsupported。 + /// + public string? Server { get; init; } +} diff --git a/PCL.Core/Utils/RegexPatterns.cs b/PCL.Core/Utils/RegexPatterns.cs index c4e49910f7..e6b02081a7 100644 --- a/PCL.Core/Utils/RegexPatterns.cs +++ b/PCL.Core/Utils/RegexPatterns.cs @@ -243,4 +243,15 @@ public static partial class RegexPatterns [GeneratedRegex(@"[0-9a-zA-Z_-]+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] private static partial Regex _ModIdMatch(); #endregion + + #region 整合包识别 + + /// + /// 懒人包(完整实例压缩包)结构识别:匹配形如 .../versions/<版本>/<版本>.json 的路径, + /// 表示压缩包内含可直接使用的 .minecraft 实例目录。 + /// + public static readonly Regex ModpackLazyInstance = _ModpackLazyInstance(); + [GeneratedRegex(@"^.*\/(?=versions\/(?[^\/]+)\/(\k)\.json$)", RegexOptions.IgnoreCase)] + private static partial Regex _ModpackLazyInstance(); + #endregion } From 734eb3d19c24ffbb97b8d2dc0807e8f0537b52ad Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Thu, 6 Aug 2026 18:57:56 +0800 Subject: [PATCH 02/19] =?UTF-8?q?ref:=E6=95=B4=E5=90=88=E5=8C=85=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E9=87=8D=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modpack/ModpackArchiveDetectorTest.cs | 47 + .../Minecraft/Modpack/ModrinthManifestTest.cs | 20 + .../Minecraft/Modpack/MultiMcManifestTest.cs | 78 + .../Modpack/ModpackArchiveDetector.cs | 41 + .../Minecraft/Modpack/ModrinthManifest.cs | 16 + PCL.Core/Minecraft/Modpack/MultiMcManifest.cs | 85 + .../Modules/Minecraft/ModModpack.Container.cs | 225 +++ .../Minecraft/ModModpack.CurseForge.cs | 270 +++ .../Modules/Minecraft/ModModpack.Hmcl.cs | 71 + .../Modules/Minecraft/ModModpack.Mcbbs.cs | 122 ++ .../Modules/Minecraft/ModModpack.Modrinth.cs | 199 ++ .../Modules/Minecraft/ModModpack.MultiMc.cs | 489 +++++ .../Modules/Minecraft/ModModpack.cs | 1608 +---------------- .../Minecraft/ZipModpackArchiveReader.cs | 39 + .../Pages/PageDownload/ModDownloadLib.cs | 18 +- 15 files changed, 1811 insertions(+), 1517 deletions(-) create mode 100644 PCL.Core.Test/Minecraft/Modpack/MultiMcManifestTest.cs create mode 100644 PCL.Core/Minecraft/Modpack/MultiMcManifest.cs create mode 100644 Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs create mode 100644 Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs create mode 100644 Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs create mode 100644 Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs create mode 100644 Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs create mode 100644 Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs create mode 100644 Plain Craft Launcher 2/Modules/Minecraft/ZipModpackArchiveReader.cs diff --git a/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs b/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs index 9929e9717e..b1ec379663 100644 --- a/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs +++ b/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs @@ -113,6 +113,53 @@ public void ReturnsUnknownForUnrecognizedArchive() Assert.AreEqual(ModpackFormat.Unknown, result.Format); } + [TestMethod] + public void DetectsDeepNestedModrinthAsLauncherPack() + { + var archive = new FakeModpackArchive("外层/整合包/modrinth.index.json", "外层/说明.txt"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.LauncherPack, result.Format); + } + + [TestMethod] + public void DetectsDeepNestedMultiMcAsLauncherPack() + { + var archive = new FakeModpackArchive("外层/子目录/mmc-pack.json", "外层/启动器.exe"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.LauncherPack, result.Format); + } + + [TestMethod] + public void DetectsDeepNestedCurseForgeAsLauncherPack() + { + var archive = new FakeModpackArchive( + new KeyValuePair("外层/整合包/manifest.json", ManifestWithoutAddons), + new KeyValuePair("外层/启动器.exe", "")); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.LauncherPack, result.Format); + } + + [TestMethod] + public void DeepModManifestDoesNotCountAsModpack() + { + // Fabric 模组的 manifest.json 不含 minecraft 字段,不应被误判为整合包 + const string modManifest = + """{"schemaVersion":1,"id":"example_mod","version":"1.0.0","depends":{"minecraft":">=1.20"}}"""; + var archive = new FakeModpackArchive( + new KeyValuePair("mods/ExampleMod/manifest.json", modManifest), + new KeyValuePair("README.txt", "")); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.Unknown, result.Format); + } + + [TestMethod] + public void DeepNestedModpackZipAsLauncherPack() + { + var archive = new FakeModpackArchive("外层/子目录/modpack.mrpack", "外层/说明.txt"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.LauncherPack, result.Format); + } + [TestMethod] public void RootManifestTakesPriorityOverSubfolderFiles() { diff --git a/PCL.Core.Test/Minecraft/Modpack/ModrinthManifestTest.cs b/PCL.Core.Test/Minecraft/Modpack/ModrinthManifestTest.cs index c40da6aca4..8c4e5945de 100644 --- a/PCL.Core.Test/Minecraft/Modpack/ModrinthManifestTest.cs +++ b/PCL.Core.Test/Minecraft/Modpack/ModrinthManifestTest.cs @@ -78,6 +78,26 @@ public void CaseInsensitiveDependencyKeysArePreserved() Assert.AreEqual("47.2.0", manifest.Dependencies["FORGE"]); } + [TestMethod] + public void GetDependencyIsCaseInsensitive() + { + const string json = """{"dependencies":{"Minecraft":"1.20.1","Fabric-Loader":"0.16.5"}}"""; + var manifest = ModrinthManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.AreEqual("1.20.1", manifest.GetDependency("minecraft")); + Assert.AreEqual("1.20.1", manifest.GetDependency("MINECRAFT")); + Assert.AreEqual("0.16.5", manifest.GetDependency("fabric-loader")); + Assert.IsNull(manifest.GetDependency("forge")); + } + + [TestMethod] + public void GetDependencyReturnsNullWhenNoDependencies() + { + var manifest = ModrinthManifest.Parse(JsonCompat.ParseNode("""{"name":"Test"}""")); + Assert.IsNotNull(manifest); + Assert.IsNull(manifest.GetDependency("minecraft")); + } + [TestMethod] public void ParseNullReturnsNull() { diff --git a/PCL.Core.Test/Minecraft/Modpack/MultiMcManifestTest.cs b/PCL.Core.Test/Minecraft/Modpack/MultiMcManifestTest.cs new file mode 100644 index 0000000000..e8524f8b56 --- /dev/null +++ b/PCL.Core.Test/Minecraft/Modpack/MultiMcManifestTest.cs @@ -0,0 +1,78 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PCL.Core.Minecraft.Modpack; +using PCL.Core.Utils; + +namespace PCL.Core.Test.Minecraft.Modpack; + +[TestClass] +public class MultiMcManifestTest +{ + [TestMethod] + public void ParsesComponents() + { + const string json = """ + { + "formatVersion": 1, + "components": [ + { "cachedName": "Minecraft", "cachedVolatile": true, "uid": "net.minecraft", "version": "1.20.1" }, + { "uid": "net.minecraftforge", "version": "47.2.0" } + ] + } + """; + var manifest = MultiMcManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.AreEqual(1, manifest.FormatVersion); + Assert.IsNotNull(manifest.Components); + Assert.AreEqual(2, manifest.Components.Count); + Assert.AreEqual("net.minecraft", manifest.Components[0].Uid); + Assert.AreEqual("1.20.1", manifest.Components[0].Version); + Assert.AreEqual("Minecraft", manifest.Components[0].CachedName); + Assert.IsTrue(manifest.Components[0].CachedVolatile); + Assert.AreEqual("net.minecraftforge", manifest.Components[1].Uid); + Assert.AreEqual("47.2.0", manifest.Components[1].Version); + Assert.IsFalse(manifest.Components[1].CachedVolatile); + } + + [TestMethod] + public void MissingComponentsStayNull() + { + var manifest = MultiMcManifest.Parse(JsonCompat.ParseNode("""{"formatVersion":1}""")); + Assert.IsNotNull(manifest); + Assert.IsNull(manifest.Components); + } + + [TestMethod] + public void ParseInstanceNameHandlesCrLf() + { + const string cfg = "InstanceType=OneSix\r\nname=My Instance\r\niconKey=default\r\n"; + Assert.AreEqual("My Instance", MultiMcManifest.ParseInstanceName(cfg)); + } + + [TestMethod] + public void ParseInstanceNameHandlesLfAndSpaces() + { + const string cfg = "InstanceType=OneSix\nname = Spaced Name\n"; + Assert.AreEqual("Spaced Name", MultiMcManifest.ParseInstanceName(cfg)); + } + + [TestMethod] + public void ParseInstanceNameIgnoresCaseAndOtherKeys() + { + const string cfg = "Name=UpperCase\niconKey=default\n"; + Assert.AreEqual("UpperCase", MultiMcManifest.ParseInstanceName(cfg)); + } + + [TestMethod] + public void ParseInstanceNameReturnsNullWhenMissing() + { + Assert.IsNull(MultiMcManifest.ParseInstanceName("InstanceType=OneSix\ntotalTimePlayed=100")); + Assert.IsNull(MultiMcManifest.ParseInstanceName(null)); + Assert.IsNull(MultiMcManifest.ParseInstanceName("")); + } + + [TestMethod] + public void ParseNullReturnsNull() + { + Assert.IsNull(MultiMcManifest.Parse(null)); + } +} diff --git a/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs b/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs index 1977038007..7713cf1f29 100644 --- a/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs +++ b/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs @@ -70,6 +70,30 @@ public static ModpackDetection Detect(IModpackArchiveReader archive) if (RegexPatterns.ModpackLazyInstance.Match(entryName).Success) return new ModpackDetection(ModpackFormat.LazyPack, ""); + // 嵌套整合包:更深的层级(深度 ≥ 3)存在已知整合包标记,说明压缩包内还打包了其他内容。 + // 仅通过唯一标记(modpack.zip / modrinth.index.json 等)或带 minecraft 字段的 manifest.json 判定, + // 避免把模组自身的 manifest.json 误判为整合包。 + foreach (var entryName in archive.EntryNames) + { + var parts = entryName.Split("/"); + if (parts.Length <= 2) + continue; // 根目录与一级目录已在上面处理 + switch (parts[^1]) + { + case "modpack.zip": + case "modpack.mrpack": + case "modpack.json": + case "modrinth.index.json": + case "mmc-pack.json": + case "mcbbs.packmeta": + return new ModpackDetection(ModpackFormat.LauncherPack, ""); + case "manifest.json": + if (_HasMinecraft(archive.ReadEntryText(entryName))) + return new ModpackDetection(ModpackFormat.LauncherPack, ""); + break; + } + } + return new ModpackDetection(ModpackFormat.Unknown, ""); } @@ -89,4 +113,21 @@ private static bool _HasAddons(string jsonText) return false; } } + + /// + /// 判断 manifest.json 是否包含 minecraft 字段(CurseForge 整合包清单的必要字段)。 + /// 模组的 manifest.json(如 Fabric 模组)不含该字段,可用于区分嵌套整合包与普通文件。 + /// + private static bool _HasMinecraft(string jsonText) + { + try + { + var node = JsonNode.Parse(jsonText); + return node?["minecraft"] is not null; + } + catch (JsonException) + { + return false; + } + } } diff --git a/PCL.Core/Minecraft/Modpack/ModrinthManifest.cs b/PCL.Core/Minecraft/Modpack/ModrinthManifest.cs index 021a4bc43a..7610c07332 100644 --- a/PCL.Core/Minecraft/Modpack/ModrinthManifest.cs +++ b/PCL.Core/Minecraft/Modpack/ModrinthManifest.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Text.Json.Nodes; using PCL.Core.Utils; @@ -46,6 +47,21 @@ public sealed class ModrinthManifest /// public List? Files { get; init; } + /// + /// 按键名(大小写不敏感)获取依赖版本,如 minecraftfabric-loader。 + /// + /// 依赖键名。 + /// 依赖的版本值;不存在时返回 null。 + public string? GetDependency(string key) + { + if (Dependencies is null) + return null; + foreach (var dependency in Dependencies) + if (string.Equals(dependency.Key, key, StringComparison.OrdinalIgnoreCase)) + return dependency.Value; + return null; + } + /// /// 宽松解析 Modrinth 清单。 /// diff --git a/PCL.Core/Minecraft/Modpack/MultiMcManifest.cs b/PCL.Core/Minecraft/Modpack/MultiMcManifest.cs new file mode 100644 index 0000000000..cf8e75025e --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/MultiMcManifest.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; +using PCL.Core.Utils; + +namespace PCL.Core.Minecraft.Modpack; + +/// +/// MultiMC 格式整合包的清单(mmc-pack.json)模型。 +/// 使用 的宽松反序列化读取;缺失的字段保持为 null/空,由安装流程负责校验。 +/// +public sealed class MultiMcManifest +{ + /// + /// 清单格式版本号。 + /// + public int FormatVersion { get; init; } + + /// + /// 组件列表,每个组件引用 patches/ 目录下的一个补丁文件。 + /// + public List? Components { get; init; } + + /// + /// 宽松解析 mmc-pack.json。 + /// + /// 清单 JSON 节点。 + /// 解析后的清单;传入 null 时返回 null。 + public static MultiMcManifest? Parse(JsonNode? node) + { + return node.ToObject(); + } + + /// + /// 从 instance.cfg 文本中解析实例显示名称。 + /// 兼容 CRLF / LF 换行,并忽略 name 键两侧的空白。 + /// + /// instance.cfg 的完整文本。 + /// 实例名称;未找到时返回 null。 + public static string? ParseInstanceName(string? instanceCfgText) + { + if (string.IsNullOrEmpty(instanceCfgText)) + return null; + foreach (var rawLine in instanceCfgText.Split(new[] { "\r\n", "\n", "\r" }, + StringSplitOptions.RemoveEmptyEntries)) + { + var separatorIndex = rawLine.IndexOf('='); + if (separatorIndex < 0) + continue; + var key = rawLine[..separatorIndex].Trim(); + if (!string.Equals(key, "name", StringComparison.OrdinalIgnoreCase)) + continue; + var value = rawLine[(separatorIndex + 1)..].Trim(); + return value.Length == 0 ? null : value; + } + + return null; + } +} + +/// +/// MultiMC 清单中的组件声明。 +/// +public sealed class MultiMcComponent +{ + /// + /// 组件唯一标识,如 net.minecraftnet.minecraftforgenet.fabricmc.fabric-loader。 + /// + public string? Uid { get; init; } + + /// + /// 组件版本号。 + /// + public string? Version { get; init; } + + /// + /// 组件的显示名称(可选)。 + /// + public string? CachedName { get; init; } + + /// + /// 是否标记为易变组件(可选)。 + /// + public bool CachedVolatile { get; init; } +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs new file mode 100644 index 0000000000..2929a38d97 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -0,0 +1,225 @@ +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Text.RegularExpressions; +using PCL.Core.App.Localization; +using PCL.Core.UI; +using PCL.Core.Utils; +using static PCL.ModLoader; + +namespace PCL; + +public static partial class ModModpack +{ + #region 带启动器的压缩包 + + private static LoaderCombo _InstallLauncherPack(string fileAddress, ZipArchive archive, + string archiveBaseFolder) + { + // 获取解压路径 + ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.SelectEmptyFolder.Message"), + Lang.Text("Common.Action.Install"), Lang.Text("Common.Action.Continue"), forceWait: true); + var targetFolder = SystemDialogs.SelectFolder(Lang.Text("Minecraft.Download.Modpack.SelectTargetFolder.Title")); + if (string.IsNullOrEmpty(targetFolder)) + throw new ModBase.CancelledException(); + if (Directory.GetFileSystemEntries(targetFolder).Length > 0) + { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.TargetFolderMustBeEmpty"), HintType.Error); + throw new ModBase.CancelledException(); + } + + // 解压 + var loader = new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[] + { + new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => + { + _ExtractModpackFiles(targetFolder, fileAddress, task, 0.9d); + Thread.Sleep(400); // 避免文件争用 + // 查找解压后的 exe 文件 + string launcher = null; + foreach (var exeFile in Directory.GetFiles(targetFolder, "*.exe", SearchOption.TopDirectoryOnly)) + { + var info = FileVersionInfo.GetVersionInfo(exeFile); + ModBase.Log($"[Modpack] 文件 {exeFile} 的产品名标识为 {info.ProductName}"); + if (info.ProductName == "Plain Craft Launcher") + { + launcher = exeFile; + ModBase.Log($"[Modpack] 发现整合包附带的 PCL 启动器:{exeFile}"); + } + else if ((info.ProductName.ContainsF("Launcher", true) || info.ProductName.ContainsF("启动", true)) && + !(info.ProductName == "Plain Craft Launcher Admin Manager")) + { + if (launcher is null) + { + launcher = exeFile; + ModBase.Log($"[Modpack] 发现整合包附带的疑似第三方启动器:{exeFile}"); + } + } + } + + task.Progress = 0.95d; + // 尝试使用附带的启动器打开 + if (launcher is not null) + { + ModBase.Log("[Modpack] 找到压缩包中附带的启动器:" + launcher); + if (ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.BundledLauncher.Message", launcher), + Lang.Text("Minecraft.Download.Modpack.BundledLauncher.Title"), + Lang.Text("Minecraft.Download.Modpack.BundledLauncher.UseBundled"), + Lang.Text("Minecraft.Download.Modpack.BundledLauncher.DoNotUse") + ) == 1) + { + ModBase.OpenExplorer(targetFolder); + ModBase.ShellOnly(launcher, "--wait"); // 要求等待已有的 PCL 退出 + ModBase.Log("[Modpack] 为换用整合包中的启动器启动,强制结束程序"); + ModMain.frmMain.EndProgram(false); + return; + } + } + else + { + ModBase.Log("[Modpack] 未找到压缩包中附带的启动器"); + } + + ModBase.OpenExplorer(targetFolder); + // 加入文件夹列表 + var instanceName = ModBase.GetFolderNameFromPath(targetFolder); + Directory.CreateDirectory(Path.Combine(targetFolder, ".minecraft")); + PageSelectLeft.AddFolder( + Path.Combine(targetFolder, ".minecraft", archiveBaseFolder.Replace("/", @"\").TrimStart('\\')), instanceName, + false); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串) + // 寻找并安装内层整合包(任意格式:modpack.* 压缩包文件或文件夹形式的整合包) + var innerPackPath = _FindInnerModpack(targetFolder); + if (innerPackPath is not null) + { + if (Directory.Exists(innerPackPath)) + { + // 文件夹形式的整合包:重新打包为临时 zip 后走统一安装流程 + var tempZip = Path.Combine(ModMain.RequestTaskTempFolder(), "modpack.zip"); + ModBase.Log("[Modpack] 内层整合包为文件夹,重新打包后安装:" + innerPackPath); + ZipFile.CreateFromDirectory(innerPackPath, tempZip); + ModpackInstall(tempZip); + } + else + { + ModBase.Log("[Modpack] 调用内层整合包文件继续安装:" + innerPackPath); + ModpackInstall(innerPackPath); + } + } + else + { + ModBase.Log("[Modpack] 未在压缩包中找到内层整合包"); + } + }) + }); + loader.Start(targetFolder); + LoaderTaskbarAdd(loader); + ModMain.frmMain.BtnExtraDownload.ShowRefresh(); + ModMain.frmMain.BtnExtraDownload.Ribble(); + return loader; + } + + /// + /// 在解压后的目录中寻找内层整合包:优先 modpack.* 压缩包文件, + /// 其次寻找包含已知清单的文件夹,可识别 CurseForge / Modrinth / MultiMC / MCBBS / HMCL 等任意格式。 + /// + /// 解压后的外层目录。 + /// 内层整合包的文件路径或文件夹路径;未找到时返回 null。 + private static string? _FindInnerModpack(string rootFolder) + { + // 优先:modpack.zip / modpack.mrpack 等压缩包文件 + foreach (var file in Directory.GetFiles(rootFolder, "modpack.*", SearchOption.AllDirectories)) + if (file.EndsWithF(".zip", true) || file.EndsWithF(".mrpack", true)) + return file; + + // 其次:包含已知清单的文件夹(按目录深度由浅到深搜索,优先匹配最外层的整合包) + foreach (var directory in Directory.GetDirectories(rootFolder, "*", SearchOption.AllDirectories) + .OrderBy(directory => directory.Length)) + { + if (File.Exists(Path.Combine(directory, "modrinth.index.json")) || + File.Exists(Path.Combine(directory, "mmc-pack.json")) || + File.Exists(Path.Combine(directory, "mcbbs.packmeta")) || + File.Exists(Path.Combine(directory, "modpack.json"))) + return directory; + if (File.Exists(Path.Combine(directory, "manifest.json"))) + try + { + var json = ModBase.GetJson(ModBase.ReadFile(Path.Combine(directory, "manifest.json"))); + if (json?["minecraft"] is not null) + return directory; + } + catch (Exception) + { + // 忽略无法解析的 manifest.json + } + } + + return null; + } + + #endregion + + #region 普通压缩包 + + private static LoaderCombo _InstallCompress(string fileAddress, ZipArchive archive) + { + // 尝试定位 .minecraft 文件夹:寻找形如 “/versions/XXX/XXX.json” 的路径 + Match match = null; + foreach (var entry in archive.Entries) + { + var entryMatch = RegexPatterns.ModpackLazyInstance.Match("/" + entry.FullName); + if (entryMatch.Success) + { + match = entryMatch; + break; + } + } + + if (match is null) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.UnknownArchiveStructure")); // 没有匹配 + var archiveBaseFolder = match.Value.Replace("/", @"\").TrimStart('\\'); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串) + var instanceName = match.Groups[1].Value; + ModBase.Log("[ModPack] 检测到压缩包的 .minecraft 根目录:" + archiveBaseFolder + ",命中的实例名:" + instanceName); + // 获取解压路径 + ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.SelectEmptyFolder.Message"), + Lang.Text("Common.Action.Install"), Lang.Text("Common.Action.Continue"), forceWait: true); + var targetFolder = SystemDialogs.SelectFolder(Lang.Text("Minecraft.Download.Modpack.SelectTargetFolder.Title")); + if (string.IsNullOrEmpty(targetFolder)) + throw new ModBase.CancelledException(); + if (targetFolder.Contains("!") || targetFolder.Contains(";")) + { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.InvalidGamePathChars", targetFolder), + HintType.Error); + throw new ModBase.CancelledException(); + } + + if (Directory.GetFileSystemEntries(targetFolder).Length > 0) + { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.TargetFolderMustBeEmpty"), HintType.Error); + throw new ModBase.CancelledException(); + } + + // 解压 + var loader = new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[] + { + new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => + { + _ExtractModpackFiles(targetFolder, fileAddress, task, 0.95d); + // 加入文件夹列表 + PageSelectLeft.AddFolder(Path.Combine(targetFolder, archiveBaseFolder), ModBase.GetFolderNameFromPath(targetFolder), + false); + Thread.Sleep(400); // 避免文件争用 + ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.InstanceSelect)); + }) + }) + { + OnStateChanged = ModDownloadLib.McInstallState + }; + loader.Start(targetFolder); + LoaderTaskbarAdd(loader); + ModMain.frmMain.BtnExtraDownload.ShowRefresh(); + ModMain.frmMain.BtnExtraDownload.Ribble(); + return loader; + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs new file mode 100644 index 0000000000..0d0129c4bc --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs @@ -0,0 +1,270 @@ +using System.IO; +using System.IO.Compression; +using System.Text.Json.Nodes; +using PCL.Core.App.Localization; +using PCL.Core.Minecraft.Modpack; +using PCL.Core.UI; +using PCL.Core.Utils; +using PCL.Network; +using PCL.Network.Loaders; +using static PCL.ModLoader; + +namespace PCL; + +public static partial class ModModpack +{ + #region CurseForge + + private static LoaderCombo _InstallCurseForge(string fileAddress, ZipArchive archive, + string archiveBaseFolder, string instanceName = null, string logo = null, string resourceId = null, + bool isOnlineInstall = false) + { + // 读取 Json 文件 + JsonObject json; + try + { + json = (JsonObject)ModBase.GetJson( + ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "manifest.json").Open())); + } + catch (Exception ex) + { + throw new Exception("CurseForge 整合包安装信息存在问题", ex); + } + + var manifest = CurseForgeManifest.Parse(json); + if (manifest is null || manifest.Minecraft is null || manifest.Minecraft.Version is null) + throw new Exception("CurseForge 整合包未提供 Minecraft 版本信息"); + + // 获取实例名 + if (instanceName is null) + instanceName = _PromptInstanceName(manifest.Name ?? ""); + + // 获取 Mod API 版本信息 + string forgeVersion = null; + string neoForgeVersion = null; + string fabricVersion = null; + var modLoader = ModComp.CompLoaderType.Any; + foreach (var loader in manifest.Minecraft.ModLoaders ?? []) + { + var id = (loader.Id ?? "").ToLower(); + if (id.StartsWithF("forge-")) + { + // Forge 指定 + if (id.Contains("recommended")) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.TooOldUnsupported")); + ModBase.Log("[ModPack] 整合包 Forge 版本:" + id); + forgeVersion = id.Replace("forge-", ""); + modLoader = ModComp.CompLoaderType.Forge; + } + else if (id.StartsWithF("neoforge-")) + { + // NeoForge 指定 + ModBase.Log("[ModPack] 整合包 NeoForge 版本:" + id); + neoForgeVersion = id.Replace("neoforge-", ""); + modLoader = ModComp.CompLoaderType.NeoForge; + } + else if (id.StartsWithF("fabric-")) + { + // Fabric 指定 + try + { + ModBase.Log("[ModPack] 整合包 Fabric 版本:" + id); + fabricVersion = id.Replace("fabric-", ""); + modLoader = ModComp.CompLoaderType.Fabric; + break; + } + catch (Exception ex) + { + ModBase.Log(ex, "读取整合包 Fabric 版本失败:" + id); + } + } + else if (id.StartsWithF("quilt-")) + { + throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported")); + } + } + + // 解压 + var installTemp = ModMain.RequestTaskTempFolder(); + var installLoaders = new List(); + var overrideHome = manifest.Overrides ?? ""; + if (!string.IsNullOrEmpty(overrideHome)) + installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), + task => + { + _ExtractModpackFiles(installTemp, fileAddress, task, 0.6d); + _CopyOverrideDirectory( + Path.Combine(installTemp, archiveBaseFolder, overrideHome == "." || overrideHome == "./" ? "" : overrideHome), + _GetVersionFolder(instanceName), task, 0.4d); + }) + { + ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, + block = false + }); // 每 6M 需要 1s + // 获取 Mod 列表 + var modList = new List(); + var modOptionalList = new List(); + // 此处直接读取原始节点:跳过文件时提示需要展示原始条目内容,以保持提示文案不变。 + foreach (var modNode in json["files"]?.AsArray() ?? []) + { + if (modNode["projectID"] is null || modNode["fileID"] is null) + { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.ModMissingRequiredInfoSkipped", modNode)); + continue; + } + + modList.Add((int)modNode["fileID"]); + if (modNode["required"] is JsonNode requiredNode && !requiredNode.ToObject()) + modOptionalList.Add((int)modNode["fileID"]); + } + + if (modList.Any()) + { + var modDownloadLoaders = new List(); + // 获取 Mod 下载信息 + modDownloadLoaders.Add(new LoaderTask( + Lang.Text("Minecraft.Download.Modpack.Stage.PrepareModsDownloadInfo"), task => + { + var allowMirror = true; + JsonArray ret; + var tryCount = 0; + do + { + tryCount += 1; + ret = (JsonArray)((JsonObject)ModBase.GetJson(ModDownload.DlModRequest( + "https://api.curseforge.com/v1/mods/files", + "POST", "{\"fileIds\": [" + modList.Join(",") + "]}", "application/json", + allowMirror)))["data"]; + if (modList.Count <= ret.Count) + { + ModBase.Log("[Modpack] 已获取到的模组数量足够,开始进行下一步"); + break; + } + + allowMirror = false; + ModBase.Log($"[Modpack] 获取模组数量不达标,设置镜像源允许状态为: {allowMirror}"); + if (tryCount > 3) throw new Exception(Lang.Text("Minecraft.Download.Modpack.SomeModsDeleted")); + } while (true); + + task.output = ret; + }) + { + ProgressWeight = modList.Count / 10d + }); // 每 10 Mod 需要 1s + // 构造 NetFile + modDownloadLoaders.Add(new LoaderTask>( + Lang.Text("Minecraft.Download.Modpack.Stage.BuildModsDownloadInfo"), task => + { + var fileList = new Dictionary(); + foreach (var modJson in task.input) + { + var id = modJson["id"].ToObject(); + // 跳过重复的 Mod(疑似 CurseForge Bug) + if (fileList.ContainsKey(id)) + continue; + // 可选 Mod 提示 + if (modOptionalList.Contains(id)) + if (ModMain.MyMsgBox( + Lang.Text("Minecraft.Download.Modpack.OptionalFile.Message", modJson["displayName"]), + Lang.Text("Minecraft.Download.Modpack.OptionalFile.Title"), + Lang.Text("Minecraft.Download.Modpack.OptionalFile.Download"), + Lang.Text("Minecraft.Download.Modpack.OptionalFile.Skip") + ) == 2) + continue; + + // 根据 modules 和文件名后缀判断资源类型 + string targetFolder; + ModComp.CompType type; + if (modJson["modules"].AsArray().Any()) // modules 可能返回 null(#1006) + { + var moduleNames = ((JsonArray)modJson["modules"]).Select(l => l["name"].ToString()).ToList(); + if (moduleNames.Contains("META-INF") || moduleNames.Contains("mcmod.info") || + (modJson?["FileName"]?.ToString()?.EndsWithF(".jar", true)).GetValueOrDefault()) + { + targetFolder = "mods"; + type = ModComp.CompType.Mod; + } + else if (moduleNames.Contains("pack.mcmeta")) + { + targetFolder = "resourcepacks"; + type = ModComp.CompType.ResourcePack; + } + else if (moduleNames.Contains("level.dat")) + { + targetFolder = "saves"; + type = ModComp.CompType.World; + } + else + { + targetFolder = "shaderpacks"; + type = ModComp.CompType.Shader; + } + } + else + { + targetFolder = "mods"; + type = ModComp.CompType.Mod; + } + + // 建立 CompFile + var file = new ModComp.CompFile((JsonObject)modJson, type); + if (!file.Available) + continue; + // 实际的添加 + fileList.Add(id, + file.ToNetFile(_GetVersionFolder(instanceName) + targetFolder + @"\", + ModComp.DownloadReason.ModPack, manifest.Minecraft.Version, modLoader)); + task.Progress += 1d / (1 + modList.Count); + } + + task.output = fileList.Values.ToList(); + }) + { + ProgressWeight = modList.Count / 200d, + show = false + }); // 每 200 Mod 需要 1s + // 下载 Mod 文件 + modDownloadLoaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadMods"), []) + { ProgressWeight = modList.Count * 1.5d }); // 每个 Mod 需要 1.5s + // 构造加载器 + installLoaders.Add( + new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadMods.MainLoader"), + modDownloadLoaders) + { show = false, ProgressWeight = modDownloadLoaders.Sum(l => l.ProgressWeight) }); + } + + // 构造加载器 + var request = new ModDownloadLib.McInstallRequest + { + targetInstanceName = instanceName, + targetInstanceFolder = _GetVersionFolder(instanceName), + minecraftName = manifest.Minecraft.Version, + forgeVersion = forgeVersion, + neoForgeVersion = neoForgeVersion, + fabricVersion = fabricVersion, + }; + var mergeLoaders = ModDownloadLib.McInstallLoader(request); + // 构造总加载器 + var loaders = new List(); + loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), + installLoaders) + { show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) }); + loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders) + { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) }); + loaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.FinalizeFiles"), task => + { + _FinalizeInstance(_GetVersionFolder(instanceName), fileAddress, logo, "CurseForge", + manifest.Version, resourceId); + }) + { + ProgressWeight = 0.1d, + show = false + }); + + // 启动 + var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.CurseForgeInstall", instanceName); + return _StartInstall(loaderName, loaders, request.targetInstanceFolder, isOnlineInstall); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs new file mode 100644 index 0000000000..cfe2d3f7c7 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs @@ -0,0 +1,71 @@ +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Text.Json.Nodes; +using PCL.Core.App.Localization; +using PCL.Core.Utils; +using static PCL.ModLoader; + +namespace PCL; + +public static partial class ModModpack +{ + #region HMCL + + private static LoaderCombo _InstallHmcl(string fileAddress, ZipArchive archive, string archiveBaseFolder) + { + // 读取 Json 文件 + JsonObject json; + try + { + json = (JsonObject)ModBase.GetJson( + ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "modpack.json").Open(), Encoding.UTF8)); + } + catch (Exception ex) + { + throw new Exception("HMCL 整合包安装信息存在问题", ex); + } + + // 获取实例名 + var instanceName = _PromptInstanceName((string)(json["name"] ?? "")); + + // 解压 + var installTemp = ModMain.RequestTaskTempFolder(); + var installLoaders = new List(); + installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), + task => + { + _ExtractModpackFiles(installTemp, fileAddress, task, 0.6d); + _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "minecraft"), + Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4d); + }) + { + ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, + block = false + }); // 每 6M 需要 1s + // 构造游戏本体安装加载器 + if (json["gameVersion"] is null) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.Hmcl")); + var request = new ModDownloadLib.McInstallRequest + { + targetInstanceName = instanceName, + targetInstanceFolder = _GetVersionFolder(instanceName), + minecraftName = json["gameVersion"].ToString() + }; + var mergeLoaders = ModDownloadLib.McInstallLoader(request); + // 构造总加载器 + var loaders = new List + { + new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), installLoaders) + { show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) }, + new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders) + { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) } + }; + + // 启动 + var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.HmclInstall", instanceName); + return _StartInstall(loaderName, loaders, request.targetInstanceFolder, false); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs new file mode 100644 index 0000000000..195e505001 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs @@ -0,0 +1,122 @@ +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Text.Json.Nodes; +using PCL.Core.App; +using PCL.Core.App.Localization; +using PCL.Core.UI; +using PCL.Core.Utils; +using static PCL.ModLoader; + +namespace PCL; + +public static partial class ModModpack +{ + #region MCBBS + + private static LoaderCombo _InstallMcbbs(string fileAddress, ZipArchive archive, + string archiveBaseFolder, string instanceName = null) + { + // 读取 Json 文件 + JsonObject json; + try + { + var entry = archive.GetEntry(archiveBaseFolder + "mcbbs.packmeta") ?? + archive.GetEntry(archiveBaseFolder + "manifest.json"); + using (var stream = entry.Open()) + { + json = (JsonObject)ModBase.GetJson(ModBase.ReadFile(stream, Encoding.UTF8)); + } + } + catch (Exception ex) + { + throw new Exception("MCBBS 整合包安装信息存在问题", ex); + } + + // 获取实例名 + if (instanceName is null) + instanceName = _PromptInstanceName(json["name"]?.ToString() ?? ""); + + // 解压与路径准备 + var installTemp = ModMain.RequestTaskTempFolder(); + var versionFolder = $"{ModFolder.mcFolderSelected}versions\\{instanceName}"; + var installLoaders = new List(); + + // 解压整合包文件任务 + var unzipTask = new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), + task => + { + _ExtractModpackFiles(installTemp, fileAddress, task, 0.6); + _CopyOverrideDirectory( + Path.Combine(installTemp, archiveBaseFolder, "overrides"), + Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), + task, 0.4); + + // JVM 参数处理 + if (json["launchInfo"] is not null) + { + var launchInfo = (JsonObject)json["launchInfo"]; + Config.Instance.JvmArgs[versionFolder] = string.Join(" ", launchInfo["javaArgument"]); + Config.Instance.GameArgs[versionFolder] = string.Join(" ", launchInfo["launchArgument"]); + } + + // 整合包版本 + if (json["version"] is not null) States.Instance.ModpackVersion[versionFolder] = json["version"].ToString(); + }); + + unzipTask.ProgressWeight = new FileInfo(fileAddress).Length / 1024.0 / 1024.0 / 6.0; // 每 6M 需要 1s + unzipTask.block = false; + installLoaders.Add(unzipTask); + + // 构造加载器 + if (json["addons"] is null) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.McbbsAddons")); + + var addons = new Dictionary(); + foreach (var entryNode in json["addons"].AsArray()) { var entry = entryNode.AsObject(); addons.Add(entry["id"].ToString(), entry["version"].ToString()); } + + if (!addons.ContainsKey("game")) + { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.Generic"), HintType.Error); + return null; + } + + if (addons.ContainsKey("quilt")) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported")); + + // 构造安装请求 + var request = new ModDownloadLib.McInstallRequest + { + targetInstanceName = instanceName, + targetInstanceFolder = _GetVersionFolder(instanceName), + minecraftName = addons["game"], + optiFineVersion = addons.ContainsKey("optifine") ? addons["optifine"] : null, + forgeVersion = addons.ContainsKey("forge") ? addons["forge"] : null, + neoForgeVersion = addons.ContainsKey("neoforge") ? addons["neoforge"] : null, + fabricVersion = addons.ContainsKey("fabric") ? addons["fabric"] : null, + }; + + var mergeLoaders = ModDownloadLib.McInstallLoader(request); + + // 构造总加载器 + var loaders = new List(); + loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), + installLoaders) + { + show = false, + block = false, + ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) + }); + loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders) + { + show = false, + ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) + }); + + // 启动任务 + var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.McbbsInstall", instanceName); + return _StartInstall(loaderName, loaders, request.targetInstanceFolder, false); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs new file mode 100644 index 0000000000..0c57c41aad --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs @@ -0,0 +1,199 @@ +using System.IO; +using System.IO.Compression; +using System.Text.Json.Nodes; +using PCL.Core.App.Localization; +using PCL.Core.Minecraft.Modpack; +using PCL.Core.UI; +using PCL.Core.Utils; +using PCL.Network; +using PCL.Network.Loaders; +using static PCL.ModLoader; + +namespace PCL; + +public static partial class ModModpack +{ + #region Modrinth + + private static LoaderCombo _InstallModrinth(string fileAddress, ZipArchive archive, + string archiveBaseFolder, string instanceName = null, string logo = null, string resourceId = null, + bool isOnlineInstall = false) + { + // 读取 Json 文件 + JsonObject json; + try + { + json = (JsonObject)ModBase.GetJson( + ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "modrinth.index.json").Open())); + } + catch (Exception ex) + { + throw new Exception("Modrinth 整合包安装信息存在问题", ex); + } + + var manifest = ModrinthManifest.Parse(json); + if (manifest is null || manifest.GetDependency("minecraft") is null) + throw new Exception("Modrinth 整合包未提供 Minecraft 版本信息"); + // 获取 Mod API 版本信息 + string minecraftVersion = null; + string forgeVersion = null; + string neoForgeVersion = null; + string fabricVersion = null; + var modLoader = ModComp.CompLoaderType.Any; + foreach (var dependency in manifest.Dependencies ?? new Dictionary()) + switch (dependency.Key.ToLower() ?? "") + { + case "minecraft": + { + minecraftVersion = dependency.Value; + break; + } + case "forge": // eg. 14.23.5.2859 / 1.19-41.1.0 + { + forgeVersion = dependency.Value; + modLoader = ModComp.CompLoaderType.Forge; + ModBase.Log("[ModPack] 整合包 Forge 版本:" + forgeVersion); + break; + } + case "neoforge": + case "neo-forge": // eg. 20.6.98-beta + { + neoForgeVersion = dependency.Value; + modLoader = ModComp.CompLoaderType.NeoForge; + ModBase.Log("[ModPack] 整合包 NeoForge 版本:" + neoForgeVersion); + break; + } + case "fabric-loader": // eg. 0.14.14 + { + fabricVersion = dependency.Value; + modLoader = ModComp.CompLoaderType.Fabric; + ModBase.Log("[ModPack] 整合包 Fabric 版本:" + fabricVersion); + break; + } + case "quilt-loader": // eg. 0.26.0 + { + throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported")); + } + + default: + { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.UnknownLoader", dependency.Key, + dependency.Value), HintType.Error); + break; + } + } + + // 获取实例名 + if (instanceName is null) + instanceName = _PromptInstanceName(manifest.Name ?? ""); + + // 解压 + var installTemp = ModMain.RequestTaskTempFolder(); + var installLoaders = new List(); + installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), + task => + { + _ExtractModpackFiles(installTemp, fileAddress, task, 0.5d); + _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "overrides"), + Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4d); + _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "client-overrides"), + Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.1d); + }) + { + ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, + block = false + }); // 每 6M 需要 1s + // 获取下载文件列表 + var fileList = new List(); + foreach (var file in manifest.Files ?? []) + { + // 检查是否需要该文件 + if (file.Env is not null) + switch (file.Env.Client ?? "") + { + case "optional": + { + if (ModMain.MyMsgBox( + Lang.Text("Minecraft.Download.Modpack.OptionalFile.Message", + ModBase.GetFileNameFromPath(file.Path ?? "")), + Lang.Text("Minecraft.Download.Modpack.OptionalFile.Title"), + Lang.Text("Minecraft.Download.Modpack.OptionalFile.Download"), + Lang.Text("Minecraft.Download.Modpack.OptionalFile.Skip") + ) == 2) continue; + + break; + } + case "unsupported": + { + continue; + } + } + + // 缺少下载地址或目标路径的文件项视为损坏,跳过(原实现会在此处抛出异常) + if (file.Downloads is null || file.Downloads.Count == 0 || string.IsNullOrEmpty(file.Path)) + { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.ModMissingRequiredInfoSkipped", file.Path ?? ""), + HintType.Warning); + continue; + } + + // 添加下载文件 + var urls = file.Downloads + .Select(x => ModComp.CompFile.HandleCurseForgeDownloadUrls(x)) + .ToList(); + // 镜像源 + urls = urls.SelectMany(x => ModDownload.DlSourceModDownloadGet(x)).ToList(); + var targetPath = _GetVersionFolder(instanceName) + file.Path; + if (!Path.GetFullPath(targetPath).StartsWithF(_GetVersionFolder(instanceName), true)) + { + ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.PathOutsideInstance.Message", targetPath), + Lang.Text("Minecraft.Download.Modpack.PathOutsideInstance.Title"), isWarn: true); + throw new ModBase.CancelledException(); + } + + fileList.Add(new DownloadFile( + ModComp.CompFile.HandleModrinthDownloadUrls(urls, ModComp.DownloadReason.ModPack, minecraftVersion, + modLoader), targetPath, + new ModBase.FileChecker(actualSize: file.FileSize, hash: file.Hashes?.Sha1), true)); + } + + if (fileList.Any()) + installLoaders.Add( + new LoaderDownload(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadAdditions"), fileList) + { ProgressWeight = fileList.Count * 1.5d }); // 每个 Mod 需要 1.5s + + // 构造加载器 + var request = new ModDownloadLib.McInstallRequest + { + targetInstanceName = instanceName, + targetInstanceFolder = _GetVersionFolder(instanceName), + minecraftName = minecraftVersion, + forgeVersion = forgeVersion, + neoForgeVersion = neoForgeVersion, + fabricVersion = fabricVersion, + }; + var mergeLoaders = ModDownloadLib.McInstallLoader(request); + // 构造总加载器 + var loaders = new List(); + loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), + installLoaders) + { show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) }); + loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders) + { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) }); + loaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.FinalizeFiles"), task => + { + _FinalizeInstance(_GetVersionFolder(instanceName), fileAddress, logo, "Modrinth", + manifest.VersionId, resourceId); + }) + { + ProgressWeight = 0.1d, + show = false + }); + + // 启动 + var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.ModrinthInstall", instanceName); + return _StartInstall(loaderName, loaders, request.targetInstanceFolder, isOnlineInstall); + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs new file mode 100644 index 0000000000..dc1da5bf54 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs @@ -0,0 +1,489 @@ +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Text.Json.Nodes; +using PCL.Core.App; +using PCL.Core.App.Localization; +using PCL.Core.Minecraft.Modpack; +using PCL.Core.UI; +using PCL.Core.Utils; +using static PCL.ModLoader; + +namespace PCL; + +public static partial class ModModpack +{ + #region MultiMC + + /// + /// MultiMC 整合包的安装覆盖信息,由 patches/ 目录下的 JSON 补丁构建, + /// 在合并实例版本 JSON 时使用。 + /// + public class MMCPackInfo + { + /// + /// MC 核心已被补丁修改, 将整体替换原版 JSON。 + /// + public bool IsMinecraftOverridden; + + /// + /// Forge JSON 已被补丁覆盖,跳过 Forge 合并。 + /// + public bool IsForgeOverridden; + + /// + /// NeoForge JSON 已被补丁覆盖,跳过 NeoForge 合并。 + /// + public bool IsNeoForgeOverridden; + + /// + /// Cleanroom JSON 已被补丁覆盖,跳过 Cleanroom 合并。 + /// + public bool IsCleanroomOverridden; + + /// + /// Fabric / LegacyFabric JSON 已被补丁覆盖,跳过合并。 + /// + public bool IsFabricOverridden; + + /// + /// 游戏参数已通过补丁改写为 arguments 形式,合并时删除原 minecraftArguments 字段。 + /// + public bool IsMcArgsEdited; + + /// + /// 需要覆盖或合并到最终版本 JSON 的字段集合。 + /// + public JsonObject OverriddenJson = new(); + } + + private static LoaderCombo _InstallMultiMc(string fileAddress, ZipArchive archive, + string archiveBaseFolder) + { + // 读取 Json 文件 + JsonObject packJson; + string packInstance; + try + { + packJson = (JsonObject)ModBase.GetJson( + ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "mmc-pack.json").Open(), Encoding.UTF8)); + packInstance = ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "instance.cfg").Open(), Encoding.UTF8); + } + catch (Exception ex) + { + throw new Exception("MMC 整合包安装信息存在问题", ex); + } + + // 构建 JSON Patches 覆盖信息 + var packInfo = _BuildMultiMcPackInfo(archive, archiveBaseFolder, packJson); + + // 获取实例名 + var instanceName = _PromptInstanceName(MultiMcManifest.ParseInstanceName(packInstance) ?? ""); + + // 解压 + var installTemp = ModMain.RequestTaskTempFolder(); + var versionFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}"; + var installLoaders = new List(); + installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), + task => + { + _ExtractModpackFiles(installTemp, fileAddress, task, 0.55d); + _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "libraries"), + Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName, "libraries"), task, 0.2d); + _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, ".minecraft"), + Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.2d); + + _MigrateMultiMcInstanceCfg(Path.Combine(installTemp, archiveBaseFolder, "instance.cfg"), versionFolder, + installTemp, archiveBaseFolder, instanceName); + }) + { + ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, + block = false + }); // 每 6M 需要 1s + // 构造实例安装请求 + var manifest = MultiMcManifest.Parse(packJson); + if (manifest is null || manifest.Components is null) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.Generic")); + var request = new ModDownloadLib.McInstallRequest + { + targetInstanceName = instanceName, + targetInstanceFolder = _GetVersionFolder(instanceName) + }; + foreach (var component in manifest.Components) + switch (component.Uid ?? "") + { + case "org.lwjgl": + { + ModBase.Log("[ModPack] 已跳过 LWJGL 项"); + break; + } + case "net.minecraft": + { + request.minecraftName = component.Version; + break; + } + case "net.minecraftforge": + { + if ((component.Version ?? "").StartsWithF("0.")) + request.cleanroomVersion = component.Version; + else + request.forgeVersion = component.Version; + + break; + } + case "net.neoforged": + { + request.neoForgeVersion = component.Version; + break; + } + case "net.fabricmc.fabric-loader": + { + request.fabricVersion = component.Version; + break; + } + case "org.quiltmc.quilt-loader": + { + throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported")); + } + } + + if (request.minecraftName is null) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.Generic")); + + if (packInfo is not null) + request.mmcPackInfo = packInfo; + // 构造加载器 + var mergeLoaders = ModDownloadLib.McInstallLoader(request); + // 构造总加载器 + var loaders = new List(); + loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), + installLoaders) + { show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) }); + loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders) + { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) }); + + // 启动 + var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.MmcInstall", instanceName); + return _StartInstall(loaderName, loaders, request.targetInstanceFolder, false); + } + + /// + /// 解析 patches/ 目录下的 JSON 补丁,构建 覆盖信息。 + /// 参考 https://github.com/MultiMC/Launcher/wiki/JSON-Patches + /// + private static MMCPackInfo? _BuildMultiMcPackInfo(ZipArchive archive, string archiveBaseFolder, + JsonObject packJson) + { + MMCPackInfo packInfo = null; + try + { + // 部分压缩工具不会写入 "patches/" 目录条目,因此不能只检查目录项, + // 而应检查是否存在任何位于 patches/ 下的文件条目。 + if (!archive.Entries.Any(e => !e.FullName.EndsWith("/") && + e.FullName.StartsWith(archiveBaseFolder + "patches/"))) + return null; + ModBase.Log("[ModPack] 安装的 MultiMC 整合包存在 JSON Patches"); + // 排序预处理 + var patches = new List>(); + foreach (var entry in archive.Entries) + if (!entry.FullName.EndsWith("/") && entry.FullName.StartsWith(archiveBaseFolder + "patches/")) + { + var patch = (JsonObject)ModBase.GetJson(ModBase.ReadFile( + archive.GetEntry(entry.FullName).Open(), Encoding.UTF8)); + patches.Add(new KeyValuePair(patch, + (int)(patch["order"] is not null ? patch["order"] : 0))); + } + + var components = (JsonArray)packJson["components"]; + var componentUids = components + .Select(c => c["uid"]?.ToString()) + .ToHashSet(); + + patches = patches + .Where(p => componentUids.Contains(p.Key["uid"]?.ToString())) + .OrderBy(p => p.Value) + .ToList(); + // 应用 Patches + packInfo = new MMCPackInfo(); + + string tweakers = null; + JsonObject assetIndex = null; + JsonObject javaVerJson = null; + string mainClass = null; + var gameArguments = new JsonArray(); + var jvmArguments = new JsonArray(); + var libJson = new JsonArray(); + var addLibJson = new JsonArray(); + foreach (var patch in patches) + { + var patchJson = patch.Key; + if ((string)patchJson["uid"] == "net.minecraft") + { + packInfo.IsMinecraftOverridden = true; + } + else if ((string)patchJson["uid"] == "net.minecraftforge") + { + if (patchJson["version"].ToString().StartsWithF("0.")) + packInfo.IsCleanroomOverridden = true; + else + packInfo.IsForgeOverridden = true; + } + else if ((string)patchJson["uid"] == "net.neoforged") + { + packInfo.IsNeoForgeOverridden = true; + } + else if ((string)patchJson["uid"] == "net.fabricmc.fabric-loader") + { + packInfo.IsFabricOverridden = true; + } + else if ((string)patchJson["uid"] == "org.quiltmc.quilt-loader") + { + throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported")); + } + + // JVM 参数 + if (patchJson["+jvmArgs"] is not null) + { + jvmArguments.Merge(patchJson["+jvmArgs"]); + ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 JVM 参数"); + } + + // Libraries + if (patchJson["libraries"] is not null || patchJson["+libraries"] is not null) + { + var libs = new JsonArray(); + if (patchJson["libraries"] is not null) + foreach (var library in patchJson["libraries"].AsArray()) + { + if (library is not JsonObject libraryObj) continue; + var libObj = libraryObj.DeepClone().AsObject(); + if (libObj["MMC-hint"] is not null) + { + libObj.Add("hint", libObj["MMC-hint"]?.DeepClone()); + libObj.Remove("MMC-hint"); + } + + libs.Add(libObj); + } + + if (patchJson["+libraries"] is not null) + foreach (var library in patchJson["+libraries"].AsArray()) + { + if (library is not JsonObject libraryObj) continue; + var libObj = libraryObj.DeepClone().AsObject(); + if (libObj["MMC-hint"] is not null) + { + libObj.Add("hint", libObj["MMC-hint"]?.DeepClone()); + libObj.Remove("MMC-hint"); + } + + libs.Add(libObj); + } + + libJson.Merge(libs); + ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 Libraries"); + } + + // Tweakers + if (patchJson["+tweakers"] is not null) + { + tweakers = (string)patchJson["+tweakers"][0]; + ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 Tweakers"); + } + + // AssetIndex + if (patchJson["assetIndex"] is not null) + { + assetIndex = patchJson["assetIndex"]?.DeepClone().AsObject(); + ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 AssetIndex"); + } + + // minecraftArguments -> arguments.game + if (patchJson["minecraftArguments"] is not null) + { + foreach (var arg in patchJson["minecraftArguments"].ToString().Split(" ")) + gameArguments.Add(arg); + packInfo.IsMcArgsEdited = true; + ModBase.Log( + $"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 minecraftArguments 至 arguments.game"); + } + + // mainClass + if (patchJson["mainClass"] is not null) + { + mainClass = (string)patchJson["mainClass"]; + ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 mainClass"); + } + + // Java 版本要求 + if (patchJson["compatibleJavaMajors"] is not null) + { + var javaVersion = 0; + string javaComponent = null; + var javaMajors = (JsonArray)patchJson["compatibleJavaMajors"]; + foreach (var java in javaMajors) + { + if (javaVersion > ModBase.Val(java)) + continue; + // 优先选择主要的版本 + if (ModBase.Val(java) == 21d) + { + javaVersion = 21; + javaComponent = "java-runtime-delta"; + } + else if (ModBase.Val(java) == 17d) + { + javaVersion = 17; + javaComponent = "java-runtime-gamma"; + } + else if (ModBase.Val(java) == 11d) + { + javaVersion = 11; + javaComponent = null; + } + else if (ModBase.Val(java) == 8d) + { + javaVersion = 8; + javaComponent = "jre-legacy"; + } + } + + if (javaVersion == 0) + { + javaVersion = (int)javaMajors[0]; + javaComponent = null; + } + + javaVerJson = new JsonObject { { "majorVersion", javaVersion } }; + if (javaComponent is not null) javaVerJson.Add("component", javaComponent); + ModBase.Log($"[ModPack] JSON-Patch {patchJson["uid"]} 要求 Java 版本: " + javaVersion); + } + } + + JsonObject jsonArguments = null; + if (!string.IsNullOrWhiteSpace(tweakers)) + { + gameArguments.Add("--tweakClass"); + gameArguments.Add(tweakers); + } + + if (gameArguments is not null || jvmArguments is not null) + { + jvmArguments.Insert(0, "-Djava.library.path=${natives_directory}"); + jvmArguments.Insert(1, "-Dminecraft.launcher.brand=${launcher_name}"); + jvmArguments.Insert(2, "-Dminecraft.launcher.version=${launcher_version}"); + jvmArguments.Insert(3, "-cp"); + jvmArguments.Insert(4, "${classpath}"); + jsonArguments = new JsonObject { { "game", gameArguments }, { "jvm", jvmArguments } }; + } + + packInfo.OverriddenJson = new JsonObject(); + if (jsonArguments is not null) + packInfo.OverriddenJson.Add("arguments", jsonArguments); + if (mainClass is not null) + packInfo.OverriddenJson.Add("mainClass", mainClass); + if (assetIndex is not null) + packInfo.OverriddenJson.Add("assetIndex", assetIndex); + if (javaVerJson is not null) + packInfo.OverriddenJson.Add("javaVersion", javaVerJson); + if (libJson is not null) + packInfo.OverriddenJson.Add("libraries", libJson); + } + catch (Exception ex) + { + ModBase.Log(ex, "应用 MMC JSON-Patches 失败"); + } + + return packInfo; + } + + /// + /// 迁移 MultiMC 实例的 instance.cfg 中可由 PCL 承接的设置(#2655)。 + /// + private static void _MigrateMultiMcInstanceCfg(string mmcSetupFile, string versionFolder, string installTemp, + string archiveBaseFolder, string instanceName) + { + try + { + // 将其中的等号替换为冒号,以符合 ini 文件格式 + if (!File.Exists(mmcSetupFile)) + return; + List lines = []; + foreach (var line in ModBase.ReadFile(mmcSetupFile).Split(new[] { "\r", "\n" }, + StringSplitOptions.RemoveEmptyEntries)) + { + if (!line.Contains("=")) + continue; + lines.Add(line.BeforeFirst("=") + ":" + line.AfterFirst("=")); + } + + ModBase.WriteFile(mmcSetupFile, lines.Join("\r\n")); + // 读取文件 + if (Convert.ToBoolean(ModBase.ReadIni(mmcSetupFile, "OverrideCommands", + false.ToString()))) + { + var preLaunchCommand = ModBase.ReadIni(mmcSetupFile, "PreLaunchCommand"); + if (!string.IsNullOrEmpty(preLaunchCommand)) + { + preLaunchCommand = preLaunchCommand.Replace(@"\""", "\"") + .Replace("$INST_JAVA", "{java}java.exe").Replace(@"$INST_MC_DIR\", "{minecraft}") + .Replace("$INST_MC_DIR", "{minecraft}").Replace(@"$INST_DIR\", "{verpath}") + .Replace("$INST_DIR", "{verpath}").Replace("$INST_ID", "{name}") + .Replace("$INST_NAME", "{name}"); + Config.Instance.PreLaunchCommand[versionFolder] = preLaunchCommand; + ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:启动前执行命令:" + preLaunchCommand); + } + } + + if (Convert.ToBoolean(ModBase.ReadIni(mmcSetupFile, "JoinServerOnLaunch", + false.ToString()))) + { + var serverAddress = ModBase.ReadIni(mmcSetupFile, "JoinServerOnLaunchAddress") + .Replace(@"\""", "\""); + Config.Instance.ServerToEnter[versionFolder] = serverAddress; + ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:自动进入服务器:" + serverAddress); + } + + if (Convert.ToBoolean(ModBase.ReadIni(mmcSetupFile, "IgnoreJavaCompatibility", + false.ToString()))) + { + Config.Instance.IgnoreJavaCompatibility[versionFolder] = true; + ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:忽略 Java 兼容性警告"); + } + + var logo = Path.GetFileName(ModBase.ReadIni(mmcSetupFile, "iconKey")); + if (!string.IsNullOrEmpty(logo) && File.Exists($"{installTemp}{archiveBaseFolder}{logo}.png")) + { + States.Instance.IsLogoCustom[versionFolder] = true; + States.Instance.LogoPath[versionFolder] = @"PCL\Logo.png"; + ModBase.CopyFile($"{installTemp}{archiveBaseFolder}{logo}.png", + $@"{ModFolder.mcFolderSelected}versions\{instanceName}\PCL\Logo.png"); + ModBase.Log($"[ModPack] 迁移 MultiMC 实例独立设置:实例图标({logo}.png)"); + } + + // JVM 参数 + var jvmArgs = ModBase.ReadIni(mmcSetupFile, "JvmArgs"); + if (!string.IsNullOrEmpty(jvmArgs)) + { + if (Convert.ToBoolean(ModBase.ReadIni(mmcSetupFile, "OverrideJavaArgs", + false.ToString()))) + { + Config.Instance.JvmArgs[versionFolder] = jvmArgs; + ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:JVM 参数(覆盖):" + jvmArgs); + } + else + { + jvmArgs = jvmArgs + " " + Config.Launch.JvmArgs; + Config.Instance.JvmArgs[versionFolder] = jvmArgs; + ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:JVM 参数(追加):" + jvmArgs); + } + } + } + catch (Exception ex) + { + ModBase.Log(ex, $"读取 MMC 配置文件失败({mmcSetupFile})"); + } + } + + #endregion +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs index 428c4d6557..031ef12092 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs @@ -1,20 +1,18 @@ -using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Text; -using System.Text.RegularExpressions; using PCL.Core.App; using PCL.Core.App.Localization; +using PCL.Core.Minecraft.Modpack; using PCL.Core.UI; using PCL.Core.Utils.Validate; using PCL.Network; using PCL.Network.Loaders; using static PCL.ModLoader; -using PCL.Core.Utils; namespace PCL; -public static class ModModpack +public static partial class ModModpack { // 触发整合包安装的外部接口 /// @@ -56,7 +54,6 @@ public static LoaderCombo ModpackInstall(string file, string instanceNam { ModBase.Log("[ModPack] 整合包安装请求:" + (file ?? "null")); ZipArchive archive = null; - var archiveBaseFolder = ""; try { // 字符校验 @@ -69,177 +66,50 @@ public static LoaderCombo ModpackInstall(string file, string instanceNam } // 获取整合包种类与关键 Json - var packType = -1; - do + ModpackFormat packType; + string archiveBaseFolder; + try { - try - { - archive = new ZipArchive(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)); - if (archive.Entries.Any(e => e.IsEncrypted)) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.EncryptedArchiveUnsupported")); - // 从根目录判断整合包类型 - if (archive.GetEntry("mcbbs.packmeta") is not null) - { - packType = 3; - break; - } // MCBBS 整合包(优先于 manifest.json 判断) - - if (archive.GetEntry("mmc-pack.json") is not null) - { - packType = 2; - break; - } // MMC 整合包(优先于 manifest.json 判断,#4194) - - if (archive.GetEntry("modrinth.index.json") is not null) - { - packType = 4; - break; - } // Modrinth 整合包 - - if (archive.GetEntry("manifest.json") is not null) - { - var json = (JsonObject)ModBase.GetJson(ModBase.ReadFile(archive.GetEntry("manifest.json").Open(), - Encoding.UTF8)); - if (json["addons"] is null) - { - packType = 0; - break; // CurseForge 整合包 - } - - packType = 3; - break; - // MCBBS 整合包 - } - - if (archive.GetEntry("modpack.json") is not null) - { - packType = 1; - break; - } // HMCL 整合包 - - if (archive.GetEntry("modpack.zip") is not null || archive.GetEntry("modpack.mrpack") is not null) - { - packType = 9; - break; - } // 带启动器的压缩包 - - // 从一级目录判断整合包类型 - var exitTry = false; - foreach (var Entry in archive.Entries) - { - var fullNames = Entry.FullName.Split("/"); - archiveBaseFolder = fullNames[0] + "/"; - // 确定为一级目录下 - if (fullNames.Count() != 2) - continue; - // 判断是否为关键文件 - if (fullNames[1] == "mcbbs.packmeta") - { - packType = 3; - exitTry = true; - break; - } // MCBBS 整合包(优先于 manifest.json 判断) - - if (fullNames[1] == "mmc-pack.json") - { - packType = 2; - exitTry = true; - break; - } // MMC 整合包(优先于 manifest.json 判断,#4194) - - if (fullNames[1] == "modrinth.index.json") - { - packType = 4; - exitTry = true; - break; - } // Modrinth 整合包 - - if (fullNames[1] == "manifest.json") - { - var json = (JsonObject)ModBase.GetJson(ModBase.ReadFile(Entry.Open(), Encoding.UTF8)); - if (json["addons"] is null) - { - packType = 0; - exitTry = true; - break; // CurseForge 整合包 - } - - packType = 3; - archiveBaseFolder = "overrides/"; - exitTry = true; - break; - // MCBBS 整合包 - } - - if (fullNames[1] == "modpack.json") - { - packType = 1; - exitTry = true; - break; - } // HMCL 整合包 - - if (fullNames[1] == "modpack.zip" || fullNames[1] == "modpack.mrpack") - { - packType = 9; - exitTry = true; - break; - } // 带启动器的压缩包 - } - - if (exitTry) break; - } - catch (Exception ex) - { - if (ex.Message.Contains("Error.WinIOError")) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.OpenFailed"), ex); - else if (file.EndsWithF(".rar", true)) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.RarUnsupported"), ex); - else - throw new Exception(Lang.Text("Minecraft.Download.Modpack.UnsupportedArchive"), ex); - } - } while (false); + archive = new ZipArchive(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)); + if (archive.Entries.Any(e => e.IsEncrypted)) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.EncryptedArchiveUnsupported")); + var detection = ModpackArchiveDetector.Detect(new ZipModpackArchiveReader(archive)); + packType = detection.Format; + archiveBaseFolder = detection.ArchiveBaseFolder; + ModBase.Log($"[ModPack] 整合包格式识别结果:{packType}(基础目录:{archiveBaseFolder})"); + } + catch (Exception ex) + { + if (ex.Message.Contains("Error.WinIOError")) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.OpenFailed"), ex); + else if (file.EndsWithF(".rar", true)) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.RarUnsupported"), ex); + else + throw new Exception(Lang.Text("Minecraft.Download.Modpack.UnsupportedArchive"), ex); + } // 执行对应的安装方法 switch (packType) { - case 0: - { - ModBase.Log("[ModPack] 整合包种类:CurseForge"); - return InstallPackCurseForge(file, archive, archiveBaseFolder, instanceName, logo, resourceId, + case ModpackFormat.CurseForge: + return _InstallCurseForge(file, archive, archiveBaseFolder, instanceName, logo, resourceId, isOnlineInstall); - } - case 1: - { - ModBase.Log("[ModPack] 整合包种类:HMCL"); - return InstallPackHMCL(file, archive, archiveBaseFolder); - } - case 2: - { - ModBase.Log("[ModPack] 整合包种类:MMC"); - return InstallPackMMC(file, archive, archiveBaseFolder); - } - case 3: - { - ModBase.Log("[ModPack] 整合包种类:MCBBS"); - return InstallPackMCBBS(file, archive, archiveBaseFolder, instanceName); - } - case 4: - { - ModBase.Log("[ModPack] 整合包种类:Modrinth"); - return InstallPackModrinth(file, archive, archiveBaseFolder, instanceName, logo, resourceId, + case ModpackFormat.Modrinth: + return _InstallModrinth(file, archive, archiveBaseFolder, instanceName, logo, resourceId, isOnlineInstall); - } - case 9: - { - ModBase.Log("[ModPack] 整合包种类:带启动器的压缩包"); - return InstallPackLauncherPack(file, archive, archiveBaseFolder); - } - + case ModpackFormat.MultiMc: + return _InstallMultiMc(file, archive, archiveBaseFolder); + case ModpackFormat.Mcbbs: + return _InstallMcbbs(file, archive, archiveBaseFolder, instanceName); + case ModpackFormat.Hmcl: + return _InstallHmcl(file, archive, archiveBaseFolder); + case ModpackFormat.LauncherPack: + return _InstallLauncherPack(file, archive, archiveBaseFolder); + case ModpackFormat.LazyPack: + return _InstallCompress(file, archive); default: - { ModBase.Log("[ModPack] 整合包种类:未能识别,假定为压缩包"); - return InstallPackCompress(file, archive); - } + return _InstallCompress(file, archive); } } finally @@ -249,7 +119,12 @@ public static LoaderCombo ModpackInstall(string file, string instanceNam } } - private static void ExtractModpackFiles(string installTemp, string fileAddress, LoaderBase loader, + #region 共享流程 + + /// + /// 解压整合包文件到临时目录,失败时切换编码并重试。 + /// + private static void _ExtractModpackFiles(string installTemp, string fileAddress, LoaderBase loader, double progressIncrement) { // 解压文件 @@ -302,7 +177,7 @@ private static void ExtractModpackFiles(string installTemp, string fileAddress, /// 从整合包的 override 目录复制文件,同时设置 PCL 的配置文件与版本隔离。 /// 对路径末尾是否为 \ 没有要求。 /// - private static void CopyOverrideDirectory(string overridesFolder, string versionFolder, LoaderBase loader, + private static void _CopyOverrideDirectory(string overridesFolder, string versionFolder, LoaderBase loader, double progressIncrement) { if (!overridesFolder.EndsWithF(@"\")) @@ -340,624 +215,38 @@ private static void CopyOverrideDirectory(string overridesFolder, string version ModBase.IniClearCache(versionIni); // 重置缓存,避免被安装过程中写入的 ini 覆盖 } - #region CurseForge - - private static LoaderCombo InstallPackCurseForge(string fileAddress, ZipArchive archive, - string archiveBaseFolder, string instanceName = null, string logo = null, string resourceId = null, - bool isOnlineInstall = false) + /// + /// 校验并获取实例名;给定的默认名无效时弹窗要求输入。 + /// + /// 用户取消输入实例名。 + private static string _PromptInstanceName(string fallback) { - // 读取 Json 文件 - JsonObject json; - try - { - json = (JsonObject)ModBase.GetJson( - ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "manifest.json").Open())); - } - catch (Exception ex) - { - throw new Exception("CurseForge 整合包安装信息存在问题", ex); - } - - if (json["minecraft"] is null || json["minecraft"]["version"] is null) - throw new Exception("CurseForge 整合包未提供 Minecraft 版本信息"); - - // 获取实例名 - if (instanceName is null) - { - instanceName = (string)(json["name"] ?? ""); - var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions")); - if (!validate.Validate(instanceName).IsValid) - instanceName = ""; - if (string.IsNullOrEmpty(instanceName)) - instanceName = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "", - [validate]); - if (string.IsNullOrEmpty(instanceName)) - throw new ModBase.CancelledException(); - } - - // 获取 Mod API 版本信息 - string forgeVersion = null; - string neoForgeVersion = null; - string fabricVersion = null; - var modLoader = ModComp.CompLoaderType.Any; - foreach (var Entry in (dynamic)json["minecraft"]["modLoaders"] ?? Array.Empty()) - { - string id = (Entry["id"] ?? "").ToString().ToLower(); - if (id.StartsWithF("forge-")) - { - // Forge 指定 - if (id.Contains("recommended")) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.TooOldUnsupported")); - ModBase.Log("[ModPack] 整合包 Forge 版本:" + id); - forgeVersion = id.Replace("forge-", ""); - modLoader = ModComp.CompLoaderType.Forge; - } - else if (id.StartsWithF("neoforge-")) - { - // NeoForge 指定 - ModBase.Log("[ModPack] 整合包 NeoForge 版本:" + id); - neoForgeVersion = id.Replace("neoforge-", ""); - modLoader = ModComp.CompLoaderType.NeoForge; - } - else if (id.StartsWithF("fabric-")) - { - // Fabric 指定 - try - { - ModBase.Log("[ModPack] 整合包 Fabric 版本:" + id); - fabricVersion = id.Replace("fabric-", ""); - modLoader = ModComp.CompLoaderType.Fabric; - break; - } - catch (Exception ex) - { - ModBase.Log(ex, "读取整合包 Fabric 版本失败:" + id); - } - } - else if (id.StartsWithF("quilt-")) - { - throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported")); - } - } - - // 解压 - var installTemp = ModMain.RequestTaskTempFolder(); - var installLoaders = new List(); - var overrideHome = (string)(json["overrides"] ?? ""); - if (!string.IsNullOrEmpty(overrideHome)) - installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), - task => - { - ExtractModpackFiles(installTemp, fileAddress, task, 0.6d); - CopyOverrideDirectory( - Path.Combine(installTemp, archiveBaseFolder, overrideHome == "." || overrideHome == "./" ? "" : overrideHome), - $@"{ModFolder.mcFolderSelected}versions\{instanceName}", task, 0.4d); - }) - { - ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, - block = false - }); // 每 6M 需要 1s - // 获取 Mod 列表 - var modList = new List(); - var modOptionalList = new List(); - foreach (var ModEntry in (dynamic)json["files"] ?? Array.Empty()) - { - if (ModEntry["projectID"] is null || ModEntry["fileID"] is null) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.ModMissingRequiredInfoSkipped", ModEntry)); - continue; - } - - modList.Add((int)ModEntry["fileID"]); - if (ModEntry["required"] is JsonNode requiredNode && !requiredNode.ToObject()) - modOptionalList.Add((int)ModEntry["fileID"]); - } - - if (modList.Any()) - { - var modDownloadLoaders = new List(); - // 获取 Mod 下载信息 - modDownloadLoaders.Add(new LoaderTask( - Lang.Text("Minecraft.Download.Modpack.Stage.PrepareModsDownloadInfo"), task => - { - var allowMirror = true; - JsonArray ret; - var tryCount = 0; - do - { - tryCount += 1; - ret = (JsonArray)((JsonObject)ModBase.GetJson(ModDownload.DlModRequest( - "https://api.curseforge.com/v1/mods/files", - "POST", "{\"fileIds\": [" + modList.Join(",") + "]}", "application/json", - allowMirror)))["data"]; - if (modList.Count <= ret.Count) - { - ModBase.Log("[Modpack] 已获取到的模组数量足够,开始进行下一步"); - break; - } - - allowMirror = false; - ModBase.Log($"[Modpack] 获取模组数量不达标,设置镜像源允许状态为: {allowMirror}"); - if (tryCount > 3) throw new Exception(Lang.Text("Minecraft.Download.Modpack.SomeModsDeleted")); - } while (true); - - task.output = ret; - }) - { - ProgressWeight = modList.Count / 10d - }); // 每 10 Mod 需要 1s - // 构造 NetFile - modDownloadLoaders.Add(new LoaderTask>( - Lang.Text("Minecraft.Download.Modpack.Stage.BuildModsDownloadInfo"), task => - { - var fileList = new Dictionary(); - foreach (var ModJson in task.input) - { - var id = ModJson["id"].ToObject(); - // 跳过重复的 Mod(疑似 CurseForge Bug) - if (fileList.ContainsKey(id)) - continue; - // 可选 Mod 提示 - if (modOptionalList.Contains(id)) - if (ModMain.MyMsgBox( - Lang.Text("Minecraft.Download.Modpack.OptionalFile.Message", ModJson["displayName"]), - Lang.Text("Minecraft.Download.Modpack.OptionalFile.Title"), - Lang.Text("Minecraft.Download.Modpack.OptionalFile.Download"), - Lang.Text("Minecraft.Download.Modpack.OptionalFile.Skip") - ) == 2) - continue; - - // 根据 modules 和文件名后缀判断资源类型 - string targetFolder; - ModComp.CompType type; - if (ModJson["modules"].AsArray().Any()) // modules 可能返回 null(#1006) - { - var moduleNames = ((JsonArray)ModJson["modules"]).Select(l => l["name"].ToString()).ToList(); - if (moduleNames.Contains("META-INF") || moduleNames.Contains("mcmod.info") || - (ModJson?["FileName"]?.ToString()?.EndsWithF(".jar", true)).GetValueOrDefault()) - { - targetFolder = "mods"; - type = ModComp.CompType.Mod; - } - else if (moduleNames.Contains("pack.mcmeta")) - { - targetFolder = "resourcepacks"; - type = ModComp.CompType.ResourcePack; - } - else if (moduleNames.Contains("level.dat")) - { - targetFolder = "saves"; - type = ModComp.CompType.World; - } - else - { - targetFolder = "shaderpacks"; - type = ModComp.CompType.Shader; - } - } - else - { - targetFolder = "mods"; - type = ModComp.CompType.Mod; - } - - // 建立 CompFile - var file = new ModComp.CompFile((JsonObject)ModJson, type); - if (!file.Available) - continue; - // 实际的添加 - fileList.Add(id, - file.ToNetFile($@"{ModFolder.mcFolderSelected}versions\{instanceName}\{targetFolder}\", - ModComp.DownloadReason.ModPack, json["minecraft"]!["version"]!.ToString(), modLoader)); - task.Progress += 1d / (1 + modList.Count); - } - - task.output = fileList.Values.ToList(); - }) - { - ProgressWeight = modList.Count / 200d, - show = false - }); // 每 200 Mod 需要 1s - // 下载 Mod 文件 - modDownloadLoaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadMods"), []) - { ProgressWeight = modList.Count * 1.5d }); // 每个 Mod 需要 1.5s - // 构造加载器 - installLoaders.Add( - new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadMods.MainLoader"), - modDownloadLoaders) - { show = false, ProgressWeight = modDownloadLoaders.Sum(l => l.ProgressWeight) }); - } - - // 构造加载器 - var request = new ModDownloadLib.McInstallRequest - { - targetInstanceName = instanceName, - targetInstanceFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\", - minecraftName = json["minecraft"]["version"].ToString(), - forgeVersion = forgeVersion, - neoForgeVersion = neoForgeVersion, - fabricVersion = fabricVersion, - }; - var mergeLoaders = ModDownloadLib.McInstallLoader(request); - // 构造总加载器 - var loaders = new List(); - loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), - installLoaders) - { show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) }); - loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders) - { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) }); - loaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.FinalizeFiles"), task => - { - // 设置图标 - var versionFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\"; - if (logo is not null && File.Exists(logo)) - { - File.Copy(logo, Path.Combine(versionFolder, "PCL", "Logo.png"), true); - States.Instance.LogoPath[versionFolder] = @"PCL\Logo.png"; - States.Instance.IsLogoCustom[versionFolder] = true; - ModBase.Log("[ModPack] 已设置整合包 Logo:" + logo); - } - - // 删除原始整合包文件 - foreach (var Target in new[] { Path.Combine(versionFolder, "原始整合包.zip"), Path.Combine(versionFolder, "原始整合包.mrpack") }) - if (File.Exists(Target)) - { - ModBase.Log("[ModPack] 删除原始整合包文件:" + Target); - File.Delete(Target); - } - - if (File.Exists(fileAddress) && ModBase.GetFileNameWithoutExtentionFromPath(fileAddress) == "modpack") - { - ModBase.Log("[ModPack] 删除安装整合包文件:" + fileAddress); - File.Delete(fileAddress); - } - - // 整合包版本 - if (json["version"] is not null) States.Instance.ModpackVersion[versionFolder] = json["version"].ToString(); - States.Instance.ModpackSource[versionFolder] = "CurseForge"; - States.Instance.ModpackId[versionFolder] = resourceId; - do - { - try - { - var projects = ModComp.CompRequest.GetCompProjectsByIds([resourceId]); - if (projects.Count == 0) - break; - States.Instance.CustomInfo[versionFolder] = projects.First().Description; - } - catch (Exception ex) - { - ModBase.Log(ex, "[ModPack] 获取整合包描述文本失败"); - } - } while (false); - }) - { - ProgressWeight = 0.1d, - show = false - }); - - // 重复任务检查 - var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.CurseForgeInstall", instanceName); - if (loaderTaskbar.Any(l => (l.name ?? "") == (loaderName ?? ""))) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.Installing"), HintType.Error); + var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions")); + if (!validate.Validate(fallback).IsValid) + fallback = ""; + if (string.IsNullOrEmpty(fallback)) + fallback = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "", + [validate]); + if (string.IsNullOrEmpty(fallback)) throw new ModBase.CancelledException(); - } - - // 启动 - var loader = new LoaderCombo(loaderName, loaders) { OnStateChanged = ModDownloadLib.McInstallState }; - loader.Start(request.targetInstanceFolder); - LoaderTaskbarAdd(loader); - ModMain.frmMain.BtnExtraDownload.ShowRefresh(); - if (!isOnlineInstall) - ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager)); - return loader; + return fallback; } - #endregion - - #region Modrinth - - private static LoaderCombo InstallPackModrinth(string fileAddress, ZipArchive archive, - string archiveBaseFolder, string instanceName = null, string logo = null, string resourceId = null, - bool isOnlineInstall = false) + /// + /// 拼接指定实例的版本文件夹路径(以 \ 结尾)。 + /// + private static string _GetVersionFolder(string instanceName) { - // 读取 Json 文件 - JsonObject json; - try - { - json = (JsonObject)ModBase.GetJson( - ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "modrinth.index.json").Open())); - } - catch (Exception ex) - { - throw new Exception("Modrinth 整合包安装信息存在问题", ex); - } - - if (json["dependencies"] is null || json["dependencies"]["minecraft"] is null) - throw new Exception("Modrinth 整合包未提供 Minecraft 版本信息"); - // 获取 Mod API 版本信息 - string minecraftVersion = null; - string forgeVersion = null; - string neoForgeVersion = null; - string fabricVersion = null; - var modLoader = ModComp.CompLoaderType.Any; - foreach (var Entry in json["dependencies"]?.AsObject() ?? new JsonObject()) - switch (Entry.Key.ToLower() ?? "") - { - case "minecraft": - { - minecraftVersion = Entry.Value?.ToObject(); - break; - } - case "forge": // eg. 14.23.5.2859 / 1.19-41.1.0 - { - forgeVersion = Entry.Value?.ToObject(); - modLoader = ModComp.CompLoaderType.Forge; - ModBase.Log("[ModPack] 整合包 Forge 版本:" + forgeVersion); - break; - } - case "neoforge": - case "neo-forge": // eg. 20.6.98-beta - { - neoForgeVersion = Entry.Value?.ToObject(); - modLoader = ModComp.CompLoaderType.NeoForge; - ModBase.Log("[ModPack] 整合包 NeoForge 版本:" + neoForgeVersion); - break; - } - case "fabric-loader": // eg. 0.14.14 - { - fabricVersion = Entry.Value?.ToObject(); - modLoader = ModComp.CompLoaderType.Fabric; - ModBase.Log("[ModPack] 整合包 Fabric 版本:" + fabricVersion); - break; - } - case "quilt-loader": // eg. 0.26.0 - { - throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported")); - } - - default: - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.UnknownLoader", Entry.Key, Entry.Value), - HintType.Error); - break; - } - } - - // 获取实例名 - if (instanceName is null) - { - instanceName = (string)(json["name"] ?? ""); - var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions")); - if (!validate.Validate(instanceName).IsValid) - instanceName = ""; - if (string.IsNullOrEmpty(instanceName)) - instanceName = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "", - [validate]); - if (string.IsNullOrEmpty(instanceName)) - throw new ModBase.CancelledException(); - } - - // 解压 - var installTemp = ModMain.RequestTaskTempFolder(); - var installLoaders = new List(); - installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), - task => - { - ExtractModpackFiles(installTemp, fileAddress, task, 0.5d); - CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "overrides"), - Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4d); - CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "client-overrides"), - Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.1d); - }) - { - ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, - block = false - }); // 每 6M 需要 1s - // 获取下载文件列表 - var fileList = new List(); - foreach (var File in (dynamic)json["files"] ?? Array.Empty()) - { - // 检查是否需要该文件 - if (File["env"] is not null) - switch (File["env"]["client"].ToString() ?? "") - { - case "optional": - { - if (ModMain.MyMsgBox( - Lang.Text("Minecraft.Download.Modpack.OptionalFile.Message", - ModBase.GetFileNameFromPath(File["path"].ToString())), - Lang.Text("Minecraft.Download.Modpack.OptionalFile.Title"), - Lang.Text("Minecraft.Download.Modpack.OptionalFile.Download"), - Lang.Text("Minecraft.Download.Modpack.OptionalFile.Skip") - ) == 2) continue; - - break; - } - case "unsupported": - { - continue; - } - } - - // 添加下载文件 - var urls = ((JsonArray)File["downloads"]) - .OfType() - .Select(x => ModComp.CompFile.HandleCurseForgeDownloadUrls(x.ToString())) - .ToList(); - // 镜像源 - urls = urls.SelectMany(x => ModDownload.DlSourceModDownloadGet(x)).ToList(); - var targetPath = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\{File["path"]}"; - if (!Path.GetFullPath(targetPath) - .StartsWithF($@"{ModFolder.mcFolderSelected}versions\{instanceName}\", true)) - { - ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.PathOutsideInstance.Message", targetPath), - Lang.Text("Minecraft.Download.Modpack.PathOutsideInstance.Title"), isWarn: true); - throw new ModBase.CancelledException(); - } - - fileList.Add(new DownloadFile( - ModComp.CompFile.HandleModrinthDownloadUrls(urls, ModComp.DownloadReason.ModPack, minecraftVersion, - modLoader), targetPath, - new ModBase.FileChecker(actualSize: ((JsonNode)File["fileSize"]).ToObject(), - hash: File["hashes"]["sha1"].ToString()), true)); - } - - if (fileList.Any()) - installLoaders.Add( - new LoaderDownload(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadAdditions"), fileList) - { ProgressWeight = fileList.Count * 1.5d }); // 每个 Mod 需要 1.5s - - // 构造加载器 - var request = new ModDownloadLib.McInstallRequest - { - targetInstanceName = instanceName, - targetInstanceFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\", - minecraftName = minecraftVersion, - forgeVersion = forgeVersion, - neoForgeVersion = neoForgeVersion, - fabricVersion = fabricVersion, - }; - var mergeLoaders = ModDownloadLib.McInstallLoader(request); - // 构造总加载器 - var loaders = new List(); - loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), - installLoaders) - { show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) }); - loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders) - { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) }); - loaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.FinalizeFiles"), task => - { - // 设置图标 - var versionFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\"; - if (logo is not null && File.Exists(logo)) - { - File.Copy(logo, Path.Combine(versionFolder, "PCL", "Logo.png"), true); - States.Instance.LogoPath[versionFolder] = @"PCL\Logo.png"; - States.Instance.IsLogoCustom[versionFolder] = true; - ModBase.Log("[ModPack] 已设置整合包 Logo:" + logo); - } - - // 删除原始整合包文件 - foreach (var Target in new[] { Path.Combine(versionFolder, "原始整合包.zip"), Path.Combine(versionFolder, "原始整合包.mrpack") }) - if (File.Exists(Target)) - { - ModBase.Log("[ModPack] 删除原始整合包文件:" + Target); - File.Delete(Target); - } - - if (File.Exists(fileAddress) && ModBase.GetFileNameWithoutExtentionFromPath(fileAddress) == "modpack") - { - ModBase.Log("[ModPack] 删除安装整合包文件:" + fileAddress); - File.Delete(fileAddress); - } - - // 整合包版本 - if (json["versionId"] is not null) - States.Instance.ModpackVersion[versionFolder] = json["versionId"].ToString(); - States.Instance.ModpackSource[versionFolder] = "Modrinth"; - States.Instance.ModpackId[versionFolder] = resourceId; - do - { - try - { - var projects = ModComp.CompRequest.GetCompProjectsByIds([resourceId]); - if (projects.Count == 0) - break; - States.Instance.CustomInfo[versionFolder] = projects.First().Description; - } - catch (Exception ex) - { - ModBase.Log(ex, "[ModPack] 获取整合包描述文本失败"); - } - } while (false); - }) - { - ProgressWeight = 0.1d, - show = false - }); - - // 重复任务检查 - var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.ModrinthInstall", instanceName); - if (loaderTaskbar.Any(l => (l.name ?? "") == (loaderName ?? ""))) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.Installing"), HintType.Error); - throw new ModBase.CancelledException(); - } - - // 启动 - var loader = new LoaderCombo(loaderName, loaders) { OnStateChanged = ModDownloadLib.McInstallState }; - loader.Start(request.targetInstanceFolder); - LoaderTaskbarAdd(loader); - ModMain.frmMain.BtnExtraDownload.ShowRefresh(); - if (!isOnlineInstall) - ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager)); - return loader; + return $@"{ModFolder.mcFolderSelected}versions\{instanceName}\"; } - #endregion - - #region HMCL - - private static LoaderCombo InstallPackHMCL(string fileAddress, ZipArchive archive, string archiveBaseFolder) + /// + /// 组装顶层安装加载器:重复任务检查、启动、加入任务栏、刷新下载按钮并跳转任务管理器页。 + /// + private static LoaderCombo _StartInstall(string loaderName, List loaders, string input, + bool isOnlineInstall) { - // 读取 Json 文件 - JsonObject json; - try - { - json = (JsonObject)ModBase.GetJson( - ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "modpack.json").Open(), Encoding.UTF8)); - } - catch (Exception ex) - { - throw new Exception("HMCL 整合包安装信息存在问题", ex); - } - - // 获取实例名 - var instanceName = (string)(json["name"] ?? ""); - var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions")); - if (!validate.Validate(instanceName).IsValid) - instanceName = ""; - if (string.IsNullOrEmpty(instanceName)) - instanceName = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "", - [validate]); - if (string.IsNullOrEmpty(instanceName)) - throw new ModBase.CancelledException(); - // 解压 - var installTemp = ModMain.RequestTaskTempFolder(); - var installLoaders = new List(); - installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), - task => - { - ExtractModpackFiles(installTemp, fileAddress, task, 0.6d); - CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "minecraft"), - Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4d); - }) - { - ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, - block = false - }); // 每 6M 需要 1s - // 构造游戏本体安装加载器 - if (json["gameVersion"] is null) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.Hmcl")); - var request = new ModDownloadLib.McInstallRequest - { - targetInstanceName = instanceName, - targetInstanceFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\", - minecraftName = json["gameVersion"].ToString() - }; - var mergeLoaders = ModDownloadLib.McInstallLoader(request); - // 构造总加载器 - var loaders = new List - { - new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), installLoaders) - { show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) }, - new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders) - { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) } - }; // 重复任务检查 - var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.HmclInstall", instanceName); if (loaderTaskbar.Any(l => (l.name ?? "") == (loaderName ?? ""))) { HintService.Hint(Lang.Text("Minecraft.Download.Modpack.Installing"), HintType.Error); @@ -966,758 +255,61 @@ private static LoaderCombo InstallPackHMCL(string fileAddress, ZipArchiv // 启动 var loader = new LoaderCombo(loaderName, loaders) { OnStateChanged = ModDownloadLib.McInstallState }; - loader.Start(request.targetInstanceFolder); + loader.Start(input); LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); - ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager)); + if (!isOnlineInstall) + ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager)); return loader; } - #endregion - - #region MCBBS - - private static LoaderCombo InstallPackMCBBS(string fileAddress, ZipArchive archive, - string archiveBaseFolder, string instanceName = null) + /// + /// 整合包安装收尾:设置图标、删除原始整合包文件、写入整合包版本/来源/ID 信息并尝试获取整合包描述。 + /// + private static void _FinalizeInstance(string versionFolder, string fileAddress, string logo, string modpackSource, + string modpackVersion, string resourceId) { - // 读取 Json 文件 - JsonObject json; - try + // 设置图标 + if (logo is not null && File.Exists(logo)) { - // VB 的 If(a, b) 在 C# 中如果是 null 合并则用 ??,如果是三元运算则用 ?: - var entry = archive.GetEntry(archiveBaseFolder + "mcbbs.packmeta") ?? - archive.GetEntry(archiveBaseFolder + "manifest.json"); - using (var stream = entry.Open()) - { - json = (JsonObject)ModBase.GetJson(ModBase.ReadFile(stream, Encoding.UTF8)); - } + File.Copy(logo, Path.Combine(versionFolder, "PCL", "Logo.png"), true); + States.Instance.LogoPath[versionFolder] = @"PCL\Logo.png"; + States.Instance.IsLogoCustom[versionFolder] = true; + ModBase.Log("[ModPack] 已设置整合包 Logo:" + logo); } - catch (Exception ex) - { - throw new Exception("MCBBS 整合包安装信息存在问题", ex); - } - - // 获取实例名 - if (instanceName is null) - { - instanceName = json["name"]?.ToString() ?? ""; - var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions")); - - if (!validate.Validate(instanceName).IsValid) instanceName = ""; - - if (string.IsNullOrEmpty(instanceName)) - instanceName = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "", - [validate]); - - if (string.IsNullOrEmpty(instanceName)) throw new ModBase.CancelledException(); - } - - // 解压与路径准备 - var installTemp = ModMain.RequestTaskTempFolder(); - var versionFolder = $"{ModFolder.mcFolderSelected}versions\\{instanceName}"; - var installLoaders = new List(); - - // 解压整合包文件任务 - var unzipTask = new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), - task => - { - ExtractModpackFiles(installTemp, fileAddress, task, 0.6); - CopyOverrideDirectory( - Path.Combine(installTemp, archiveBaseFolder, "overrides"), - Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), - task, 0.4); - // JVM 参数处理 - if (json["launchInfo"] is not null) + // 删除原始整合包文件 + foreach (var target in new[] { Path.Combine(versionFolder, "原始整合包.zip"), Path.Combine(versionFolder, "原始整合包.mrpack") }) + if (File.Exists(target)) { - var launchInfo = (JsonObject)json["launchInfo"]; - Config.Instance.JvmArgs[versionFolder] = string.Join(" ", launchInfo["javaArgument"]); - Config.Instance.GameArgs[versionFolder] = string.Join(" ", launchInfo["launchArgument"]); + ModBase.Log("[ModPack] 删除原始整合包文件:" + target); + File.Delete(target); } - // 整合包版本 - if (json["version"] is not null) States.Instance.ModpackVersion[versionFolder] = json["version"].ToString(); - }); - - unzipTask.ProgressWeight = new FileInfo(fileAddress).Length / 1024.0 / 1024.0 / 6.0; // 每 6M 需要 1s - unzipTask.block = false; - installLoaders.Add(unzipTask); - - // 构造加载器 - if (json["addons"] is null) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.McbbsAddons")); - - var addons = new Dictionary(); - foreach (var EntryNode in json["addons"].AsArray()) { var entry = EntryNode.AsObject(); addons.Add(entry["id"].ToString(), entry["version"].ToString()); } - - if (!addons.ContainsKey("game")) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.Generic"), HintType.Error); - return null; - } - - if (addons.ContainsKey("quilt")) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported")); - - // 构造安装请求 - var request = new ModDownloadLib.McInstallRequest - { - targetInstanceName = instanceName, - targetInstanceFolder = $"{ModFolder.mcFolderSelected}versions\\{instanceName}\\", - minecraftName = addons["game"], - optiFineVersion = addons.ContainsKey("optifine") ? addons["optifine"] : null, - forgeVersion = addons.ContainsKey("forge") ? addons["forge"] : null, - neoForgeVersion = addons.ContainsKey("neoforge") ? addons["neoforge"] : null, - fabricVersion = addons.ContainsKey("fabric") ? addons["fabric"] : null, - }; - - var mergeLoaders = ModDownloadLib.McInstallLoader(request); - - // 构造总加载器 - var loaders = new List(); - loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), - installLoaders) + if (File.Exists(fileAddress) && ModBase.GetFileNameWithoutExtentionFromPath(fileAddress) == "modpack") { - show = false, - block = false, - ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) - }); - loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders) - { - show = false, - ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) - }); - - // 重复任务检查 - var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.McbbsInstall", instanceName); - if (loaderTaskbar.Any(l => l.name == loaderName)) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.Installing"), HintType.Error); - throw new ModBase.CancelledException(); + ModBase.Log("[ModPack] 删除安装整合包文件:" + fileAddress); + File.Delete(fileAddress); } - // 启动任务 - var loader = new LoaderCombo(loaderName, loaders); - loader.OnStateChanged = ModDownloadLib.McInstallState; - - loader.Start(request.targetInstanceFolder); - LoaderTaskbarAdd(loader); - - ModMain.frmMain.BtnExtraDownload.ShowRefresh(); - ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager)); - - return loader; - } - - #endregion - - #region 带启动器的压缩包 - - private static LoaderCombo InstallPackLauncherPack(string fileAddress, ZipArchive archive, - string archiveBaseFolder) - { - // 获取解压路径 - ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.SelectEmptyFolder.Message"), - Lang.Text("Common.Action.Install"), Lang.Text("Common.Action.Continue"), forceWait: true); - var targetFolder = SystemDialogs.SelectFolder(Lang.Text("Minecraft.Download.Modpack.SelectTargetFolder.Title")); - if (string.IsNullOrEmpty(targetFolder)) - throw new ModBase.CancelledException(); - if (Directory.GetFileSystemEntries(targetFolder).Length > 0) + // 整合包版本 + if (modpackVersion is not null) States.Instance.ModpackVersion[versionFolder] = modpackVersion; + States.Instance.ModpackSource[versionFolder] = modpackSource; + States.Instance.ModpackId[versionFolder] = resourceId; + do { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.TargetFolderMustBeEmpty"), HintType.Error); - throw new ModBase.CancelledException(); - } - - // 解压 - var loader = new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[] - { - new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => - { - ExtractModpackFiles(targetFolder, fileAddress, task, 0.9d); - Thread.Sleep(400); // 避免文件争用 - // 查找解压后的 exe 文件 - string launcher = null; - foreach (var ExeFile in Directory.GetFiles(targetFolder, "*.exe", SearchOption.TopDirectoryOnly)) - { - var info = FileVersionInfo.GetVersionInfo(ExeFile); - ModBase.Log($"[Modpack] 文件 {ExeFile} 的产品名标识为 {info.ProductName}"); - if (info.ProductName == "Plain Craft Launcher") - { - launcher = ExeFile; - ModBase.Log($"[Modpack] 发现整合包附带的 PCL 启动器:{ExeFile}"); - } - else if ((info.ProductName.ContainsF("Launcher", true) || info.ProductName.ContainsF("启动", true)) && - !(info.ProductName == "Plain Craft Launcher Admin Manager")) - { - if (launcher is null) - { - launcher = ExeFile; - ModBase.Log($"[Modpack] 发现整合包附带的疑似第三方启动器:{ExeFile}"); - } - } - } - - task.Progress = 0.95d; - // 尝试使用附带的启动器打开 - if (launcher is not null) - { - ModBase.Log("[Modpack] 找到压缩包中附带的启动器:" + launcher); - if (ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.BundledLauncher.Message", launcher), - Lang.Text("Minecraft.Download.Modpack.BundledLauncher.Title"), - Lang.Text("Minecraft.Download.Modpack.BundledLauncher.UseBundled"), - Lang.Text("Minecraft.Download.Modpack.BundledLauncher.DoNotUse") - ) == 1) - { - ModBase.OpenExplorer(targetFolder); - ModBase.ShellOnly(launcher, "--wait"); // 要求等待已有的 PCL 退出 - ModBase.Log("[Modpack] 为换用整合包中的启动器启动,强制结束程序"); - ModMain.frmMain.EndProgram(false); - return; - } - } - else - { - ModBase.Log("[Modpack] 未找到压缩包中附带的启动器"); - } - - ModBase.OpenExplorer(targetFolder); - // 加入文件夹列表 - var instanceName = ModBase.GetFolderNameFromPath(targetFolder); - Directory.CreateDirectory(Path.Combine(targetFolder, ".minecraft")); - PageSelectLeft.AddFolder( - Path.Combine(targetFolder, ".minecraft", archiveBaseFolder.Replace("/", @"\").TrimStart('\\')), instanceName, - false); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串) - // 调用 modpack 文件进行安装 - var modpackFile = Directory.GetFiles(targetFolder, "modpack.*", SearchOption.AllDirectories).First(); - ModBase.Log("[Modpack] 调用 modpack 文件继续安装:" + modpackFile); - ModpackInstall(modpackFile); - }) - }); - loader.Start(targetFolder); - LoaderTaskbarAdd(loader); - ModMain.frmMain.BtnExtraDownload.ShowRefresh(); - ModMain.frmMain.BtnExtraDownload.Ribble(); - return loader; - } - - #endregion - - #region 普通压缩包 - - private static LoaderCombo InstallPackCompress(string fileAddress, ZipArchive archive) - { - // 尝试定位 .minecraft 文件夹:寻找形如 “/versions/XXX/XXX.json” 的路径 - Match match = null; - var regex = new Regex(@"^.*\/(?=versions\/(?[^\/]+)\/(\k)\.json$)", RegexOptions.IgnoreCase); - foreach (var Entry in archive.Entries) - { - var entryMatch = regex.Match("/" + Entry.FullName); - if (entryMatch.Success) - { - match = entryMatch; - break; - } - } - - if (match is null) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.UnknownArchiveStructure")); // 没有匹配 - var archiveBaseFolder = match.Value.Replace("/", @"\").TrimStart('\\'); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串) - var instanceName = match.Groups[1].Value; - ModBase.Log("[ModPack] 检测到压缩包的 .minecraft 根目录:" + archiveBaseFolder + ",命中的实例名:" + instanceName); - // 获取解压路径 - ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.SelectEmptyFolder.Message"), - Lang.Text("Common.Action.Install"), Lang.Text("Common.Action.Continue"), forceWait: true); - var targetFolder = SystemDialogs.SelectFolder(Lang.Text("Minecraft.Download.Modpack.SelectTargetFolder.Title")); - if (string.IsNullOrEmpty(targetFolder)) - throw new ModBase.CancelledException(); - if (targetFolder.Contains("!") || targetFolder.Contains(";")) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.InvalidGamePathChars", targetFolder), - HintType.Error); - throw new ModBase.CancelledException(); - } - - if (Directory.GetFileSystemEntries(targetFolder).Length > 0) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.TargetFolderMustBeEmpty"), HintType.Error); - throw new ModBase.CancelledException(); - } - - // 解压 - var loader = new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[] - { - new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => - { - ExtractModpackFiles(targetFolder, fileAddress, task, 0.95d); - // 加入文件夹列表 - PageSelectLeft.AddFolder(Path.Combine(targetFolder, archiveBaseFolder), ModBase.GetFolderNameFromPath(targetFolder), - false); - Thread.Sleep(400); // 避免文件争用 - ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.InstanceSelect)); - }) - }) - { - OnStateChanged = ModDownloadLib.McInstallState - }; - loader.Start(targetFolder); - LoaderTaskbarAdd(loader); - ModMain.frmMain.BtnExtraDownload.ShowRefresh(); - ModMain.frmMain.BtnExtraDownload.Ribble(); - return loader; - } - - #endregion - - #region MultiMC - - public class MMCPackInfo - { - public JsonObject additionalJson = new(); - public bool isCleanroomOverrided; - public bool isFabricOverrided; - public bool isForgeOverrided; - public bool isMcArgsEdited; - public bool isMinecraftOverrided; - public bool isNeoForgeOverrided; - public JsonArray jvmArgs = new(); - public JsonArray libraries = new(); - public JsonObject overridedJson = new(); - public string tweakers = null; - } - - private static LoaderCombo InstallPackMMC(string fileAddress, ZipArchive archive, string archiveBaseFolder) - { - // 读取 Json 文件 - JsonObject packJson; - string packInstance; - MMCPackInfo packInfo = null; - try - { - packJson = (JsonObject)ModBase.GetJson( - ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "mmc-pack.json").Open(), Encoding.UTF8)); - packInstance = ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "instance.cfg").Open(), Encoding.UTF8); - - #region JSON Patches - - // 参考 https://github.com/MultiMC/Launcher/wiki/JSON-Patches - do - { - try - { - if (!archive.Entries.Any(e => - e.FullName.Equals(archiveBaseFolder + "patches/", StringComparison.OrdinalIgnoreCase))) - break; - ModBase.Log("[ModPack] 安装的 MultiMC 整合包存在 JSON Patches"); - // 排序预处理 - var patches = new List>(); - foreach (var entry in archive.Entries) - if (!entry.FullName.EndsWith("/") && entry.FullName.StartsWith(archiveBaseFolder + "patches/")) - { - var patch = (JsonObject)ModBase.GetJson(ModBase.ReadFile( - archive.GetEntry(entry.FullName).Open(), Encoding.UTF8)); - patches.Add(new KeyValuePair(patch, - (int)(patch["order"] is not null ? patch["order"] : 0))); - } - - var components = (JsonArray)packJson["components"]; - var componentUids = components - .Select(c => c["uid"]?.ToString()) - .ToHashSet(); - - patches = patches - .Where(p => componentUids.Contains(p.Key["uid"]?.ToString())) - .OrderBy(p => p.Value) - .ToList(); - // 应用 Patches - packInfo = new MMCPackInfo(); - - string tweakers = null; - JsonObject assetIndex = null; - JsonObject javaVerJson = null; - string mainClass = null; - var gameArguments = new JsonArray(); - var jvmArguments = new JsonArray(); - var libJson = new JsonArray(); - var addLibJson = new JsonArray(); - foreach (var Patch in patches) - { - var patchJson = Patch.Key; - if ((string)patchJson["uid"] == "net.minecraft") - { - packInfo.isMinecraftOverrided = true; - } - else if ((string)patchJson["uid"] == "net.minecraftforge") - { - if (patchJson["version"].ToString().StartsWithF("0.")) - packInfo.isCleanroomOverrided = true; - else - packInfo.isForgeOverrided = true; - } - else if ((string)patchJson["uid"] == "net.neoforged") - { - packInfo.isNeoForgeOverrided = true; - } - else if ((string)patchJson["uid"] == "net.fabricmc.fabric-loader") - { - packInfo.isFabricOverrided = true; - } - else if ((string)patchJson["uid"] == "org.quiltmc.quilt-loader") - { - throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported")); - } - - // JVM 参数 - if (patchJson["+jvmArgs"] is not null) - { - jvmArguments.Merge(patchJson["+jvmArgs"]); - ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 JVM 参数"); - } - - // Libraries - if (patchJson["libraries"] is not null || patchJson["+libraries"] is not null) - { - var libs = new JsonArray(); - if (patchJson["libraries"] is not null) - foreach (var Library in patchJson["libraries"].AsArray()) - { - if (Library is not JsonObject LibraryObj) continue; - var libJobj = LibraryObj.DeepClone().AsObject(); - if (libJobj["MMC-hint"] is not null) - { - libJobj.Add("hint", libJobj["MMC-hint"]?.DeepClone()); - libJobj.Remove("MMC-hint"); - } - - libs.Add(libJobj); - } - - if (patchJson["+libraries"] is not null) - foreach (var Library in patchJson["+libraries"].AsArray()) // TODO: 此处处理不严谨,但也能用吧 - { - if (Library is not JsonObject LibraryObj) continue; - var libJobj = LibraryObj.DeepClone().AsObject(); - if (libJobj["MMC-hint"] is not null) - { - libJobj.Add("hint", libJobj["MMC-hint"]?.DeepClone()); - libJobj.Remove("MMC-hint"); - } - - libs.Add(libJobj); - } - - libJson.Merge(libs); - ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 Libraries"); - } - - // Tweakers - if (patchJson["+tweakers"] is not null) - { - tweakers = (string)patchJson["+tweakers"][0]; - ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 Tweakers"); - } - - // AssetIndex - if (patchJson["assetIndex"] is not null) - { - assetIndex = patchJson["assetIndex"]?.DeepClone().AsObject(); - ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 AssetIndex"); - } - - // minecraftArguments -> arguments.game - if (patchJson["minecraftArguments"] is not null) - { - foreach (var Arg in patchJson["minecraftArguments"].ToString().Split(" ")) - gameArguments.Add(Arg); - packInfo.isMcArgsEdited = true; - ModBase.Log( - $"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 minecraftArguments 至 arguments.game"); - } - - // mainClass - if (patchJson["mainClass"] is not null) - { - mainClass = (string)patchJson["mainClass"]; - ModBase.Log($"[ModPack] 已应用 JSON-Patch {patchJson["uid"]} 的 mainClass"); - } - - // Java 版本要求 - if (patchJson["compatibleJavaMajors"] is not null) - { - var javaVersion = 0; - string javaComponent = null; - var javaMajors = (JsonArray)patchJson["compatibleJavaMajors"]; - foreach (var Java in javaMajors) - { - if (javaVersion > ModBase.Val(Java)) - continue; - // 优先选择主要的版本 - if (ModBase.Val(Java) == 21d) - { - javaVersion = 21; - javaComponent = "java-runtime-delta"; - } - else if (ModBase.Val(Java) == 17d) - { - javaVersion = 17; - javaComponent = "java-runtime-gamma"; - } - else if (ModBase.Val(Java) == 11d) - { - javaVersion = 11; - javaComponent = null; - } - else if (ModBase.Val(Java) == 8d) - { - javaVersion = 8; - javaComponent = "jre-legacy"; - } - } - - if (javaVersion == 0) - { - javaVersion = (int)javaMajors[0]; - javaComponent = null; - } - - javaVerJson = new JsonObject { { "majorVersion", javaVersion } }; - if (javaComponent is not null) javaVerJson.Add("component", javaComponent); - ModBase.Log($"[ModPack] JSON-Patch {patchJson["uid"]} 要求 Java 版本: " + javaVersion); - } - } - - JsonObject jsonArguments = null; - if (!string.IsNullOrWhiteSpace(tweakers)) - { - gameArguments.Add("--tweakClass"); - gameArguments.Add(tweakers); - } - - if (gameArguments is not null || jvmArguments is not null) - { - jvmArguments.Insert(0, "-Djava.library.path=${natives_directory}"); - jvmArguments.Insert(1, "-Dminecraft.launcher.brand=${launcher_name}"); - jvmArguments.Insert(2, "-Dminecraft.launcher.version=${launcher_version}"); - jvmArguments.Insert(3, "-cp"); - jvmArguments.Insert(4, "${classpath}"); - jsonArguments = new JsonObject { { "game", gameArguments }, { "jvm", jvmArguments } }; - } - - packInfo.overridedJson = new JsonObject(); - if (jsonArguments is not null) - packInfo.overridedJson.Add("arguments", jsonArguments); - if (mainClass is not null) - packInfo.overridedJson.Add("mainClass", mainClass); - if (assetIndex is not null) - packInfo.overridedJson.Add("assetIndex", assetIndex); - if (javaVerJson is not null) - packInfo.overridedJson.Add("javaVersion", javaVerJson); - if (libJson is not null) - packInfo.overridedJson.Add("libraries", libJson); - } - catch (Exception ex) - { - ModBase.Log(ex, "应用 MMC JSON-Patches 失败"); - } - } while (false); - } - - #endregion - - catch (Exception ex) - { - throw new Exception("MMC 整合包安装信息存在问题", ex); - } - - // 获取实例名 - var instanceName = packInstance.RegexSeek(@"(?<=\nname\=)[^\n]+") ?? ""; - var validate = new FolderNameValidator(Path.Combine(ModFolder.mcFolderSelected, "versions")); - if (!validate.Validate(instanceName).IsValid) - instanceName = ""; - if (string.IsNullOrEmpty(instanceName)) - instanceName = ModMain.MyMsgBoxInput(Lang.Text("Minecraft.Download.Modpack.InputInstanceName"), "", "", - [validate]); - if (string.IsNullOrEmpty(instanceName)) - throw new ModBase.CancelledException(); - // 解压 - var installTemp = ModMain.RequestTaskTempFolder(); - var versionFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}"; - var installLoaders = new List(); - installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), - task => - { - ExtractModpackFiles(installTemp, fileAddress, task, 0.55d); - CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "libraries"), - Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName, "libraries"), task, 0.2d); - CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, ".minecraft"), - Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.2d); - - #region instance.cfg - - // 读取 MMC 设置文件(#2655) try { - var mMCSetupFile = Path.Combine(installTemp, archiveBaseFolder, "instance.cfg"); - // 将其中的等号替换为冒号,以符合 ini 文件格式 - if (File.Exists(mMCSetupFile)) - { - List lines = []; - foreach (var Line in ModBase.ReadFile(mMCSetupFile).Split(new[] { "\r", "\n" }, - StringSplitOptions.RemoveEmptyEntries)) - { - if (!Line.Contains("=")) - continue; - lines.Add(Line.BeforeFirst("=") + ":" + Line.AfterFirst("=")); - } - - ModBase.WriteFile(mMCSetupFile, lines.Join("\r\n")); - // 读取文件 - if (Convert.ToBoolean(ModBase.ReadIni(mMCSetupFile, "OverrideCommands", - false.ToString()))) - { - var preLaunchCommand = ModBase.ReadIni(mMCSetupFile, "PreLaunchCommand"); - if (!string.IsNullOrEmpty(preLaunchCommand)) - { - preLaunchCommand = preLaunchCommand.Replace(@"\""", "\"") - .Replace("$INST_JAVA", "{java}java.exe").Replace(@"$INST_MC_DIR\", "{minecraft}") - .Replace("$INST_MC_DIR", "{minecraft}").Replace(@"$INST_DIR\", "{verpath}") - .Replace("$INST_DIR", "{verpath}").Replace("$INST_ID", "{name}") - .Replace("$INST_NAME", "{name}"); - Config.Instance.PreLaunchCommand[versionFolder] = preLaunchCommand; - ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:启动前执行命令:" + preLaunchCommand); - } - } - - if (Convert.ToBoolean(ModBase.ReadIni(mMCSetupFile, "JoinServerOnLaunch", - false.ToString()))) - { - var serverAddress = ModBase.ReadIni(mMCSetupFile, "JoinServerOnLaunchAddress") - .Replace(@"\""", "\""); - Config.Instance.ServerToEnter[versionFolder] = serverAddress; - ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:自动进入服务器:" + serverAddress); - } - - if (Convert.ToBoolean(ModBase.ReadIni(mMCSetupFile, "IgnoreJavaCompatibility", - false.ToString()))) - { - Config.Instance.IgnoreJavaCompatibility[versionFolder] = true; - ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:忽略 Java 兼容性警告"); - } - - var logo = Path.GetFileName(ModBase.ReadIni(mMCSetupFile, "iconKey")); - if (!string.IsNullOrEmpty(logo) && File.Exists($"{installTemp}{archiveBaseFolder}{logo}.png")) - { - States.Instance.IsLogoCustom[versionFolder] = true; - States.Instance.LogoPath[versionFolder] = @"PCL\Logo.png"; - ModBase.CopyFile($"{installTemp}{archiveBaseFolder}{logo}.png", - $@"{ModFolder.mcFolderSelected}versions\{instanceName}\PCL\Logo.png"); - ModBase.Log($"[ModPack] 迁移 MultiMC 实例独立设置:实例图标({logo}.png)"); - } - - // JVM 参数 - var jvmArgs = ModBase.ReadIni(mMCSetupFile, "JvmArgs"); - if (!string.IsNullOrEmpty(jvmArgs)) - { - if (Convert.ToBoolean(ModBase.ReadIni(mMCSetupFile, "OverrideJavaArgs", - false.ToString()))) - { - Config.Instance.JvmArgs[versionFolder] = jvmArgs; - ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:JVM 参数(覆盖):" + jvmArgs); - } - else - { - jvmArgs = jvmArgs + - " " + - Config.Launch.JvmArgs; - Config.Instance.JvmArgs[versionFolder] = jvmArgs; - ModBase.Log("[ModPack] 迁移 MultiMC 实例独立设置:JVM 参数(追加):" + jvmArgs); - } - } - } + var projects = ModComp.CompRequest.GetCompProjectsByIds([resourceId]); + if (projects.Count == 0) + break; + States.Instance.CustomInfo[versionFolder] = projects.First().Description; } catch (Exception ex) { - ModBase.Log(ex, $"读取 MMC 配置文件失败({installTemp}{archiveBaseFolder}instance.cfg)"); - } - - #endregion - }) - { - ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, - block = false - }); // 每 6M 需要 1s - // 构造实例安装请求 - if (packJson["components"] is null) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.Generic")); - var request = new ModDownloadLib.McInstallRequest - { - targetInstanceName = instanceName, - targetInstanceFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\" - }; - foreach (var Component in packJson["components"].AsArray()) - switch ((Component["uid"] ?? "").ToString() ?? "") - { - case "org.lwjgl": - { - ModBase.Log("[ModPack] 已跳过 LWJGL 项"); - break; - } - case "net.minecraft": - { - request.minecraftName = (string)Component["version"]; - break; - } - case "net.minecraftforge": - { - if (Component["version"].ToString().StartsWithF("0.")) - request.cleanroomVersion = (string)Component["version"]; - else - request.forgeVersion = (string)Component["version"]; - - break; - } - case "net.neoforged": - { - request.neoForgeVersion = (string)Component["version"]; - break; - } - case "net.fabricmc.fabric-loader": - { - request.fabricVersion = (string)Component["version"]; - break; - } - case "org.quiltmc.quilt-loader": - { - throw new Exception(Lang.Text("Minecraft.Download.Modpack.QuiltUnsupported")); - } + ModBase.Log(ex, "[ModPack] 获取整合包描述文本失败"); } - - if (packInfo is not null) - request.mmcPackInfo = packInfo; - // 构造加载器 - var mergeLoaders = ModDownloadLib.McInstallLoader(request); - // 构造总加载器 - var loaders = new List(); - loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ModpackInstall"), - installLoaders) - { show = false, block = false, ProgressWeight = installLoaders.Sum(l => l.ProgressWeight) }); - loaders.Add(new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.GameInstall"), mergeLoaders) - { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) }); - - // 重复任务检查 - var loaderName = Lang.Text("Minecraft.Download.Modpack.Task.MmcInstall", instanceName); - if (loaderTaskbar.Any(l => (l.name ?? "") == (loaderName ?? ""))) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.Installing"), HintType.Error); - throw new ModBase.CancelledException(); - } - - // 启动 - var loader = new LoaderCombo(loaderName, loaders) { OnStateChanged = ModDownloadLib.McInstallState }; - loader.Start(request.targetInstanceFolder); - LoaderTaskbarAdd(loader); - ModMain.frmMain.BtnExtraDownload.ShowRefresh(); - ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.TaskManager)); - return loader; + } while (false); } #endregion diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ZipModpackArchiveReader.cs b/Plain Craft Launcher 2/Modules/Minecraft/ZipModpackArchiveReader.cs new file mode 100644 index 0000000000..818a0f7f23 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ZipModpackArchiveReader.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using PCL.Core.Minecraft.Modpack; + +namespace PCL; + +/// +/// 基于 实现,供格式识别使用。 +/// +internal sealed class ZipModpackArchiveReader : IModpackArchiveReader +{ + private readonly ZipArchive _archive; + + public ZipModpackArchiveReader(ZipArchive archive) + { + _archive = archive; + } + + public IEnumerable EntryNames + { + get + { + foreach (var entry in _archive.Entries) + yield return entry.FullName; + } + } + + public bool EntryExists(string entryName) + { + return _archive.GetEntry(entryName) is not null; + } + + public string ReadEntryText(string entryName) + { + using var stream = _archive.GetEntry(entryName)?.Open(); + return stream is null ? "" : ModBase.ReadFile(stream); + } +} diff --git a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs index d954a11cc2..49cd489f15 100644 --- a/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs +++ b/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.cs @@ -4394,17 +4394,17 @@ private static void MergeJson(string outputFolder, string minecraftFolder, strin // 相关讨论见 #2801 if (mMCPackInfo is not null) { - if (mMCPackInfo.isMinecraftOverrided) + if (mMCPackInfo.IsMinecraftOverridden) { ModBase.Log("[Download] 当前实例的 MC 核心已被修改,使用对应的 MMC 整合包参数"); - outputJson = mMCPackInfo.overridedJson; + outputJson = mMCPackInfo.OverriddenJson; } else { ModBase.Log("[Download] 存在无修改 MC 核心文件的 MMC 整合包信息,应用相关参数"); outputJson = minecraftJson; // 合并来自 MultiMC 的 JSON - outputJson.Merge(mMCPackInfo.overridedJson); + outputJson.Merge(mMCPackInfo.OverriddenJson); } } else @@ -4421,7 +4421,7 @@ private static void MergeJson(string outputFolder, string minecraftFolder, strin } if (hasForge) - if (mMCPackInfo is null || !mMCPackInfo.isForgeOverrided) + if (mMCPackInfo is null || !mMCPackInfo.IsForgeOverridden) { // 合并 Forge forgeJson.Remove("releaseTime"); @@ -4430,7 +4430,7 @@ private static void MergeJson(string outputFolder, string minecraftFolder, strin } if (hasNeoForge) - if (mMCPackInfo is null || !mMCPackInfo.isNeoForgeOverrided) + if (mMCPackInfo is null || !mMCPackInfo.IsNeoForgeOverridden) { // 合并 NeoForge neoForgeJson.Remove("releaseTime"); @@ -4439,7 +4439,7 @@ private static void MergeJson(string outputFolder, string minecraftFolder, strin } if (hasCleanroom) - if (mMCPackInfo is null || !mMCPackInfo.isCleanroomOverrided) + if (mMCPackInfo is null || !mMCPackInfo.IsCleanroomOverridden) { // 合并 Cleanroom cleanroomJson.Remove("releaseTime"); @@ -4456,7 +4456,7 @@ private static void MergeJson(string outputFolder, string minecraftFolder, strin } if (hasFabric) - if (mMCPackInfo is null || !mMCPackInfo.isFabricOverrided) + if (mMCPackInfo is null || !mMCPackInfo.IsFabricOverridden) { // 合并 Fabric fabricJson.Remove("releaseTime"); @@ -4465,7 +4465,7 @@ private static void MergeJson(string outputFolder, string minecraftFolder, strin } if (hasLegacyFabric) - if (mMCPackInfo is null || !mMCPackInfo.isFabricOverrided) + if (mMCPackInfo is null || !mMCPackInfo.IsFabricOverridden) { // 合并 Fabric legacyFabricJson.Remove("releaseTime"); @@ -4550,7 +4550,7 @@ private static void MergeJson(string outputFolder, string minecraftFolder, strin // 修改 if (realArguments is not null && !string.IsNullOrEmpty(realArguments.Replace(" ", ""))) outputJson["minecraftArguments"] = realArguments; - if (mMCPackInfo is not null && mMCPackInfo.isMcArgsEdited) + if (mMCPackInfo is not null && mMCPackInfo.IsMcArgsEdited) outputJson.Remove("minecraftArguments"); outputJson.Remove("_comment_"); outputJson.Remove("inheritsFrom"); From b7986140e1fd4535fe009688786db2c33c2bfb60 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Thu, 6 Aug 2026 19:44:08 +0800 Subject: [PATCH 03/19] =?UTF-8?q?ref:=E6=95=B4=E5=90=88=E5=8C=85=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E9=87=8D=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Minecraft/Modpack/McbbsManifestTest.cs | 72 +++++++++ PCL.Core/Minecraft/Modpack/McbbsManifest.cs | 77 +++++++++ .../Minecraft/FolderModpackArchiveReader.cs | 38 +++++ .../Modules/Minecraft/ModModpack.Container.cs | 22 ++- .../Minecraft/ModModpack.CurseForge.cs | 14 +- .../Modules/Minecraft/ModModpack.Hmcl.cs | 11 +- .../Modules/Minecraft/ModModpack.Mcbbs.cs | 40 +++-- .../Modules/Minecraft/ModModpack.Modrinth.cs | 86 ++++++++-- .../Modules/Minecraft/ModModpack.MultiMc.cs | 24 ++- .../Modules/Minecraft/ModModpack.cs | 150 +++++++++++------- 10 files changed, 414 insertions(+), 120 deletions(-) create mode 100644 PCL.Core.Test/Minecraft/Modpack/McbbsManifestTest.cs create mode 100644 PCL.Core/Minecraft/Modpack/McbbsManifest.cs create mode 100644 Plain Craft Launcher 2/Modules/Minecraft/FolderModpackArchiveReader.cs diff --git a/PCL.Core.Test/Minecraft/Modpack/McbbsManifestTest.cs b/PCL.Core.Test/Minecraft/Modpack/McbbsManifestTest.cs new file mode 100644 index 0000000000..4427498519 --- /dev/null +++ b/PCL.Core.Test/Minecraft/Modpack/McbbsManifestTest.cs @@ -0,0 +1,72 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PCL.Core.Minecraft.Modpack; +using PCL.Core.Utils; + +namespace PCL.Core.Test.Minecraft.Modpack; + +[TestClass] +public class McbbsManifestTest +{ + [TestMethod] + public void ParsesCompleteManifest() + { + const string json = """ + { + "manifestType": "minecraftModpack", + "name": "示例整合包", + "version": "1.0.0", + "addons": [ + { "id": "game", "version": "1.20.1" }, + { "id": "forge", "version": "47.2.0" } + ], + "launchInfo": { + "javaArgument": ["-Xmx2G", "-XX:+UseG1GC"], + "launchArgument": ["--username", "Player"] + } + } + """; + var manifest = McbbsManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.AreEqual("示例整合包", manifest.Name); + Assert.AreEqual("1.0.0", manifest.Version); + Assert.IsNotNull(manifest.Addons); + Assert.AreEqual(2, manifest.Addons.Count); + Assert.AreEqual("game", manifest.Addons[0].Id); + Assert.AreEqual("1.20.1", manifest.Addons[0].Version); + Assert.AreEqual("forge", manifest.Addons[1].Id); + Assert.AreEqual("47.2.0", manifest.Addons[1].Version); + Assert.IsNotNull(manifest.LaunchInfo); + Assert.AreEqual("-Xmx2G", manifest.LaunchInfo.JavaArgument?[0]?.ToString()); + Assert.AreEqual("-XX:+UseG1GC", manifest.LaunchInfo.JavaArgument?[1]?.ToString()); + Assert.AreEqual("--username", manifest.LaunchInfo.LaunchArgument?[0]?.ToString()); + } + + [TestMethod] + public void MissingOptionalFieldsStayNull() + { + const string json = """{"name":"Test"}"""; + var manifest = McbbsManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.AreEqual("Test", manifest.Name); + Assert.IsNull(manifest.Addons); + Assert.IsNull(manifest.LaunchInfo); + Assert.IsNull(manifest.Version); + } + + [TestMethod] + public void SingleStringLaunchArgumentIsPreserved() + { + // 兼容个别包把 launchInfo 写成单个字符串而不是数组 + const string json = """{"launchInfo":{"javaArgument":"-Xmx2G"}}"""; + var manifest = McbbsManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.IsNotNull(manifest.LaunchInfo); + Assert.AreEqual("-Xmx2G", manifest.LaunchInfo.JavaArgument?.ToString()); + } + + [TestMethod] + public void ParseNullReturnsNull() + { + Assert.IsNull(McbbsManifest.Parse(null)); + } +} diff --git a/PCL.Core/Minecraft/Modpack/McbbsManifest.cs b/PCL.Core/Minecraft/Modpack/McbbsManifest.cs new file mode 100644 index 0000000000..402a048da1 --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/McbbsManifest.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using System.Text.Json.Nodes; +using PCL.Core.Utils; + +namespace PCL.Core.Minecraft.Modpack; + +/// +/// MCBBS 格式整合包的清单(mcbbs.packmeta)模型。 +/// MCBBS 格式可视为 CurseForge 格式的超集:在 manifest.json 的基础上补充了 +/// addons(游戏与加载器附加信息)与 launchInfo(启动参数)。 +/// 使用 的宽松反序列化读取;缺失的字段保持为 null/空,由安装流程负责校验。 +/// +public sealed class McbbsManifest +{ + /// + /// 整合包名称。 + /// + public string? Name { get; init; } + + /// + /// 整合包版本号。 + /// + public string? Version { get; init; } + + /// + /// 附加信息列表,键如 gameforgeneoforgefabricoptifine; + /// 缺失时表示清单未声明附加信息。 + /// + public List? Addons { get; init; } + + /// + /// 启动参数信息(可选)。 + /// + public McbbsLaunchInfo? LaunchInfo { get; init; } + + /// + /// 宽松解析 MCBBS 清单。 + /// + /// 清单 JSON 节点。 + /// 解析后的清单;传入 null 时返回 null。 + public static McbbsManifest? Parse(JsonNode? node) + { + return node.ToObject(); + } +} + +/// +/// MCBBS 清单中的附加信息项。 +/// +public sealed class McbbsAddon +{ + /// + /// 附加信息标识,如 gameforgefabric。 + /// + public string? Id { get; init; } + + /// + /// 附加信息版本号。 + /// + public string? Version { get; init; } +} + +/// +/// MCBBS 清单中的启动参数信息。 +/// +public sealed class McbbsLaunchInfo +{ + /// + /// JVM 参数。保留原始 JSON 节点,由安装流程按既有方式拼接(兼容数组与字符串两种写法)。 + /// + public JsonNode? JavaArgument { get; init; } + + /// + /// 游戏启动参数。保留原始 JSON 节点,由安装流程按既有方式拼接。 + /// + public JsonNode? LaunchArgument { get; init; } +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/FolderModpackArchiveReader.cs b/Plain Craft Launcher 2/Modules/Minecraft/FolderModpackArchiveReader.cs new file mode 100644 index 0000000000..b72435aa3e --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/FolderModpackArchiveReader.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.IO; +using PCL.Core.Minecraft.Modpack; + +namespace PCL; + +/// +/// 基于目录的 实现,用于直接安装已解压的文件夹形式整合包 +/// (例如嵌套整合包内层),避免重新打包为 zip。 +/// +internal sealed class FolderModpackArchiveReader : IModpackArchiveReader +{ + private readonly string _root; + + public FolderModpackArchiveReader(string rootDirectory) + { + _root = rootDirectory; + } + + public IEnumerable EntryNames + { + get + { + foreach (var file in Directory.GetFiles(_root, "*", SearchOption.AllDirectories)) + yield return Path.GetRelativePath(_root, file).Replace("\\", "/"); + } + } + + public bool EntryExists(string entryName) + { + return File.Exists(Path.Combine(_root, entryName)); + } + + public string ReadEntryText(string entryName) + { + return ModBase.ReadFile(Path.Combine(_root, entryName)); + } +} diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs index 2929a38d97..5f80e2343d 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -3,6 +3,7 @@ using System.IO.Compression; using System.Text.RegularExpressions; using PCL.Core.App.Localization; +using PCL.Core.Minecraft.Modpack; using PCL.Core.UI; using PCL.Core.Utils; using static PCL.ModLoader; @@ -13,8 +14,7 @@ public static partial class ModModpack { #region 带启动器的压缩包 - private static LoaderCombo _InstallLauncherPack(string fileAddress, ZipArchive archive, - string archiveBaseFolder) + private static LoaderCombo _InstallLauncherPack(string sourcePath, string archiveBaseFolder) { // 获取解压路径 ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.SelectEmptyFolder.Message"), @@ -33,7 +33,7 @@ private static LoaderCombo _InstallLauncherPack(string fileAddress, ZipA { new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => { - _ExtractModpackFiles(targetFolder, fileAddress, task, 0.9d); + _ExtractModpackFiles(targetFolder, sourcePath, task, 0.9d); Thread.Sleep(400); // 避免文件争用 // 查找解压后的 exe 文件 string launcher = null; @@ -93,11 +93,9 @@ private static LoaderCombo _InstallLauncherPack(string fileAddress, ZipA { if (Directory.Exists(innerPackPath)) { - // 文件夹形式的整合包:重新打包为临时 zip 后走统一安装流程 - var tempZip = Path.Combine(ModMain.RequestTaskTempFolder(), "modpack.zip"); - ModBase.Log("[Modpack] 内层整合包为文件夹,重新打包后安装:" + innerPackPath); - ZipFile.CreateFromDirectory(innerPackPath, tempZip); - ModpackInstall(tempZip); + // 文件夹形式的整合包:直接以文件夹为来源安装,无需重新打包 + ModBase.Log("[Modpack] 内层整合包为文件夹,直接安装:" + innerPackPath); + _InstallSource(new FolderModpackArchiveReader(innerPackPath), innerPackPath); } else { @@ -160,13 +158,13 @@ private static LoaderCombo _InstallLauncherPack(string fileAddress, ZipA #region 普通压缩包 - private static LoaderCombo _InstallCompress(string fileAddress, ZipArchive archive) + private static LoaderCombo _InstallCompress(string sourcePath, IModpackArchiveReader archive) { // 尝试定位 .minecraft 文件夹:寻找形如 “/versions/XXX/XXX.json” 的路径 Match match = null; - foreach (var entry in archive.Entries) + foreach (var entryName in archive.EntryNames) { - var entryMatch = RegexPatterns.ModpackLazyInstance.Match("/" + entry.FullName); + var entryMatch = RegexPatterns.ModpackLazyInstance.Match("/" + entryName); if (entryMatch.Success) { match = entryMatch; @@ -203,7 +201,7 @@ private static LoaderCombo _InstallCompress(string fileAddress, ZipArchi { new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => { - _ExtractModpackFiles(targetFolder, fileAddress, task, 0.95d); + _ExtractModpackFiles(targetFolder, sourcePath, task, 0.95d); // 加入文件夹列表 PageSelectLeft.AddFolder(Path.Combine(targetFolder, archiveBaseFolder), ModBase.GetFolderNameFromPath(targetFolder), false); diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs index 0d0129c4bc..c18dc86044 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs @@ -15,7 +15,7 @@ public static partial class ModModpack { #region CurseForge - private static LoaderCombo _InstallCurseForge(string fileAddress, ZipArchive archive, + private static LoaderCombo _InstallCurseForge(string sourcePath, IModpackArchiveReader archive, string archiveBaseFolder, string instanceName = null, string logo = null, string resourceId = null, bool isOnlineInstall = false) { @@ -23,8 +23,7 @@ private static LoaderCombo _InstallCurseForge(string fileAddress, ZipArc JsonObject json; try { - json = (JsonObject)ModBase.GetJson( - ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "manifest.json").Open())); + json = (JsonObject)ModBase.GetJson(archive.ReadEntryText(archiveBaseFolder + "manifest.json")); } catch (Exception ex) { @@ -87,18 +86,19 @@ private static LoaderCombo _InstallCurseForge(string fileAddress, ZipArc // 解压 var installTemp = ModMain.RequestTaskTempFolder(); var installLoaders = new List(); - var overrideHome = manifest.Overrides ?? ""; + // overrides 字段缺失时按规范默认为 "overrides",避免整包不解压导致覆写内容(mods/config/saves 等)丢失 + var overrideHome = manifest.Overrides ?? "overrides"; if (!string.IsNullOrEmpty(overrideHome)) installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), task => { - _ExtractModpackFiles(installTemp, fileAddress, task, 0.6d); + _ExtractModpackFiles(installTemp, sourcePath, task, 0.6d); _CopyOverrideDirectory( Path.Combine(installTemp, archiveBaseFolder, overrideHome == "." || overrideHome == "./" ? "" : overrideHome), _GetVersionFolder(instanceName), task, 0.4d); }) { - ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, + ProgressWeight = new FileInfo(sourcePath).Length / 1024d / 1024d / 6d, block = false }); // 每 6M 需要 1s // 获取 Mod 列表 @@ -253,7 +253,7 @@ private static LoaderCombo _InstallCurseForge(string fileAddress, ZipArc { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) }); loaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.FinalizeFiles"), task => { - _FinalizeInstance(_GetVersionFolder(instanceName), fileAddress, logo, "CurseForge", + _FinalizeInstance(_GetVersionFolder(instanceName), sourcePath, logo, "CurseForge", manifest.Version, resourceId); }) { diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs index cfe2d3f7c7..88e6901cdc 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json.Nodes; using PCL.Core.App.Localization; +using PCL.Core.Minecraft.Modpack; using PCL.Core.Utils; using static PCL.ModLoader; @@ -12,14 +13,14 @@ public static partial class ModModpack { #region HMCL - private static LoaderCombo _InstallHmcl(string fileAddress, ZipArchive archive, string archiveBaseFolder) + private static LoaderCombo _InstallHmcl(string sourcePath, IModpackArchiveReader archive, + string archiveBaseFolder) { // 读取 Json 文件 JsonObject json; try { - json = (JsonObject)ModBase.GetJson( - ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "modpack.json").Open(), Encoding.UTF8)); + json = (JsonObject)ModBase.GetJson(archive.ReadEntryText(archiveBaseFolder + "modpack.json")); } catch (Exception ex) { @@ -35,12 +36,12 @@ private static LoaderCombo _InstallHmcl(string fileAddress, ZipArchive a installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), task => { - _ExtractModpackFiles(installTemp, fileAddress, task, 0.6d); + _ExtractModpackFiles(installTemp, sourcePath, task, 0.6d); _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "minecraft"), Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4d); }) { - ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, + ProgressWeight = new FileInfo(sourcePath).Length / 1024d / 1024d / 6d, block = false }); // 每 6M 需要 1s // 构造游戏本体安装加载器 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs index 195e505001..dc2539a4c9 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs @@ -4,6 +4,7 @@ using System.Text.Json.Nodes; using PCL.Core.App; using PCL.Core.App.Localization; +using PCL.Core.Minecraft.Modpack; using PCL.Core.UI; using PCL.Core.Utils; using static PCL.ModLoader; @@ -14,28 +15,30 @@ public static partial class ModModpack { #region MCBBS - private static LoaderCombo _InstallMcbbs(string fileAddress, ZipArchive archive, + private static LoaderCombo _InstallMcbbs(string sourcePath, IModpackArchiveReader archive, string archiveBaseFolder, string instanceName = null) { // 读取 Json 文件 JsonObject json; try { - var entry = archive.GetEntry(archiveBaseFolder + "mcbbs.packmeta") ?? - archive.GetEntry(archiveBaseFolder + "manifest.json"); - using (var stream = entry.Open()) - { - json = (JsonObject)ModBase.GetJson(ModBase.ReadFile(stream, Encoding.UTF8)); - } + var entryName = archive.EntryExists(archiveBaseFolder + "mcbbs.packmeta") + ? archiveBaseFolder + "mcbbs.packmeta" + : archiveBaseFolder + "manifest.json"; + json = (JsonObject)ModBase.GetJson(archive.ReadEntryText(entryName)); } catch (Exception ex) { throw new Exception("MCBBS 整合包安装信息存在问题", ex); } + var manifest = McbbsManifest.Parse(json); + if (manifest is null) + throw new Exception("MCBBS 整合包安装信息存在问题"); + // 获取实例名 if (instanceName is null) - instanceName = _PromptInstanceName(json["name"]?.ToString() ?? ""); + instanceName = _PromptInstanceName(manifest.Name ?? ""); // 解压与路径准备 var installTemp = ModMain.RequestTaskTempFolder(); @@ -46,34 +49,37 @@ private static LoaderCombo _InstallMcbbs(string fileAddress, ZipArchive var unzipTask = new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), task => { - _ExtractModpackFiles(installTemp, fileAddress, task, 0.6); + _ExtractModpackFiles(installTemp, sourcePath, task, 0.6); _CopyOverrideDirectory( Path.Combine(installTemp, archiveBaseFolder, "overrides"), Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4); // JVM 参数处理 - if (json["launchInfo"] is not null) + if (manifest.LaunchInfo is not null) { - var launchInfo = (JsonObject)json["launchInfo"]; - Config.Instance.JvmArgs[versionFolder] = string.Join(" ", launchInfo["javaArgument"]); - Config.Instance.GameArgs[versionFolder] = string.Join(" ", launchInfo["launchArgument"]); + Config.Instance.JvmArgs[versionFolder] = + string.Join(" ", manifest.LaunchInfo.JavaArgument ?? new JsonArray()); + Config.Instance.GameArgs[versionFolder] = + string.Join(" ", manifest.LaunchInfo.LaunchArgument ?? new JsonArray()); } // 整合包版本 - if (json["version"] is not null) States.Instance.ModpackVersion[versionFolder] = json["version"].ToString(); + if (manifest.Version is not null) States.Instance.ModpackVersion[versionFolder] = manifest.Version; }); - unzipTask.ProgressWeight = new FileInfo(fileAddress).Length / 1024.0 / 1024.0 / 6.0; // 每 6M 需要 1s + unzipTask.ProgressWeight = new FileInfo(sourcePath).Length / 1024.0 / 1024.0 / 6.0; // 每 6M 需要 1s unzipTask.block = false; installLoaders.Add(unzipTask); // 构造加载器 - if (json["addons"] is null) + if (manifest.Addons is null) throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.McbbsAddons")); var addons = new Dictionary(); - foreach (var entryNode in json["addons"].AsArray()) { var entry = entryNode.AsObject(); addons.Add(entry["id"].ToString(), entry["version"].ToString()); } + foreach (var addon in manifest.Addons) + if (addon.Id is not null) + addons.Add(addon.Id, addon.Version ?? ""); if (!addons.ContainsKey("game")) { diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs index 0c57c41aad..645bbdad4a 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs @@ -1,7 +1,9 @@ using System.IO; using System.IO.Compression; +using System.Text; using System.Text.Json.Nodes; using PCL.Core.App.Localization; +using PCL.Core.Minecraft; using PCL.Core.Minecraft.Modpack; using PCL.Core.UI; using PCL.Core.Utils; @@ -15,7 +17,7 @@ public static partial class ModModpack { #region Modrinth - private static LoaderCombo _InstallModrinth(string fileAddress, ZipArchive archive, + private static LoaderCombo _InstallModrinth(string sourcePath, IModpackArchiveReader archive, string archiveBaseFolder, string instanceName = null, string logo = null, string resourceId = null, bool isOnlineInstall = false) { @@ -23,8 +25,7 @@ private static LoaderCombo _InstallModrinth(string fileAddress, ZipArchi JsonObject json; try { - json = (JsonObject)ModBase.GetJson( - ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "modrinth.index.json").Open())); + json = (JsonObject)ModBase.GetJson(archive.ReadEntryText(archiveBaseFolder + "modrinth.index.json")); } catch (Exception ex) { @@ -93,18 +94,20 @@ private static LoaderCombo _InstallModrinth(string fileAddress, ZipArchi installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), task => { - _ExtractModpackFiles(installTemp, fileAddress, task, 0.5d); + _ExtractModpackFiles(installTemp, sourcePath, task, 0.5d); _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "overrides"), Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4d); _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "client-overrides"), Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.1d); }) { - ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, + ProgressWeight = new FileInfo(sourcePath).Length / 1024d / 1024d / 6d, block = false }); // 每 6M 需要 1s // 获取下载文件列表 var fileList = new List(); + // 目录形式的下载项(地图存档等):先下载到临时目录,再解压到目标文件夹 + var archiveExtracts = new List<(string archivePath, string targetDirectory)>(); foreach (var file in manifest.Files ?? []) { // 检查是否需要该文件 @@ -151,16 +154,37 @@ private static LoaderCombo _InstallModrinth(string fileAddress, ZipArchi throw new ModBase.CancelledException(); } - fileList.Add(new DownloadFile( - ModComp.CompFile.HandleModrinthDownloadUrls(urls, ModComp.DownloadReason.ModPack, minecraftVersion, - modLoader), targetPath, - new ModBase.FileChecker(actualSize: file.FileSize, hash: file.Hashes?.Sha1), true)); + var fileChecker = new ModBase.FileChecker(actualSize: file.FileSize, hash: file.Hashes?.Sha1); + if (_IsDirectoryDownload(file)) + { + // 目录形式(文件名无扩展名,通常是地图存档 zip):先下载到临时目录,安装阶段再解压 + var archivePath = Path.Combine(installTemp, "download_" + archiveExtracts.Count + ".tmp"); + fileList.Add(new DownloadFile( + ModComp.CompFile.HandleModrinthDownloadUrls(urls, ModComp.DownloadReason.ModPack, minecraftVersion, + modLoader), archivePath, fileChecker, true)); + archiveExtracts.Add((archivePath, targetPath)); + } + else + { + fileList.Add(new DownloadFile( + ModComp.CompFile.HandleModrinthDownloadUrls(urls, ModComp.DownloadReason.ModPack, minecraftVersion, + modLoader), targetPath, fileChecker, true)); + } } if (fileList.Any()) installLoaders.Add( new LoaderDownload(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadAdditions"), fileList) { ProgressWeight = fileList.Count * 1.5d }); // 每个 Mod 需要 1.5s + // 解压目录形式的下载项(地图存档等) + if (archiveExtracts.Any()) + installLoaders.Add(new LoaderTask( + Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), task => + { + foreach (var (archivePath, targetDirectory) in archiveExtracts) + _ExtractArchiveToDirectory(archivePath, targetDirectory); + }) + { show = false, ProgressWeight = archiveExtracts.Count * 0.5d }); // 构造加载器 var request = new ModDownloadLib.McInstallRequest @@ -182,7 +206,7 @@ private static LoaderCombo _InstallModrinth(string fileAddress, ZipArchi { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) }); loaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.FinalizeFiles"), task => { - _FinalizeInstance(_GetVersionFolder(instanceName), fileAddress, logo, "Modrinth", + _FinalizeInstance(_GetVersionFolder(instanceName), sourcePath, logo, "Modrinth", manifest.VersionId, resourceId); }) { @@ -195,5 +219,47 @@ private static LoaderCombo _InstallModrinth(string fileAddress, ZipArchi return _StartInstall(loaderName, loaders, request.targetInstanceFolder, isOnlineInstall); } + /// + /// 判断 Modrinth 文件项的目标是否是一个文件夹(文件名无扩展名)。 + /// 若是,下载内容应为需解压的压缩包(通常用于地图存档等文件夹形式的内容)。 + /// + private static bool _IsDirectoryDownload(ModrinthFile file) + { + var path = file.Path ?? ""; + var trimmed = path.TrimEnd('/', '\\'); + return !string.IsNullOrEmpty(trimmed) && !Path.GetFileName(trimmed).Contains("."); + } + + /// + /// 将下载的压缩包解压到目标文件夹(用于地图存档等目录形式的下载项)。 + /// 解压到临时暂存目录后,若存在单层嵌套的存档根目录(zip 内再包一层文件夹)则直接使用该目录; + /// 内容不是压缩包时回退为按普通文件复制。 + /// + private static void _ExtractArchiveToDirectory(string archivePath, string targetDirectory) + { + try + { + var staging = Path.Combine(Path.GetDirectoryName(archivePath) ?? "", "_extract_" + Path.GetFileName(archivePath)); + ModBase.DeleteDirectory(staging); + Directory.CreateDirectory(staging); + ModBase.ExtractFile(archivePath, staging, Encoding.UTF8, null); + var saveRoot = SaveImportHelper.GetSaveRootDirectory(staging) ?? staging; + ModBase.CopyDirectory(saveRoot, targetDirectory, null); + ModBase.DeleteDirectory(staging); + } + catch (Exception ex) + { + // 内容不是可解压的压缩包,改为按普通文件复制到目标路径 + ModBase.Log(ex, "整合包下载项按压缩包解压失败,改为直接复制文件:" + archivePath); + Directory.CreateDirectory(Path.GetDirectoryName(targetDirectory) ?? targetDirectory); + ModBase.CopyFile(archivePath, targetDirectory); + } + finally + { + if (File.Exists(archivePath)) + File.Delete(archivePath); + } + } + #endregion } diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs index dc1da5bf54..6ed94c479e 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs @@ -57,7 +57,7 @@ public class MMCPackInfo public JsonObject OverriddenJson = new(); } - private static LoaderCombo _InstallMultiMc(string fileAddress, ZipArchive archive, + private static LoaderCombo _InstallMultiMc(string sourcePath, IModpackArchiveReader archive, string archiveBaseFolder) { // 读取 Json 文件 @@ -65,9 +65,8 @@ private static LoaderCombo _InstallMultiMc(string fileAddress, ZipArchiv string packInstance; try { - packJson = (JsonObject)ModBase.GetJson( - ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "mmc-pack.json").Open(), Encoding.UTF8)); - packInstance = ModBase.ReadFile(archive.GetEntry(archiveBaseFolder + "instance.cfg").Open(), Encoding.UTF8); + packJson = (JsonObject)ModBase.GetJson(archive.ReadEntryText(archiveBaseFolder + "mmc-pack.json")); + packInstance = archive.ReadEntryText(archiveBaseFolder + "instance.cfg"); } catch (Exception ex) { @@ -87,7 +86,7 @@ private static LoaderCombo _InstallMultiMc(string fileAddress, ZipArchiv installLoaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), task => { - _ExtractModpackFiles(installTemp, fileAddress, task, 0.55d); + _ExtractModpackFiles(installTemp, sourcePath, task, 0.55d); _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "libraries"), Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName, "libraries"), task, 0.2d); _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, ".minecraft"), @@ -97,7 +96,7 @@ private static LoaderCombo _InstallMultiMc(string fileAddress, ZipArchiv installTemp, archiveBaseFolder, instanceName); }) { - ProgressWeight = new FileInfo(fileAddress).Length / 1024d / 1024d / 6d, + ProgressWeight = new FileInfo(sourcePath).Length / 1024d / 1024d / 6d, block = false }); // 每 6M 需要 1s // 构造实例安装请求 @@ -171,7 +170,7 @@ private static LoaderCombo _InstallMultiMc(string fileAddress, ZipArchiv /// 解析 patches/ 目录下的 JSON 补丁,构建 覆盖信息。 /// 参考 https://github.com/MultiMC/Launcher/wiki/JSON-Patches /// - private static MMCPackInfo? _BuildMultiMcPackInfo(ZipArchive archive, string archiveBaseFolder, + private static MMCPackInfo? _BuildMultiMcPackInfo(IModpackArchiveReader archive, string archiveBaseFolder, JsonObject packJson) { MMCPackInfo packInfo = null; @@ -179,17 +178,16 @@ private static LoaderCombo _InstallMultiMc(string fileAddress, ZipArchiv { // 部分压缩工具不会写入 "patches/" 目录条目,因此不能只检查目录项, // 而应检查是否存在任何位于 patches/ 下的文件条目。 - if (!archive.Entries.Any(e => !e.FullName.EndsWith("/") && - e.FullName.StartsWith(archiveBaseFolder + "patches/"))) + if (!archive.EntryNames.Any(e => !e.EndsWith("/") && + e.StartsWith(archiveBaseFolder + "patches/"))) return null; ModBase.Log("[ModPack] 安装的 MultiMC 整合包存在 JSON Patches"); // 排序预处理 var patches = new List>(); - foreach (var entry in archive.Entries) - if (!entry.FullName.EndsWith("/") && entry.FullName.StartsWith(archiveBaseFolder + "patches/")) + foreach (var entryName in archive.EntryNames) + if (!entryName.EndsWith("/") && entryName.StartsWith(archiveBaseFolder + "patches/")) { - var patch = (JsonObject)ModBase.GetJson(ModBase.ReadFile( - archive.GetEntry(entry.FullName).Open(), Encoding.UTF8)); + var patch = (JsonObject)ModBase.GetJson(archive.ReadEntryText(entryName)); patches.Add(new KeyValuePair(patch, (int)(patch["order"] is not null ? patch["order"] : 0))); } diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs index 031ef12092..9d51f1c901 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs @@ -56,61 +56,20 @@ public static LoaderCombo ModpackInstall(string file, string instanceNam ZipArchive archive = null; try { - // 字符校验 - var targetFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\"; - if (targetFolder.Contains("!") || targetFolder.Contains(";")) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.InvalidGamePathChars", targetFolder), - HintType.Error); - throw new ModBase.CancelledException(); - } - - // 获取整合包种类与关键 Json - ModpackFormat packType; - string archiveBaseFolder; - try - { - archive = new ZipArchive(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)); - if (archive.Entries.Any(e => e.IsEncrypted)) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.EncryptedArchiveUnsupported")); - var detection = ModpackArchiveDetector.Detect(new ZipModpackArchiveReader(archive)); - packType = detection.Format; - archiveBaseFolder = detection.ArchiveBaseFolder; - ModBase.Log($"[ModPack] 整合包格式识别结果:{packType}(基础目录:{archiveBaseFolder})"); - } - catch (Exception ex) - { - if (ex.Message.Contains("Error.WinIOError")) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.OpenFailed"), ex); - else if (file.EndsWithF(".rar", true)) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.RarUnsupported"), ex); - else - throw new Exception(Lang.Text("Minecraft.Download.Modpack.UnsupportedArchive"), ex); - } + archive = new ZipArchive(new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read)); + if (archive.Entries.Any(e => e.IsEncrypted)) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.EncryptedArchiveUnsupported")); + } + catch (Exception ex) + { + // 打开压缩包失败(非压缩包、文件损坏等) + throw _WrapSourceOpenError(ex, file); + } - // 执行对应的安装方法 - switch (packType) - { - case ModpackFormat.CurseForge: - return _InstallCurseForge(file, archive, archiveBaseFolder, instanceName, logo, resourceId, - isOnlineInstall); - case ModpackFormat.Modrinth: - return _InstallModrinth(file, archive, archiveBaseFolder, instanceName, logo, resourceId, - isOnlineInstall); - case ModpackFormat.MultiMc: - return _InstallMultiMc(file, archive, archiveBaseFolder); - case ModpackFormat.Mcbbs: - return _InstallMcbbs(file, archive, archiveBaseFolder, instanceName); - case ModpackFormat.Hmcl: - return _InstallHmcl(file, archive, archiveBaseFolder); - case ModpackFormat.LauncherPack: - return _InstallLauncherPack(file, archive, archiveBaseFolder); - case ModpackFormat.LazyPack: - return _InstallCompress(file, archive); - default: - ModBase.Log("[ModPack] 整合包种类:未能识别,假定为压缩包"); - return _InstallCompress(file, archive); - } + try + { + return _InstallSource(new ZipModpackArchiveReader(archive), file, instanceName, logo, resourceId, + isOnlineInstall); } finally { @@ -119,14 +78,93 @@ public static LoaderCombo ModpackInstall(string file, string instanceNam } } + /// + /// 从统一的整合包来源(zip 文件或文件夹)安装整合包,并返回顶层加载器。 + /// 内部完成格式检测与分派。 + /// + private static LoaderCombo _InstallSource(IModpackArchiveReader source, string sourcePath, + string instanceName = null, string logo = null, string resourceId = null, bool isOnlineInstall = false) + { + // 字符校验 + var targetFolder = $@"{ModFolder.mcFolderSelected}versions\{instanceName}\"; + if (targetFolder.Contains("!") || targetFolder.Contains(";")) + { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.InvalidGamePathChars", targetFolder), + HintType.Error); + throw new ModBase.CancelledException(); + } + + // 获取整合包种类与关键 Json + ModpackFormat packType; + string archiveBaseFolder; + try + { + var detection = ModpackArchiveDetector.Detect(source); + packType = detection.Format; + archiveBaseFolder = detection.ArchiveBaseFolder; + ModBase.Log($"[ModPack] 整合包格式识别结果:{packType}(基础目录:{archiveBaseFolder})"); + } + catch (Exception ex) + { + // 格式检测阶段失败 + throw _WrapSourceOpenError(ex, sourcePath); + } + + // 执行对应的安装方法 + switch (packType) + { + case ModpackFormat.CurseForge: + return _InstallCurseForge(sourcePath, source, archiveBaseFolder, instanceName, logo, resourceId, + isOnlineInstall); + case ModpackFormat.Modrinth: + return _InstallModrinth(sourcePath, source, archiveBaseFolder, instanceName, logo, resourceId, + isOnlineInstall); + case ModpackFormat.MultiMc: + return _InstallMultiMc(sourcePath, source, archiveBaseFolder); + case ModpackFormat.Mcbbs: + return _InstallMcbbs(sourcePath, source, archiveBaseFolder, instanceName); + case ModpackFormat.Hmcl: + return _InstallHmcl(sourcePath, source, archiveBaseFolder); + case ModpackFormat.LauncherPack: + return _InstallLauncherPack(sourcePath, archiveBaseFolder); + case ModpackFormat.LazyPack: + return _InstallCompress(sourcePath, source); + default: + ModBase.Log("[ModPack] 整合包种类:未能识别,假定为压缩包"); + return _InstallCompress(sourcePath, source); + } + } + + /// + /// 将压缩包打开/格式检测阶段的异常包装为“无法处理该压缩包”类的用户可见错误。 + /// + private static Exception _WrapSourceOpenError(Exception ex, string sourcePath) + { + if (ex.Message.Contains("Error.WinIOError")) + return new Exception(Lang.Text("Minecraft.Download.Modpack.OpenFailed"), ex); + if (sourcePath.EndsWithF(".rar", true)) + return new Exception(Lang.Text("Minecraft.Download.Modpack.RarUnsupported"), ex); + return new Exception(Lang.Text("Minecraft.Download.Modpack.UnsupportedArchive"), ex); + } + #region 共享流程 /// /// 解压整合包文件到临时目录,失败时切换编码并重试。 /// - private static void _ExtractModpackFiles(string installTemp, string fileAddress, LoaderBase loader, + private static void _ExtractModpackFiles(string installTemp, string sourcePath, LoaderBase loader, double progressIncrement) { + // 文件夹来源:直接复制,无需编码切换重试 + if (Directory.Exists(sourcePath)) + { + var folderInitialProgress = loader.Progress; + loader.Progress = folderInitialProgress; + ModBase.CopyDirectory(sourcePath, installTemp, delta => loader.Progress += delta * progressIncrement); + loader.Progress = folderInitialProgress + progressIncrement; + return; + } + // 解压文件 var retryCount = 1; var encode = Encoding.GetEncoding("GB18030"); @@ -141,7 +179,7 @@ private static void _ExtractModpackFiles(string installTemp, string fileAddress, ModBase.DeleteDirectory(installTemp); // 解压文件,ProgressIncrementHandler 通过 Lambda 更新进度 - ModBase.ExtractFile(fileAddress, installTemp, encode, + ModBase.ExtractFile(sourcePath, installTemp, encode, delta => loader.Progress += delta * progressIncrement); // 解压成功,更新进度并退出循环 From ece27862ad6fb0e3b5e24cfe7d140d4c9071afa1 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Thu, 6 Aug 2026 20:58:24 +0800 Subject: [PATCH 04/19] =?UTF-8?q?ref:=E6=95=B4=E5=90=88=E5=8C=85=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E9=87=8D=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Minecraft/ModModpack.Container.cs | 156 +++++++++++------- .../Modules/Minecraft/ModModpack.cs | 2 +- 2 files changed, 97 insertions(+), 61 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs index 5f80e2343d..1dafdb3bcf 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -1,6 +1,6 @@ using System.Diagnostics; using System.IO; -using System.IO.Compression; +using System.Text.Json.Nodes; using System.Text.RegularExpressions; using PCL.Core.App.Localization; using PCL.Core.Minecraft.Modpack; @@ -14,30 +14,29 @@ public static partial class ModModpack { #region 带启动器的压缩包 - private static LoaderCombo _InstallLauncherPack(string sourcePath, string archiveBaseFolder) + /// + /// 懒人包 .minecraft 根目录下属于共享/系统目录、应由启动器在运行时自动补全的内容, + /// 安装时不需要复制进实例文件夹。 + /// + private static readonly HashSet _LazyPackSharedFolders = new(StringComparer.OrdinalIgnoreCase) { - // 获取解压路径 - ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.SelectEmptyFolder.Message"), - Lang.Text("Common.Action.Install"), Lang.Text("Common.Action.Continue"), forceWait: true); - var targetFolder = SystemDialogs.SelectFolder(Lang.Text("Minecraft.Download.Modpack.SelectTargetFolder.Title")); - if (string.IsNullOrEmpty(targetFolder)) - throw new ModBase.CancelledException(); - if (Directory.GetFileSystemEntries(targetFolder).Length > 0) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.TargetFolderMustBeEmpty"), HintType.Error); - throw new ModBase.CancelledException(); - } + "versions", "libraries", "assets", "crash-reports", "logs", + "launcher_profiles.json", "usercache.json", "usernamecache.json" + }; - // 解压 + private static LoaderCombo _InstallLauncherPack(string sourcePath) + { + // 解压到临时目录(无需用户选择空文件夹),内层整合包安装完成后由系统自动清理 + var installTemp = ModMain.RequestTaskTempFolder(); var loader = new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[] { new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => { - _ExtractModpackFiles(targetFolder, sourcePath, task, 0.9d); + _ExtractModpackFiles(installTemp, sourcePath, task, 0.9d); Thread.Sleep(400); // 避免文件争用 // 查找解压后的 exe 文件 string launcher = null; - foreach (var exeFile in Directory.GetFiles(targetFolder, "*.exe", SearchOption.TopDirectoryOnly)) + foreach (var exeFile in Directory.GetFiles(installTemp, "*.exe", SearchOption.TopDirectoryOnly)) { var info = FileVersionInfo.GetVersionInfo(exeFile); ModBase.Log($"[Modpack] 文件 {exeFile} 的产品名标识为 {info.ProductName}"); @@ -68,7 +67,8 @@ private static LoaderCombo _InstallLauncherPack(string sourcePath, strin Lang.Text("Minecraft.Download.Modpack.BundledLauncher.DoNotUse") ) == 1) { - ModBase.OpenExplorer(targetFolder); + // 换用附带启动器:保留临时目录供其使用 + ModBase.OpenExplorer(installTemp); ModBase.ShellOnly(launcher, "--wait"); // 要求等待已有的 PCL 退出 ModBase.Log("[Modpack] 为换用整合包中的启动器启动,强制结束程序"); ModMain.frmMain.EndProgram(false); @@ -80,36 +80,24 @@ private static LoaderCombo _InstallLauncherPack(string sourcePath, strin ModBase.Log("[Modpack] 未找到压缩包中附带的启动器"); } - ModBase.OpenExplorer(targetFolder); - // 加入文件夹列表 - var instanceName = ModBase.GetFolderNameFromPath(targetFolder); - Directory.CreateDirectory(Path.Combine(targetFolder, ".minecraft")); - PageSelectLeft.AddFolder( - Path.Combine(targetFolder, ".minecraft", archiveBaseFolder.Replace("/", @"\").TrimStart('\\')), instanceName, - false); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串) // 寻找并安装内层整合包(任意格式:modpack.* 压缩包文件或文件夹形式的整合包) - var innerPackPath = _FindInnerModpack(targetFolder); + var innerPackPath = _FindInnerModpack(installTemp); if (innerPackPath is not null) { + ModBase.Log("[Modpack] 调用内层整合包继续安装:" + innerPackPath); if (Directory.Exists(innerPackPath)) - { - // 文件夹形式的整合包:直接以文件夹为来源安装,无需重新打包 - ModBase.Log("[Modpack] 内层整合包为文件夹,直接安装:" + innerPackPath); _InstallSource(new FolderModpackArchiveReader(innerPackPath), innerPackPath); - } else - { - ModBase.Log("[Modpack] 调用内层整合包文件继续安装:" + innerPackPath); ModpackInstall(innerPackPath); - } } else { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.UnknownArchiveStructure"), HintType.Error); ModBase.Log("[Modpack] 未在压缩包中找到内层整合包"); } }) }); - loader.Start(targetFolder); + loader.Start(installTemp); LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); @@ -156,7 +144,7 @@ private static LoaderCombo _InstallLauncherPack(string sourcePath, strin #endregion - #region 普通压缩包 + #region 懒人包(完整实例压缩包) private static LoaderCombo _InstallCompress(string sourcePath, IModpackArchiveReader archive) { @@ -175,49 +163,97 @@ private static LoaderCombo _InstallCompress(string sourcePath, IModpackA if (match is null) throw new Exception(Lang.Text("Minecraft.Download.Modpack.UnknownArchiveStructure")); // 没有匹配 var archiveBaseFolder = match.Value.Replace("/", @"\").TrimStart('\\'); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串) - var instanceName = match.Groups[1].Value; - ModBase.Log("[ModPack] 检测到压缩包的 .minecraft 根目录:" + archiveBaseFolder + ",命中的实例名:" + instanceName); - // 获取解压路径 - ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.SelectEmptyFolder.Message"), - Lang.Text("Common.Action.Install"), Lang.Text("Common.Action.Continue"), forceWait: true); - var targetFolder = SystemDialogs.SelectFolder(Lang.Text("Minecraft.Download.Modpack.SelectTargetFolder.Title")); - if (string.IsNullOrEmpty(targetFolder)) - throw new ModBase.CancelledException(); - if (targetFolder.Contains("!") || targetFolder.Contains(";")) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.InvalidGamePathChars", targetFolder), - HintType.Error); - throw new ModBase.CancelledException(); - } + var packVersionName = match.Groups[1].Value; + ModBase.Log("[ModPack] 检测到懒人包的 .minecraft 根目录:" + archiveBaseFolder + ",命中的实例名:" + packVersionName); - if (Directory.GetFileSystemEntries(targetFolder).Length > 0) - { - HintService.Hint(Lang.Text("Minecraft.Download.Modpack.TargetFolderMustBeEmpty"), HintType.Error); - throw new ModBase.CancelledException(); - } + // 实例名:与现有实例不冲突时直接用懒人包自己的,冲突时让用户改名 + var instanceName = _PromptInstanceName(packVersionName); - // 解压 + // 解压到临时目录,复制实例内容到版本文件夹后删除临时目录 + var installTemp = ModMain.RequestTaskTempFolder(); var loader = new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[] { new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => { - _ExtractModpackFiles(targetFolder, sourcePath, task, 0.95d); - // 加入文件夹列表 - PageSelectLeft.AddFolder(Path.Combine(targetFolder, archiveBaseFolder), ModBase.GetFolderNameFromPath(targetFolder), - false); - Thread.Sleep(400); // 避免文件争用 + _ExtractModpackFiles(installTemp, sourcePath, task, 0.6d); + task.Progress = 0.6d; + _InstallLazyPackInstance(installTemp, archiveBaseFolder, packVersionName, instanceName); + task.Progress = 0.95d; + // 用完即删 + ModBase.DeleteDirectory(installTemp); ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.InstanceSelect)); }) + { + ProgressWeight = new FileInfo(sourcePath).Length / 1024d / 1024d / 6d + } // 每 6M 需要 1s }) { OnStateChanged = ModDownloadLib.McInstallState }; - loader.Start(targetFolder); + loader.Start(_GetVersionFolder(instanceName)); LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); return loader; } + /// + /// 将懒人包解压后的 .minecraft 实例内容安装到当前游戏文件夹的 versions\{实例名} 下。 + /// 仅复制实例自身内容(版本配置、mods/config/saves 等),共享目录(libraries/assets 等)由启动器在运行时自动补全。 + /// + private static void _InstallLazyPackInstance(string installTemp, string archiveBaseFolder, string packVersionName, + string instanceName) + { + var mcRoot = Path.Combine(installTemp, archiveBaseFolder.TrimEnd('\\')); + var instanceFolder = _GetVersionFolder(instanceName); + Directory.CreateDirectory(instanceFolder); + + // 复制版本文件夹全部内容(json/jar 改名并修正 id,其余文件如 mods/config 一并复制,兼容隔离与非隔离两种布局) + var versionDir = Path.Combine(mcRoot, "versions", packVersionName); + if (Directory.Exists(versionDir)) + foreach (var item in Directory.GetFileSystemEntries(versionDir)) + { + var name = Path.GetFileName(item); + if (name.Equals(packVersionName + ".json", StringComparison.OrdinalIgnoreCase)) + { + var versionJson = (JsonObject)ModBase.GetJson(ModBase.ReadFile(item)); + if (!string.Equals(instanceName, packVersionName, StringComparison.OrdinalIgnoreCase)) + versionJson["id"] = instanceName; + ModBase.WriteFile(Path.Combine(instanceFolder, instanceName + ".json"), versionJson.ToString()); + } + else if (name.Equals(packVersionName + ".jar", StringComparison.OrdinalIgnoreCase)) + { + ModBase.CopyFile(item, Path.Combine(instanceFolder, instanceName + ".jar")); + } + else if (Directory.Exists(item)) + { + ModBase.CopyDirectory(item, Path.Combine(instanceFolder, name), null); + } + else + { + ModBase.CopyFile(item, Path.Combine(instanceFolder, name)); + } + } + + // 复制 .minecraft 根目录下的实例内容(mods/config/saves/options.txt 等),跳过共享目录 + foreach (var item in Directory.GetFileSystemEntries(mcRoot)) + { + var name = Path.GetFileName(item); + if (_LazyPackSharedFolders.Contains(name)) + continue; + var dest = Path.Combine(instanceFolder, name); + if (Directory.Exists(item)) + ModBase.CopyDirectory(item, dest, null); + else + ModBase.CopyFile(item, dest); + } + + // 开启版本隔离 + var versionIni = instanceFolder + @"PCL\Setup.ini"; + ModBase.WriteIni(versionIni, "VersionArgumentIndie", 1.ToString()); + ModBase.WriteIni(versionIni, "VersionArgumentIndieV2", true.ToString()); + ModBase.IniClearCache(versionIni); + } + #endregion } diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs index 9d51f1c901..93e06d0a43 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs @@ -126,7 +126,7 @@ private static LoaderCombo _InstallSource(IModpackArchiveReader source, case ModpackFormat.Hmcl: return _InstallHmcl(sourcePath, source, archiveBaseFolder); case ModpackFormat.LauncherPack: - return _InstallLauncherPack(sourcePath, archiveBaseFolder); + return _InstallLauncherPack(sourcePath); case ModpackFormat.LazyPack: return _InstallCompress(sourcePath, source); default: From e35def1eec1e99c97d83368a6accd34c9e212635 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Fri, 7 Aug 2026 14:03:46 +0800 Subject: [PATCH 05/19] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=BA=86=E9=83=A8?= =?UTF-8?q?=E5=88=86MMC=E6=95=B4=E5=90=88=E5=8C=85=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=90=8Ejava=E7=89=88=E6=9C=AC=E8=8E=B7=E5=8F=96=E4=B8=8D?= =?UTF-8?q?=E5=AF=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Plain Craft Launcher 2/Modules/Base/ModBase.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Plain Craft Launcher 2/Modules/Base/ModBase.cs b/Plain Craft Launcher 2/Modules/Base/ModBase.cs index 8cd8df0cf1..968f16775b 100644 --- a/Plain Craft Launcher 2/Modules/Base/ModBase.cs +++ b/Plain Craft Launcher 2/Modules/Base/ModBase.cs @@ -1857,6 +1857,10 @@ public static double Val(object str) { try { + // JsonNode 数值(如版本 json 中的 majorVersion)无法被 Conversion.Val 转换,需单独处理, + // 否则会捕获异常返回 0,导致 JSON 中声明的 Java 版本要求被忽略(见 MMC 整合包 Java 选择 bug)。 + if (str is JsonNode node) + return node.ToObject(); return str is "&" ? 0d : Conversion.Val(str); } catch From 634054e6d5af48a5562d90e4840ab993c94ad744 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Fri, 7 Aug 2026 15:10:07 +0800 Subject: [PATCH 06/19] =?UTF-8?q?imp:=E5=B8=A6=E5=90=AF=E5=8A=A8=E5=99=A8?= =?UTF-8?q?=E7=9A=84=E6=95=B4=E5=90=88=E5=8C=85=E5=AE=89=E8=A3=85=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Minecraft/Modpack/FakeModpackArchive.cs | 9 ++ .../Modpack/IModpackArchiveReader.cs | 7 + .../Minecraft/FolderModpackArchiveReader.cs | 6 + .../Modules/Minecraft/ModModpack.Container.cs | 133 ++++++++++++------ .../Modules/Minecraft/ModModpack.cs | 2 +- .../Minecraft/ZipModpackArchiveReader.cs | 10 ++ 6 files changed, 120 insertions(+), 47 deletions(-) diff --git a/PCL.Core.Test/Minecraft/Modpack/FakeModpackArchive.cs b/PCL.Core.Test/Minecraft/Modpack/FakeModpackArchive.cs index ead66136a5..021c630639 100644 --- a/PCL.Core.Test/Minecraft/Modpack/FakeModpackArchive.cs +++ b/PCL.Core.Test/Minecraft/Modpack/FakeModpackArchive.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using PCL.Core.Minecraft.Modpack; @@ -32,4 +33,12 @@ public string ReadEntryText(string entryName) { return _entries[entryName]; } + + public void ExtractEntryToFile(string entryName, string destinationPath) + { + if (!_entries.TryGetValue(entryName, out var content)) + return; + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath) ?? ""); + File.WriteAllText(destinationPath, content); + } } diff --git a/PCL.Core/Minecraft/Modpack/IModpackArchiveReader.cs b/PCL.Core/Minecraft/Modpack/IModpackArchiveReader.cs index 2f53ad27b5..6563a5a11c 100644 --- a/PCL.Core/Minecraft/Modpack/IModpackArchiveReader.cs +++ b/PCL.Core/Minecraft/Modpack/IModpackArchiveReader.cs @@ -24,4 +24,11 @@ public interface IModpackArchiveReader /// /// 条目名。 string ReadEntryText(string entryName); + + /// + /// 将指定条目按原始字节提取到目标文件(用于探测二进制文件,如附带启动器的 exe)。 + /// + /// 条目名。 + /// 目标文件路径。 + void ExtractEntryToFile(string entryName, string destinationPath); } diff --git a/Plain Craft Launcher 2/Modules/Minecraft/FolderModpackArchiveReader.cs b/Plain Craft Launcher 2/Modules/Minecraft/FolderModpackArchiveReader.cs index b72435aa3e..68db8213ed 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/FolderModpackArchiveReader.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/FolderModpackArchiveReader.cs @@ -35,4 +35,10 @@ public string ReadEntryText(string entryName) { return ModBase.ReadFile(Path.Combine(_root, entryName)); } + + public void ExtractEntryToFile(string entryName, string destinationPath) + { + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath) ?? ""); + File.Copy(Path.Combine(_root, entryName), destinationPath, true); + } } diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs index 1dafdb3bcf..8541a5940e 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -24,62 +24,103 @@ public static partial class ModModpack "launcher_profiles.json", "usercache.json", "usernamecache.json" }; - private static LoaderCombo _InstallLauncherPack(string sourcePath) + private static LoaderCombo _InstallLauncherPack(string sourcePath, IModpackArchiveReader archive) { - // 解压到临时目录(无需用户选择空文件夹),内层整合包安装完成后由系统自动清理 - var installTemp = ModMain.RequestTaskTempFolder(); - var loader = new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[] + // 阶段 0:只探测压缩包是否附带启动器(仅提取顶层 exe 读取产品名,不完整解压) + string bundledLauncherEntry = null; + var probeFolder = ModMain.RequestTaskTempFolder(); + try { - new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => + foreach (var entryName in archive.EntryNames) { - _ExtractModpackFiles(installTemp, sourcePath, task, 0.9d); - Thread.Sleep(400); // 避免文件争用 - // 查找解压后的 exe 文件 - string launcher = null; - foreach (var exeFile in Directory.GetFiles(installTemp, "*.exe", SearchOption.TopDirectoryOnly)) + if (entryName.Contains("/") || !entryName.EndsWithF(".exe", true)) + continue; // 只探测顶层的 *.exe + try { - var info = FileVersionInfo.GetVersionInfo(exeFile); - ModBase.Log($"[Modpack] 文件 {exeFile} 的产品名标识为 {info.ProductName}"); - if (info.ProductName == "Plain Craft Launcher") - { - launcher = exeFile; - ModBase.Log($"[Modpack] 发现整合包附带的 PCL 启动器:{exeFile}"); - } - else if ((info.ProductName.ContainsF("Launcher", true) || info.ProductName.ContainsF("启动", true)) && - !(info.ProductName == "Plain Craft Launcher Admin Manager")) + var probePath = Path.Combine(probeFolder, Path.GetFileName(entryName)); + archive.ExtractEntryToFile(entryName, probePath); + var info = FileVersionInfo.GetVersionInfo(probePath); + var productName = info.ProductName; + ModBase.Log($"[Modpack] 探测到附带可执行文件 {entryName},产品名:{productName}"); + // PCL 或第三方启动器(与原有判断一致),排除 PCL 管理助手 + if (productName == "Plain Craft Launcher" || + (productName is not null && + (productName.ContainsF("Launcher", true) || productName.ContainsF("启动", true)) && + productName != "Plain Craft Launcher Admin Manager")) { - if (launcher is null) - { - launcher = exeFile; - ModBase.Log($"[Modpack] 发现整合包附带的疑似第三方启动器:{exeFile}"); - } + bundledLauncherEntry = entryName; + break; } } + catch (Exception ex) + { + ModBase.Log(ex, "探测压缩包内附带启动器失败:" + entryName); + } + } + } + finally + { + ModBase.DeleteDirectory(probeFolder); + } - task.Progress = 0.95d; - // 尝试使用附带的启动器打开 - if (launcher is not null) + // 询问用户是否换用附带启动器 + if (bundledLauncherEntry is not null) + { + var bundledPath = bundledLauncherEntry.Replace("/", @"\"); + ModBase.Log("[Modpack] 找到压缩包中附带的启动器:" + bundledPath); + if (ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.BundledLauncher.Message", bundledPath), + Lang.Text("Minecraft.Download.Modpack.BundledLauncher.Title"), + Lang.Text("Minecraft.Download.Modpack.BundledLauncher.UseBundled"), + Lang.Text("Minecraft.Download.Modpack.BundledLauncher.DoNotUse")) == 1) + { + // 附带启动器需要把文件安装到它自己的持久目录里,因此让用户选一个空文件夹并直接解压进去。 + ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.SelectEmptyFolder.Message"), + Lang.Text("Common.Action.Install"), Lang.Text("Common.Action.Continue"), forceWait: true); + var targetFolder = SystemDialogs.SelectFolder(Lang.Text("Minecraft.Download.Modpack.SelectTargetFolder.Title")); + if (string.IsNullOrEmpty(targetFolder)) + throw new ModBase.CancelledException(); + if (Directory.GetFileSystemEntries(targetFolder).Length > 0) + { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.TargetFolderMustBeEmpty"), HintType.Error); + throw new ModBase.CancelledException(); + } + + // 直接解压到所选文件夹(不走临时目录),然后从该文件夹运行附带启动器 + var launcherLoader = new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[] { - ModBase.Log("[Modpack] 找到压缩包中附带的启动器:" + launcher); - if (ModMain.MyMsgBox(Lang.Text("Minecraft.Download.Modpack.BundledLauncher.Message", launcher), - Lang.Text("Minecraft.Download.Modpack.BundledLauncher.Title"), - Lang.Text("Minecraft.Download.Modpack.BundledLauncher.UseBundled"), - Lang.Text("Minecraft.Download.Modpack.BundledLauncher.DoNotUse") - ) == 1) + new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => { - // 换用附带启动器:保留临时目录供其使用 - ModBase.OpenExplorer(installTemp); - ModBase.ShellOnly(launcher, "--wait"); // 要求等待已有的 PCL 退出 + _ExtractModpackFiles(targetFolder, sourcePath, task, 0.9d); + task.Progress = 0.95d; + ModBase.OpenExplorer(targetFolder); + ModBase.ShellOnly(Path.Combine(targetFolder, bundledPath), "--wait"); // 要求等待已有的 PCL 退出 ModBase.Log("[Modpack] 为换用整合包中的启动器启动,强制结束程序"); ModMain.frmMain.EndProgram(false); - return; - } - } - else - { - ModBase.Log("[Modpack] 未找到压缩包中附带的启动器"); - } + }) + }); + launcherLoader.Start(targetFolder); + LoaderTaskbarAdd(launcherLoader); + ModMain.frmMain.BtnExtraDownload.ShowRefresh(); + ModMain.frmMain.BtnExtraDownload.Ribble(); + return launcherLoader; + } + + ModBase.Log("[Modpack] 用户选择不使用附带启动器,继续安装到 PCL"); + } + else + { + ModBase.Log("[Modpack] 未找到压缩包中附带的启动器"); + } + // 阶段 1:安装到 PCL(解压到临时目录,找内层整合包安装,临时目录由系统自动清理) + var installTemp = ModMain.RequestTaskTempFolder(); + var installLoader = new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), new[] + { + new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => + { + _ExtractModpackFiles(installTemp, sourcePath, task, 0.9d); + Thread.Sleep(400); // 避免文件争用 + task.Progress = 0.95d; // 寻找并安装内层整合包(任意格式:modpack.* 压缩包文件或文件夹形式的整合包) var innerPackPath = _FindInnerModpack(installTemp); if (innerPackPath is not null) @@ -97,11 +138,11 @@ private static LoaderCombo _InstallLauncherPack(string sourcePath) } }) }); - loader.Start(installTemp); - LoaderTaskbarAdd(loader); + installLoader.Start(installTemp); + LoaderTaskbarAdd(installLoader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); ModMain.frmMain.BtnExtraDownload.Ribble(); - return loader; + return installLoader; } /// diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs index 93e06d0a43..47f7543d36 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs @@ -126,7 +126,7 @@ private static LoaderCombo _InstallSource(IModpackArchiveReader source, case ModpackFormat.Hmcl: return _InstallHmcl(sourcePath, source, archiveBaseFolder); case ModpackFormat.LauncherPack: - return _InstallLauncherPack(sourcePath); + return _InstallLauncherPack(sourcePath, source); case ModpackFormat.LazyPack: return _InstallCompress(sourcePath, source); default: diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ZipModpackArchiveReader.cs b/Plain Craft Launcher 2/Modules/Minecraft/ZipModpackArchiveReader.cs index 818a0f7f23..9896d6798a 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ZipModpackArchiveReader.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ZipModpackArchiveReader.cs @@ -36,4 +36,14 @@ public string ReadEntryText(string entryName) using var stream = _archive.GetEntry(entryName)?.Open(); return stream is null ? "" : ModBase.ReadFile(stream); } + + public void ExtractEntryToFile(string entryName, string destinationPath) + { + using var stream = _archive.GetEntry(entryName)?.Open(); + if (stream is null) + return; + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath) ?? ""); + using var file = File.Create(destinationPath); + stream.CopyTo(file); + } } From 62b652bd9747595513d516f2ad39459e01258874 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Fri, 7 Aug 2026 20:28:10 +0800 Subject: [PATCH 07/19] =?UTF-8?q?fix:=E6=87=92=E4=BA=BA=E5=8C=85=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E7=9A=84=E4=B8=80=E4=BA=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Localization/Languages/en-US.xaml | 4 + .../App/Localization/Languages/zh-CN.xaml | 4 + .../Controls/MyMsg/MyMsgText.xaml | 3 + .../Controls/MyMsg/MyMsgText.xaml.cs | 18 +++ .../Modules/Minecraft/ModModpack.Container.cs | 108 +++++++++++++++++- Plain Craft Launcher 2/Modules/ModMain.cs | 18 ++- 6 files changed, 146 insertions(+), 9 deletions(-) diff --git a/PCL.Core/App/Localization/Languages/en-US.xaml b/PCL.Core/App/Localization/Languages/en-US.xaml index c090e7a13e..319cbf83bf 100644 --- a/PCL.Core/App/Localization/Languages/en-US.xaml +++ b/PCL.Core/App/Localization/Languages/en-US.xaml @@ -2006,6 +2006,10 @@ This modpack version is too old and no longer supported! No suitable file structure was found. This may not be a Minecraft archive. Cannot install modpack, unknown mod loader {0} (version {1})! + A same-named file has different content: {0} Skip or overwrite it? + File content conflict + Overwrite all + Skip all Failed to open the modpack file. The file may be corrupted or in an unsupported archive format. diff --git a/PCL.Core/App/Localization/Languages/zh-CN.xaml b/PCL.Core/App/Localization/Languages/zh-CN.xaml index 9bae29f93c..1ad3aa61a9 100644 --- a/PCL.Core/App/Localization/Languages/zh-CN.xaml +++ b/PCL.Core/App/Localization/Languages/zh-CN.xaml @@ -2006,6 +2006,10 @@ 该整合包版本过老,已不支持进行安装! 未能找到适合的文件结构,这可能不是一个 MC 压缩包 无法安装整合包,其中出现了未知的模组加载器 {0}(版本为 {1})! + 发现同名文件内容不同:{0} 要跳过还是覆盖? + 文件内容冲突 + 全部覆盖 + 全部跳过 打开整合包文件失败,文件可能损坏或为不支持的压缩包格式 diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml index 724dfd415a..7f26b554d0 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml @@ -46,6 +46,9 @@ + diff --git a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs index ec78916856..f73570544b 100644 --- a/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs +++ b/Plain Craft Launcher 2/Controls/MyMsg/MyMsgText.xaml.cs @@ -20,12 +20,14 @@ public MyMsgText(ModMain.MyMsgBoxConverter converter) AppendUniqueNameSuffix(Btn1); AppendUniqueNameSuffix(Btn2); AppendUniqueNameSuffix(Btn3); + AppendUniqueNameSuffix(Btn4); myConverter = converter; LabTitle.Text = converter.Title; LabCaption.Text = converter.Text; ConfigurePrimaryButton(converter.Button1, converter.IsWarn); ConfigureSecondaryButton(Btn2, converter.Button2); ConfigureSecondaryButton(Btn3, converter.Button3); + ConfigureSecondaryButton(Btn4, converter.Button4); ShapeLine.StrokeThickness = ModBase.GetWPFSize(1d); } @@ -176,6 +178,22 @@ public void Btn3_Click(object sender, MouseButtonEventArgs e) } } + public void Btn4_Click(object sender, MouseButtonEventArgs e) + { + if (myConverter.IsExited) + return; + if (myConverter.Button4Action is not null) + { + myConverter.Button4Action(); + } + else + { + myConverter.IsExited = true; + myConverter.Result = 4; + Close(); + } + } + private void Drag(object sender, MouseButtonEventArgs e) { try diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs index 8541a5940e..ab0e5a5ef9 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO; +using System.IO.Compression; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using PCL.Core.App.Localization; @@ -42,11 +43,12 @@ private static LoaderCombo _InstallLauncherPack(string sourcePath, IModp var info = FileVersionInfo.GetVersionInfo(probePath); var productName = info.ProductName; ModBase.Log($"[Modpack] 探测到附带可执行文件 {entryName},产品名:{productName}"); - // PCL 或第三方启动器(与原有判断一致),排除 PCL 管理助手 - if (productName == "Plain Craft Launcher" || + // PCL 或第三方启动器,排除 PCL 管理助手 + if (productName == "Plain Craft Launcher Community Edition" || + productName == "Plain Craft Launcher" || (productName is not null && (productName.ContainsF("Launcher", true) || productName.ContainsF("启动", true)) && - productName != "Plain Craft Launcher Admin Manager")) + productName != "Plain Craft Launcher Admin Manager")) //我不知道这个”PCL 管理助手“是什么,原来在重构前的ModModpack.cs第1150行 { bundledLauncherEntry = entryName; break; @@ -153,12 +155,18 @@ private static LoaderCombo _InstallLauncherPack(string sourcePath, IModp /// 内层整合包的文件路径或文件夹路径;未找到时返回 null。 private static string? _FindInnerModpack(string rootFolder) { - // 优先:modpack.zip / modpack.mrpack 等压缩包文件 + // 优先:modpack.* 压缩包文件(常见命名,无需校验) foreach (var file in Directory.GetFiles(rootFolder, "modpack.*", SearchOption.AllDirectories)) if (file.EndsWithF(".zip", true) || file.EndsWithF(".mrpack", true)) return file; - // 其次:包含已知清单的文件夹(按目录深度由浅到深搜索,优先匹配最外层的整合包) + // 其次:任意名称的 .zip / .mrpack 文件(部分懒人包的内层整合包不叫 modpack.*),校验确实是整合包 + foreach (var file in Directory.EnumerateFiles(rootFolder, "*.zip", SearchOption.AllDirectories) + .Concat(Directory.EnumerateFiles(rootFolder, "*.mrpack", SearchOption.AllDirectories))) + if (_IsModpackArchive(file)) + return file; + + // 再次:包含已知清单的文件夹(按目录深度由浅到深搜索,优先匹配最外层的整合包) foreach (var directory in Directory.GetDirectories(rootFolder, "*", SearchOption.AllDirectories) .OrderBy(directory => directory.Length)) { @@ -183,6 +191,25 @@ private static LoaderCombo _InstallLauncherPack(string sourcePath, IModp return null; } + /// + /// 判断一个压缩包文件是否是整合包:使用格式识别器检查其根目录或一级目录是否存在已知整合包清单。 + /// 资源包等普通 zip 不包含这些清单,会被排除,避免误把非整合包文件当作内层整合包。 + /// + /// 压缩包文件路径。 + private static bool _IsModpackArchive(string filePath) + { + try + { + using var archive = ZipFile.OpenRead(filePath); + var format = ModpackArchiveDetector.Detect(new ZipModpackArchiveReader(archive)).Format; + return format != ModpackFormat.Unknown; + } + catch (Exception) + { + return false; // 无法打开或识别失败的压缩包不算整合包 + } + } + #endregion #region 懒人包(完整实例压缩包) @@ -289,6 +316,13 @@ private static void _InstallLazyPackInstance(string installTemp, string archiveB ModBase.CopyFile(item, dest); } + // 复制共享目录(libraries/assets)到游戏文件夹根目录,避免重复下载并保留整合包的特殊依赖版本; + // 同名文件内容不同时由用户决定跳过还是覆盖。 + _CopySharedFolderWithConflict(Path.Combine(mcRoot, "libraries"), + Path.Combine(ModFolder.mcFolderSelected, "libraries")); + _CopySharedFolderWithConflict(Path.Combine(mcRoot, "assets"), + Path.Combine(ModFolder.mcFolderSelected, "assets")); + // 开启版本隔离 var versionIni = instanceFolder + @"PCL\Setup.ini"; ModBase.WriteIni(versionIni, "VersionArgumentIndie", 1.ToString()); @@ -296,5 +330,69 @@ private static void _InstallLazyPackInstance(string installTemp, string archiveB ModBase.IniClearCache(versionIni); } + /// + /// 复制共享目录(如 libraries/assets)到游戏文件夹根目录。 + /// 同名文件先比较内容:内容相同则跳过;内容不同时弹窗让用户选择跳过或覆盖(可全部跳过/全部覆盖,默认跳过)。 + /// + private static void _CopySharedFolderWithConflict(string sourceFolder, string destFolder) + { + if (!Directory.Exists(sourceFolder)) + return; + Directory.CreateDirectory(destFolder); + var overwriteAll = false; + var skipAll = false; + foreach (var sourceFile in Directory.EnumerateFiles(sourceFolder, "*", SearchOption.AllDirectories)) + { + var relativePath = Path.GetRelativePath(sourceFolder, sourceFile); + var destFile = Path.Combine(destFolder, relativePath); + if (File.Exists(destFile)) + { + // 同名文件:先比较内容,相同则无需处理 + if (_FilesEqual(sourceFile, destFile)) + continue; + if (skipAll) + continue; + if (!overwriteAll) + { + var choice = ModMain.MyMsgBox( + Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.Message", relativePath), + Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.Title"), + Lang.Text("Minecraft.Download.Modpack.OptionalFile.Skip"), + Lang.Text("Common.Action.Overwrite"), + Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.OverwriteAll"), + highLight: true, forceWait: true, + button4: Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.SkipAll")); + switch (choice) + { + case 1: continue; // 跳过 + case 2: break; // 覆盖 + case 3: overwriteAll = true; break; // 全部覆盖 + case 4: skipAll = true; continue; // 全部跳过 + } + } + } + + Directory.CreateDirectory(Path.GetDirectoryName(destFile) ?? ""); + File.Copy(sourceFile, destFile, true); + } + } + + /// + /// 比较两个文件内容是否相同(先比大小,再比 MD5)。 + /// + private static bool _FilesEqual(string fileA, string fileB) + { + try + { + if (new FileInfo(fileA).Length != new FileInfo(fileB).Length) + return false; + return ModBase.GetFileMD5(fileA) == ModBase.GetFileMD5(fileB); + } + catch (Exception) + { + return false; + } + } + #endregion } diff --git a/Plain Craft Launcher 2/Modules/ModMain.cs b/Plain Craft Launcher 2/Modules/ModMain.cs index c1ecd882a1..4bc298e552 100644 --- a/Plain Craft Launcher 2/Modules/ModMain.cs +++ b/Plain Craft Launcher 2/Modules/ModMain.cs @@ -253,6 +253,13 @@ public class MyMsgBoxConverter /// public Action Button3Action; + public string Button4 = ""; + + /// + /// 点击第四个按钮将执行该方法,不关闭弹窗。 + /// + public Action Button4Action; + /// /// 输入模式:文本框的文本。 /// 选择模式:需要放进去的 List(Of MyListItem)。 @@ -327,18 +334,21 @@ public enum MyMsgBoxType /// 是否为警告弹窗,若为 True,弹窗配色和背景会变为红色。 public static int MyMsgBox(string caption, string? title = null, string? button1 = null, string? button2 = "", string? button3 = "", bool isWarn = false, bool highLight = true, bool forceWait = false, - Action button1Action = null, Action button2Action = null, Action button3Action = null) + Action button1Action = null, Action button2Action = null, Action button3Action = null, string? button4 = "", + Action button4Action = null) { title ??= GetDefaultDialogTitle(); button1 ??= GetDefaultConfirmText(); button2 ??= ""; button3 ??= ""; + button4 ??= ""; // 将弹窗列入队列 var converter = new MyMsgBoxConverter { - Type = MyMsgBoxType.Text, Button1 = button1, Button2 = button2, Button3 = button3, Text = caption, - IsWarn = isWarn, Title = title, HighLight = highLight, ForceWait = true, Button1Action = button1Action, - Button2Action = button2Action, Button3Action = button3Action + Type = MyMsgBoxType.Text, Button1 = button1, Button2 = button2, Button3 = button3, Button4 = button4, + Text = caption, IsWarn = isWarn, Title = title, HighLight = highLight, ForceWait = true, + Button1Action = button1Action, Button2Action = button2Action, Button3Action = button3Action, + Button4Action = button4Action }; WaitingMyMsgBox.Add(converter); if (ModBase.RunInUi()) From fe41d9a6949d738d2f7f3fbc20764c9d7ac0553d Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Fri, 7 Aug 2026 22:40:21 +0800 Subject: [PATCH 08/19] =?UTF-8?q?fix:=E6=95=B4=E5=90=88=E5=8C=85=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E6=97=B6=E7=9A=84=E5=9B=BE=E6=A0=87=E4=B8=8D=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Minecraft/Modpack/CurseForgeManifest.cs | 5 ++ .../Minecraft/ModModpack.CurseForge.cs | 46 ++++++++++++++++--- .../Modules/Minecraft/ModModpack.cs | 24 +++++++--- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs b/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs index 40072876be..38dc73a2fc 100644 --- a/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs +++ b/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs @@ -35,6 +35,11 @@ public sealed class CurseForgeManifest /// public string? Author { get; init; } + /// + /// 整合包图标路径(可选),通常是压缩包内的相对路径,如 profileImage/xxx.jpg。 + /// + public string? Image { get; init; } + /// /// 游戏相关信息;缺失时表示清单未声明游戏版本。 /// diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs index c18dc86044..9ad5ef589a 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs @@ -152,6 +152,8 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac ProgressWeight = modList.Count / 10d }); // 每 10 Mod 需要 1s // 构造 NetFile + // 地图(World)文件:下载到临时目录,随后解压成 saves\地图名\ 文件夹 + var worldExtracts = new List<(string archivePath, string targetDirectory)>(); modDownloadLoaders.Add(new LoaderTask>( Lang.Text("Minecraft.Download.Modpack.Stage.BuildModsDownloadInfo"), task => { @@ -189,7 +191,9 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac targetFolder = "resourcepacks"; type = ModComp.CompType.ResourcePack; } - else if (moduleNames.Contains("level.dat")) + // 地图存档的 level.dat 常位于一层父文件夹内(如 MyMap/level.dat),需按路径段判断 + else if (moduleNames.Any(name => + name.Split("/").Last().Equals("level.dat", StringComparison.OrdinalIgnoreCase))) { targetFolder = "saves"; type = ModComp.CompType.World; @@ -210,10 +214,24 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac var file = new ModComp.CompFile((JsonObject)modJson, type); if (!file.Available) continue; - // 实际的添加 - fileList.Add(id, - file.ToNetFile(_GetVersionFolder(instanceName) + targetFolder + @"\", - ModComp.DownloadReason.ModPack, manifest.Minecraft.Version, modLoader)); + if (type == ModComp.CompType.World) + { + // 地图:先下载到临时目录,下载完成后解压成 saves\地图名\ 文件夹(压缩包本身无法被游戏直接读取) + var archivePath = Path.Combine(installTemp, "world_" + worldExtracts.Count + ".tmp"); + fileList.Add(id, + file.ToNetFile(archivePath, + ModComp.DownloadReason.ModPack, manifest.Minecraft.Version, modLoader)); + worldExtracts.Add((archivePath, + _GetVersionFolder(instanceName) + "saves\\" + + ModBase.GetFileNameWithoutExtentionFromPath(file.FileName) + "\\")); + } + else + { + // 实际的添加 + fileList.Add(id, + file.ToNetFile(_GetVersionFolder(instanceName) + targetFolder + @"\", + ModComp.DownloadReason.ModPack, manifest.Minecraft.Version, modLoader)); + } task.Progress += 1d / (1 + modList.Count); } @@ -226,6 +244,14 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac // 下载 Mod 文件 modDownloadLoaders.Add(new LoaderDownload(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadMods"), []) { ProgressWeight = modList.Count * 1.5d }); // 每个 Mod 需要 1.5s + // 解压地图存档(下载到临时目录的地图,解压成 saves\地图名\ 文件夹) + modDownloadLoaders.Add(new LoaderTask( + Lang.Text("Minecraft.Download.Modpack.Stage.ExtractModpack"), task => + { + foreach (var (archivePath, targetDirectory) in worldExtracts) + _ExtractArchiveToDirectory(archivePath, targetDirectory); + }) + { show = false, ProgressWeight = 1d }); // 构造加载器 installLoaders.Add( new LoaderCombo(Lang.Text("Minecraft.Download.Modpack.Stage.DownloadMods.MainLoader"), @@ -253,8 +279,16 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac { show = false, ProgressWeight = mergeLoaders.Sum(l => l.ProgressWeight) }); loaders.Add(new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.FinalizeFiles"), task => { - _FinalizeInstance(_GetVersionFolder(instanceName), sourcePath, logo, "CurseForge", + var versionFolder = _GetVersionFolder(instanceName); + _FinalizeInstance(versionFolder, sourcePath, logo, "CurseForge", manifest.Version, resourceId); + // 本地安装没有外部 logo 时,尝试使用整合包内嵌的图标(manifest.json 的 image 字段) + if (logo is null && manifest.Image is not null) + { + var iconPath = Path.Combine(installTemp, archiveBaseFolder.Replace("/", @"\"), + manifest.Image.Replace("/", @"\")); + _SetInstanceIcon(versionFolder, iconPath); + } }) { ProgressWeight = 0.1d, diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs index 47f7543d36..4af0c573f3 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs @@ -308,13 +308,8 @@ private static void _FinalizeInstance(string versionFolder, string fileAddress, string modpackVersion, string resourceId) { // 设置图标 - if (logo is not null && File.Exists(logo)) - { - File.Copy(logo, Path.Combine(versionFolder, "PCL", "Logo.png"), true); - States.Instance.LogoPath[versionFolder] = @"PCL\Logo.png"; - States.Instance.IsLogoCustom[versionFolder] = true; - ModBase.Log("[ModPack] 已设置整合包 Logo:" + logo); - } + if (logo is not null) + _SetInstanceIcon(versionFolder, logo); // 删除原始整合包文件 foreach (var target in new[] { Path.Combine(versionFolder, "原始整合包.zip"), Path.Combine(versionFolder, "原始整合包.mrpack") }) @@ -350,5 +345,20 @@ private static void _FinalizeInstance(string versionFolder, string fileAddress, } while (false); } + /// + /// 将图标文件复制到实例的 PCL\Logo.png 并标记为自定义图标。 + /// + private static void _SetInstanceIcon(string versionFolder, string iconSourcePath) + { + if (!File.Exists(iconSourcePath)) + return; + var logoPath = Path.Combine(versionFolder, "PCL", "Logo.png"); + Directory.CreateDirectory(Path.GetDirectoryName(logoPath) ?? ""); + File.Copy(iconSourcePath, logoPath, true); + States.Instance.LogoPath[versionFolder] = @"PCL\Logo.png"; + States.Instance.IsLogoCustom[versionFolder] = true; + ModBase.Log("[ModPack] 已设置整合包 Logo:" + iconSourcePath); + } + #endregion } From 2b347efee7ce4ea0be35d089106b5b98b17ae53a Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Sat, 8 Aug 2026 20:10:14 +0800 Subject: [PATCH 09/19] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E6=95=B4=E5=90=88?= =?UTF-8?q?=E5=8C=85=E6=8E=A8=E8=8D=90=E5=86=85=E5=AD=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modpack/CurseForgeManifestTest.cs | 34 ++++++++++++++ .../App/Localization/Languages/en-US.xaml | 3 ++ .../App/Localization/Languages/zh-CN.xaml | 3 ++ PCL.Core/App/States.cs | 1 + .../Minecraft/Modpack/CurseForgeManifest.cs | 16 +++++++ .../Minecraft/ModModpack.CurseForge.cs | 45 +++++++++++++++++-- .../Modules/Minecraft/ModModpack.cs | 6 ++- .../Pages/PageInstance/PageInstanceSetup.xaml | 16 ++++--- .../PageInstance/PageInstanceSetup.xaml.cs | 16 ++++++- 9 files changed, 128 insertions(+), 12 deletions(-) diff --git a/PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs b/PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs index fe92799d2e..5bbe9b6c92 100644 --- a/PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs +++ b/PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs @@ -16,6 +16,7 @@ public void ParsesCompleteManifest() "manifestVersion": 1, "name": "示例整合包", "version": "1.0.0", + "recommendedRam": 8192, "author": "作者名", "minecraft": { "version": "1.20.1", @@ -34,6 +35,7 @@ public void ParsesCompleteManifest() Assert.AreEqual(1, manifest.ManifestVersion); Assert.AreEqual("示例整合包", manifest.Name); Assert.AreEqual("1.0.0", manifest.Version); + Assert.AreEqual(8192, manifest.RecommendedRam); Assert.AreEqual("作者名", manifest.Author); Assert.AreEqual("1.20.1", manifest.Minecraft?.Version); Assert.IsNotNull(manifest.Minecraft?.ModLoaders); @@ -49,6 +51,36 @@ public void ParsesCompleteManifest() Assert.AreEqual("overrides", manifest.Overrides); } + [TestMethod] + public void RecommendedRamPrefersRootOverMinecraft() + { + const string json = """ + { + "recommendedRam": 4096, + "minecraft": { + "version": "1.20.1", + "recommendedRam": 8192 + } + } + """; + var manifest = CurseForgeManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.AreEqual(4096, manifest.RecommendedRam); + Assert.AreEqual(8192, manifest.Minecraft?.RecommendedRam); + Assert.AreEqual(4096, manifest.RecommendedRamEffective); + } + + [TestMethod] + public void RecommendedRamFromMinecraftWhenRootMissing() + { + const string json = """{"minecraft": { "version": "1.20.1", "recommendedRam": 8192 }}"""; + var manifest = CurseForgeManifest.Parse(JsonCompat.ParseNode(json)); + Assert.IsNotNull(manifest); + Assert.IsNull(manifest.RecommendedRam); + Assert.AreEqual(8192, manifest.Minecraft?.RecommendedRam); + Assert.AreEqual(8192, manifest.RecommendedRamEffective); + } + [TestMethod] public void MissingRequiredDefaultsToRequired() { @@ -70,6 +102,8 @@ public void MissingOptionalFieldsStayNull() Assert.IsNull(manifest.Files); Assert.IsNull(manifest.Overrides); Assert.IsNull(manifest.Version); + Assert.IsNull(manifest.RecommendedRam); + Assert.IsNull(manifest.RecommendedRamEffective); } [TestMethod] diff --git a/PCL.Core/App/Localization/Languages/en-US.xaml b/PCL.Core/App/Localization/Languages/en-US.xaml index 319cbf83bf..db22670084 100644 --- a/PCL.Core/App/Localization/Languages/en-US.xaml +++ b/PCL.Core/App/Localization/Languages/en-US.xaml @@ -1386,6 +1386,7 @@ These settings only apply to this game instance and do not affect other instances. Instance settings initialized! Memory used + Use modpack recommended memory Global settings @@ -1994,6 +1995,8 @@ File path validation failed Cannot install this modpack: Quilt is no longer supported by this launcher. PCL CE cannot handle .rar archive files. Please extract and re-archive them in .zip format and try again! + This modpack recommends {0} GiB of memory. Use the modpack's recommended memory? + Modpack recommended memory Next, select an empty folder. The modpack will be installed there. Select installation target (must be an empty folder) Some mod versions in the modpack have been deleted by the mod authors, so installation cannot continue. Please report this issue to the modpack author. diff --git a/PCL.Core/App/Localization/Languages/zh-CN.xaml b/PCL.Core/App/Localization/Languages/zh-CN.xaml index 1ad3aa61a9..18e3ad468e 100644 --- a/PCL.Core/App/Localization/Languages/zh-CN.xaml +++ b/PCL.Core/App/Localization/Languages/zh-CN.xaml @@ -1386,6 +1386,7 @@ 这些设置只对该游戏实例生效,不影响其他实例。 已初始化实例独立设置! 已使用内存 + 使用整合包推荐内存 全局设置 @@ -1994,6 +1995,8 @@ 文件路径校验失败 无法安装整合包:当前启动器已不再支持 Quilt 加载器。 PCL CE 无法处理 rar 格式的压缩包,请在解压后重新压缩为 zip 格式再试 + 该整合包有推荐内存 {0} GiB,是否使用整合包推荐的内存? + 整合包推荐内存 接下来请选择一个空文件夹,它会被安装到这个文件夹里。 选择安装目标(必须是一个空文件夹) 整合包中的部分模组版本已被模组作者删除,所以没法继续安装了,请向整合包作者反馈该问题 diff --git a/PCL.Core/App/States.cs b/PCL.Core/App/States.cs index e09988900f..2942ed8168 100644 --- a/PCL.Core/App/States.cs +++ b/PCL.Core/App/States.cs @@ -352,5 +352,6 @@ public static partial class States [ConfigItem("VersionModpackVersion", "")] public partial ArgConfig ModpackVersion { get; } [ConfigItem("VersionModpackSource", "")] public partial ArgConfig ModpackSource { get; } [ConfigItem("VersionModpackId", "")] public partial ArgConfig ModpackId { get; } + [ConfigItem("VersionModpackRam", 0)] public partial ArgConfig ModpackRam { get; } } } diff --git a/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs b/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs index 38dc73a2fc..26dce63edb 100644 --- a/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs +++ b/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs @@ -30,6 +30,17 @@ public sealed class CurseForgeManifest /// public string? Version { get; init; } + /// + /// 整合包推荐的启动内存(单位 MB,可选),如 4096 表示 4 GiB;缺失时表示未声明推荐内存。 + /// + public int? RecommendedRam { get; init; } + + /// + /// 有效的推荐内存:根级 recommendedRam 优先,其次 minecraft.recommendedRam; + /// 两者都不存在时表示未声明推荐内存。 + /// + public int? RecommendedRamEffective => RecommendedRam ?? Minecraft?.RecommendedRam; + /// /// 作者名称(可选)。 /// @@ -76,6 +87,11 @@ public sealed class CurseForgeMinecraft /// public string? Version { get; init; } + /// + /// 推荐内存(单位 MB,可选);部分整合包把 recommendedRam 放在 minecraft 对象内而非根级。 + /// + public int? RecommendedRam { get; init; } + /// /// 加载器列表,如 forge-47.2.0fabric-0.15.11。 /// diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs index 9ad5ef589a..b16611864a 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs @@ -1,6 +1,7 @@ using System.IO; using System.IO.Compression; using System.Text.Json.Nodes; +using PCL.Core.App; using PCL.Core.App.Localization; using PCL.Core.Minecraft.Modpack; using PCL.Core.UI; @@ -38,6 +39,17 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac if (instanceName is null) instanceName = _PromptInstanceName(manifest.Name ?? ""); + // 推荐内存询问:清单声明了推荐内存时,询问是否使用 + // (确定 → 使用推荐内存模式;取消 → 跟随全局设置,即默认选项) + var useRecommendedRam = false; + if (manifest.RecommendedRamEffective is > 0) + useRecommendedRam = ModMain.MyMsgBox( + Lang.Text("Minecraft.Download.Modpack.RecommendedRam.Message", + (manifest.RecommendedRamEffective.Value / 1024d).ToString("0.#")), + Lang.Text("Minecraft.Download.Modpack.RecommendedRam.Title"), + Lang.Text("Common.Action.Confirm"), + Lang.Text("Common.Action.Cancel")) == 1; + // 获取 Mod API 版本信息 string forgeVersion = null; string neoForgeVersion = null; @@ -174,14 +186,16 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac ) == 2) continue; - // 根据 modules 和文件名后缀判断资源类型 + // 根据 modules、文件名后缀与游戏版本需求判断资源类型 string targetFolder; ModComp.CompType type; if (modJson["modules"].AsArray().Any()) // modules 可能返回 null(#1006) { var moduleNames = ((JsonArray)modJson["modules"]).Select(l => l["name"].ToString()).ToList(); + var fileName = modJson?["FileName"]?.ToString() ?? ""; + var gameVersions = ((JsonArray)modJson?["gameVersions"])?.Select(v => v.ToString()).ToList() ?? []; if (moduleNames.Contains("META-INF") || moduleNames.Contains("mcmod.info") || - (modJson?["FileName"]?.ToString()?.EndsWithF(".jar", true)).GetValueOrDefault()) + fileName.EndsWithF(".jar", true)) { targetFolder = "mods"; type = ModComp.CompType.Mod; @@ -191,13 +205,27 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac targetFolder = "resourcepacks"; type = ModComp.CompType.ResourcePack; } - // 地图存档的 level.dat 常位于一层父文件夹内(如 MyMap/level.dat),需按路径段判断 + // 光影包:含 shaders 目录 / .placebo 文件,或声明了 OptiFine/Iris 需求 + else if (moduleNames.Contains("shaders") || moduleNames.Any(name => name.EndsWithF(".placebo", true)) || + gameVersions.Contains("OptiFine", StringComparer.OrdinalIgnoreCase) || + gameVersions.Contains("Iris", StringComparer.OrdinalIgnoreCase)) + { + targetFolder = "shaderpacks"; + type = ModComp.CompType.Shader; + } + // 地图存档:level.dat 可能位于一层父文件夹内(如 MyMap/level.dat),需按路径段判断 else if (moduleNames.Any(name => name.Split("/").Last().Equals("level.dat", StringComparison.OrdinalIgnoreCase))) { targetFolder = "saves"; type = ModComp.CompType.World; } + // 地图存档(包裹式):modules 仅有一个无扩展名的顶层条目时,是“zip 内再包一层存档文件夹”的地图 + else if (moduleNames.Count == 1 && !Path.HasExtension(moduleNames[0])) + { + targetFolder = "saves"; + type = ModComp.CompType.World; + } else { targetFolder = "shaderpacks"; @@ -281,7 +309,16 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac { var versionFolder = _GetVersionFolder(instanceName); _FinalizeInstance(versionFolder, sourcePath, logo, "CurseForge", - manifest.Version, resourceId); + manifest.Version, resourceId, manifest.RecommendedRamEffective ?? 0); + // 应用推荐内存选择:仅当清单声明了推荐内存时写配置 + // (确定 → 使用推荐内存模式;取消 → 跟随全局设置,即默认选项) + // MemorySolution 注册了 UI 观察者(ModSetup.VersionRamType),需在 UI 线程写入, + // 避免在后台安装线程触发观察者导致跨线程访问 UI 控件。 + if (manifest.RecommendedRamEffective is > 0) + { + var memorySolution = useRecommendedRam ? 3 : 2; + ModBase.RunInUi(() => Config.Instance.MemorySolution[versionFolder] = memorySolution); + } // 本地安装没有外部 logo 时,尝试使用整合包内嵌的图标(manifest.json 的 image 字段) if (logo is null && manifest.Image is not null) { diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs index 4af0c573f3..1b060e3a56 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs @@ -302,10 +302,11 @@ private static LoaderCombo _StartInstall(string loaderName, List - /// 整合包安装收尾:设置图标、删除原始整合包文件、写入整合包版本/来源/ID 信息并尝试获取整合包描述。 + /// 整合包安装收尾:设置图标、删除原始整合包文件、写入整合包版本/来源/ID/推荐内存信息并尝试获取整合包描述。 /// + /// 整合包推荐的启动内存(单位 MB);0 表示未声明。 private static void _FinalizeInstance(string versionFolder, string fileAddress, string logo, string modpackSource, - string modpackVersion, string resourceId) + string modpackVersion, string resourceId, int recommendedRam = 0) { // 设置图标 if (logo is not null) @@ -329,6 +330,7 @@ private static void _FinalizeInstance(string versionFolder, string fileAddress, if (modpackVersion is not null) States.Instance.ModpackVersion[versionFolder] = modpackVersion; States.Instance.ModpackSource[versionFolder] = modpackSource; States.Instance.ModpackId[versionFolder] = resourceId; + States.Instance.ModpackRam[versionFolder] = recommendedRam; // 0 表示未声明推荐内存 do { try diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml index 700b96a6f5..ab62280e4a 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml @@ -104,6 +104,7 @@ + @@ -121,25 +122,30 @@ + + 0; // 服务器 TextServerEnter.Text = Config.Instance.ServerToEnter[PageInstanceLeft.McInstance.PathInstance]; @@ -445,13 +448,24 @@ public static double GetRam(McInstance version, bool? is32BitJava = default) if (Config.Instance.MemorySolution[instancePath] == 2) return PageSetupLaunch.GetRam(version, true, is32BitJava); + // 使用整合包推荐内存:按整合包清单声明的推荐内存分配(单位 MB → GB) + if (Config.Instance.MemorySolution[instancePath] == 3 && States.Instance.ModpackRam[instancePath] > 0) + { + var ramRecommended = States.Instance.ModpackRam[instancePath] / 1024d; + // 若使用 32 位 Java,则限制为 1G + if (is32BitJava ?? !ModJava.IsGameSet64BitJava(PageInstanceLeft.McInstance)) + ramRecommended = Math.Min(1d, ramRecommended); + return ramRecommended; + } + // ------------------------------------------ // 修改下方代码时需要一并修改 PageSetupLaunch // ------------------------------------------ // 使用当前实例的设置 var ramGive = default(double); - if (Config.Instance.MemorySolution[instancePath] == 0) + // 推荐内存模式未声明推荐值时回退到自动配置 + if (Config.Instance.MemorySolution[instancePath] == 0 || Config.Instance.MemorySolution[instancePath] == 3) { // 自动配置 var ramAvailable = From f08b1924362a797abf021aa47d1af22b0c3f16a8 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Mon, 10 Aug 2026 19:02:36 +0800 Subject: [PATCH 10/19] =?UTF-8?q?imp:=E6=95=B4=E5=90=88=E5=8C=85=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E5=86=85=E5=AD=98=E5=8A=9F=E8=83=BD=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Localization/Languages/en-US.xaml | 8 + .../App/Localization/Languages/zh-CN.xaml | 8 + .../Modules/Minecraft/ModLaunch.cs | 25 +++ .../Modules/Minecraft/ModModpack.Container.cs | 151 ++++++++++++------ .../Minecraft/ModModpack.CurseForge.cs | 56 +++++-- .../PageInstance/PageInstanceSetup.xaml.cs | 7 +- 6 files changed, 190 insertions(+), 65 deletions(-) diff --git a/PCL.Core/App/Localization/Languages/en-US.xaml b/PCL.Core/App/Localization/Languages/en-US.xaml index db22670084..53b8090b42 100644 --- a/PCL.Core/App/Localization/Languages/en-US.xaml +++ b/PCL.Core/App/Localization/Languages/en-US.xaml @@ -1997,6 +1997,10 @@ PCL CE cannot handle .rar archive files. Please extract and re-archive them in .zip format and try again! This modpack recommends {0} GiB of memory. Use the modpack's recommended memory? Modpack recommended memory + The modpack author's recommended memory may not suit your computer. Auto-configuration is recommended. + Modpack recommended memory + Keep recommended + Use auto-config Next, select an empty folder. The modpack will be installed there. Select installation target (must be an empty folder) Some mod versions in the modpack have been deleted by the mod authors, so installation cannot continue. Please report this issue to the modpack author. @@ -2282,6 +2286,10 @@ This instance requires Microsoft login. Please use a Microsoft login profile! This instance requires Microsoft or third-party login. Please use a matching profile! This instance requires third-party login. Please use a third-party login profile! + The modpack author's recommended memory may not suit your computer. Switch to auto-configuration? + Modpack recommended memory + Switch to auto-config + Keep recommended diff --git a/PCL.Core/App/Localization/Languages/zh-CN.xaml b/PCL.Core/App/Localization/Languages/zh-CN.xaml index 18e3ad468e..819a89cd0e 100644 --- a/PCL.Core/App/Localization/Languages/zh-CN.xaml +++ b/PCL.Core/App/Localization/Languages/zh-CN.xaml @@ -1997,6 +1997,10 @@ PCL CE 无法处理 rar 格式的压缩包,请在解压后重新压缩为 zip 格式再试 该整合包有推荐内存 {0} GiB,是否使用整合包推荐的内存? 整合包推荐内存 + 该整合包有推荐内存 {0} GiB,可能不适合你的电脑,建议使用自动配置 + 整合包推荐内存 + 继续用整合包的推荐内存 + 使用自动配置 接下来请选择一个空文件夹,它会被安装到这个文件夹里。 选择安装目标(必须是一个空文件夹) 整合包中的部分模组版本已被模组作者删除,所以没法继续安装了,请向整合包作者反馈该问题 @@ -2282,6 +2286,10 @@ 当前实例要求使用正版验证,请使用正版验证档案启动游戏! 当前实例要求使用正版验证或第三方验证,请使用符合要求的档案启动游戏! 当前实例要求使用第三方验证,请使用第三方验证档案启动游戏! + 整合包作者的推荐内存可能不适合你的电脑,是否切换自动配置? + 整合包推荐内存 + 切换自动配置 + 继续用整合包的推荐内存 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs index bac9cad01e..4055cce51c 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.cs @@ -88,6 +88,31 @@ private static void McLaunchPrecheck() if (!string.IsNullOrEmpty(checkResult)) throw new ArgumentException(checkResult); + // 检查整合包推荐内存是否适合当前剩余内存:不足时提醒用户切换自动配置 + if (Config.Instance.MemorySolution[ModInstanceList.McMcInstanceSelected.PathInstance] == 3) + { + var recommendedRamMb = States.Instance.ModpackRam[ModInstanceList.McMcInstanceSelected.PathInstance]; + if (recommendedRamMb > 0) + { + var availableMb = KernelInterop.GetAvailablePhysicalMemoryBytes() / 1024d / 1024d; + // 剩余内存*80% 仍不足推荐值,或分配推荐内存后剩余不足 2GB + if (availableMb * 0.8d < recommendedRamMb || availableMb - recommendedRamMb < 2d * 1024d) + { + var switchToAuto = ModMain.MyMsgBox( + Lang.Text("Minecraft.Launch.ModpackRecommendedRamUnfit.Message"), + Lang.Text("Minecraft.Launch.ModpackRecommendedRamUnfit.Title"), + Lang.Text("Minecraft.Launch.ModpackRecommendedRamUnfit.SwitchAuto"), + Lang.Text("Minecraft.Launch.ModpackRecommendedRamUnfit.KeepRecommended")) == 1; + if (switchToAuto) + { + // 切换为自动配置并持久化,本次启动后续读取内存时即生效 + ModBase.RunInUiWait(() => Config.Instance.MemorySolution[ModInstanceList.McMcInstanceSelected.PathInstance] = 0); + ModBase.Log("[Launch] 整合包推荐内存可能不适合当前环境,已切换为自动配置"); + } + } + } + } + #if BETA if (currentLaunchOptions?.SaveBatch is null) // 保存脚本时不提示 { diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs index ab0e5a5ef9..868ce6d957 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -268,6 +268,8 @@ private static LoaderCombo _InstallCompress(string sourcePath, IModpackA /// /// 将懒人包解压后的 .minecraft 实例内容安装到当前游戏文件夹的 versions\{实例名} 下。 /// 仅复制实例自身内容(版本配置、mods/config/saves 等),共享目录(libraries/assets 等)由启动器在运行时自动补全。 + /// 先复制根目录内容,再复制实例目录(版本文件夹)内容,两者同名文件统一走“校验+复制”, + /// 内容不同时弹窗让用户决定(默认覆盖,即实例目录内容优先)。 /// private static void _InstallLazyPackInstance(string installTemp, string archiveBaseFolder, string packVersionName, string instanceName) @@ -275,8 +277,24 @@ private static void _InstallLazyPackInstance(string installTemp, string archiveB var mcRoot = Path.Combine(installTemp, archiveBaseFolder.TrimEnd('\\')); var instanceFolder = _GetVersionFolder(instanceName); Directory.CreateDirectory(instanceFolder); + // 本次安装的冲突处理状态(“全部覆盖 / 全部跳过”在两个复制阶段间保持) + var conflictState = new CopyConflictState(); - // 复制版本文件夹全部内容(json/jar 改名并修正 id,其余文件如 mods/config 一并复制,兼容隔离与非隔离两种布局) + // 先复制 .minecraft 根目录下的实例内容(mods/config/saves/options.txt 等),跳过共享目录 + foreach (var item in Directory.GetFileSystemEntries(mcRoot)) + { + var name = Path.GetFileName(item); + if (_LazyPackSharedFolders.Contains(name)) + continue; + var dest = Path.Combine(instanceFolder, name); + if (Directory.Exists(item)) + _CopyDirectoryWithConflict(item, dest, conflictState, overwriteAsDefault: true); + else + _CopyFileWithConflict(item, dest, name, conflictState, overwriteAsDefault: true); + } + + // 再复制版本文件夹全部内容(json/jar 改名并修正 id,其余文件如 mods/config 一并复制,兼容隔离与非隔离两种布局)。 + // 与根目录同名且内容不同的文件弹窗让用户决定,默认覆盖(实例目录内容优先)。 var versionDir = Path.Combine(mcRoot, "versions", packVersionName); if (Directory.Exists(versionDir)) foreach (var item in Directory.GetFileSystemEntries(versionDir)) @@ -295,32 +313,21 @@ private static void _InstallLazyPackInstance(string installTemp, string archiveB } else if (Directory.Exists(item)) { - ModBase.CopyDirectory(item, Path.Combine(instanceFolder, name), null); + _CopyDirectoryWithConflict(item, Path.Combine(instanceFolder, name), conflictState, + overwriteAsDefault: true); } else { - ModBase.CopyFile(item, Path.Combine(instanceFolder, name)); + _CopyFileWithConflict(item, Path.Combine(instanceFolder, name), name, conflictState, + overwriteAsDefault: true); } } - // 复制 .minecraft 根目录下的实例内容(mods/config/saves/options.txt 等),跳过共享目录 - foreach (var item in Directory.GetFileSystemEntries(mcRoot)) - { - var name = Path.GetFileName(item); - if (_LazyPackSharedFolders.Contains(name)) - continue; - var dest = Path.Combine(instanceFolder, name); - if (Directory.Exists(item)) - ModBase.CopyDirectory(item, dest, null); - else - ModBase.CopyFile(item, dest); - } - // 复制共享目录(libraries/assets)到游戏文件夹根目录,避免重复下载并保留整合包的特殊依赖版本; - // 同名文件内容不同时由用户决定跳过还是覆盖。 - _CopySharedFolderWithConflict(Path.Combine(mcRoot, "libraries"), + // 同名文件内容不同时由用户决定跳过还是覆盖(默认跳过)。 + _CopyDirectoryWithConflict(Path.Combine(mcRoot, "libraries"), Path.Combine(ModFolder.mcFolderSelected, "libraries")); - _CopySharedFolderWithConflict(Path.Combine(mcRoot, "assets"), + _CopyDirectoryWithConflict(Path.Combine(mcRoot, "assets"), Path.Combine(ModFolder.mcFolderSelected, "assets")); // 开启版本隔离 @@ -331,50 +338,94 @@ private static void _InstallLazyPackInstance(string installTemp, string archiveB } /// - /// 复制共享目录(如 libraries/assets)到游戏文件夹根目录。 - /// 同名文件先比较内容:内容相同则跳过;内容不同时弹窗让用户选择跳过或覆盖(可全部跳过/全部覆盖,默认跳过)。 + /// 复制一个目录到目标目录(含子目录),同名文件先比较内容(相同则跳过); + /// 内容不同时弹窗让用户选择(跳过 / 覆盖 / 全部覆盖 / 全部跳过)。 /// - private static void _CopySharedFolderWithConflict(string sourceFolder, string destFolder) + /// 源目录。 + /// 目标目录。 + /// 本次复制操作的冲突处理状态;未提供时本次复制独立新建。 + /// 弹窗默认选项是否为“覆盖”;false 时默认“跳过”。 + private static void _CopyDirectoryWithConflict(string sourceFolder, string destFolder, CopyConflictState? state = null, + bool overwriteAsDefault = false) { + state ??= new CopyConflictState(); if (!Directory.Exists(sourceFolder)) return; Directory.CreateDirectory(destFolder); - var overwriteAll = false; - var skipAll = false; foreach (var sourceFile in Directory.EnumerateFiles(sourceFolder, "*", SearchOption.AllDirectories)) { var relativePath = Path.GetRelativePath(sourceFolder, sourceFile); - var destFile = Path.Combine(destFolder, relativePath); - if (File.Exists(destFile)) + _CopyFileWithConflict(sourceFile, Path.Combine(destFolder, relativePath), relativePath, state, + overwriteAsDefault); + } + } + + /// + /// 复制单个文件到目标路径,同名文件先比较内容(相同则跳过); + /// 内容不同时弹窗让用户选择(跳过 / 覆盖 / 全部覆盖 / 全部跳过)。 + /// 弹窗默认选项由 决定:MyMsgText 会高亮第一个按钮,因此默认按钮放在第一位。 + /// + /// 源文件。 + /// 目标文件。 + /// 弹窗提示中显示的文件路径;为 null 时显示文件名。 + /// 本次复制操作的冲突处理状态。 + /// 弹窗默认选项是否为“覆盖”;false 时默认“跳过”。 + private static void _CopyFileWithConflict(string sourceFile, string destFile, string displayPath, + CopyConflictState state, bool overwriteAsDefault = false) + { + if (File.Exists(destFile)) + { + // 同名文件:先比较内容,相同则无需处理 + if (_FilesEqual(sourceFile, destFile)) + return; + if (state.SkipAll) + return; + if (!state.OverwriteAll) { - // 同名文件:先比较内容,相同则无需处理 - if (_FilesEqual(sourceFile, destFile)) - continue; - if (skipAll) - continue; - if (!overwriteAll) + var choice = ModMain.MyMsgBox( + Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.Message", + displayPath ?? Path.GetFileName(sourceFile)), + Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.Title"), + overwriteAsDefault + ? Lang.Text("Common.Action.Overwrite") + : Lang.Text("Minecraft.Download.Modpack.OptionalFile.Skip"), + overwriteAsDefault + ? Lang.Text("Minecraft.Download.Modpack.OptionalFile.Skip") + : Lang.Text("Common.Action.Overwrite"), + Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.OverwriteAll"), + highLight: true, forceWait: true, + button4: Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.SkipAll")); + switch (choice) { - var choice = ModMain.MyMsgBox( - Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.Message", relativePath), - Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.Title"), - Lang.Text("Minecraft.Download.Modpack.OptionalFile.Skip"), - Lang.Text("Common.Action.Overwrite"), - Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.OverwriteAll"), - highLight: true, forceWait: true, - button4: Lang.Text("Minecraft.Download.Modpack.OverwriteConfirm.SkipAll")); - switch (choice) - { - case 1: continue; // 跳过 - case 2: break; // 覆盖 - case 3: overwriteAll = true; break; // 全部覆盖 - case 4: skipAll = true; continue; // 全部跳过 - } + case 1: // 第一个按钮:覆盖(overwriteAsDefault)或跳过(非 overwriteAsDefault) + if (!overwriteAsDefault) + return; + break; + case 2: // 第二个按钮:跳过(overwriteAsDefault)或覆盖(非 overwriteAsDefault) + if (overwriteAsDefault) + return; + break; + case 3: // 全部覆盖 + state.OverwriteAll = true; + break; + case 4: // 全部跳过 + state.SkipAll = true; + return; } } - - Directory.CreateDirectory(Path.GetDirectoryName(destFile) ?? ""); - File.Copy(sourceFile, destFile, true); } + + Directory.CreateDirectory(Path.GetDirectoryName(destFile) ?? ""); + File.Copy(sourceFile, destFile, true); + } + + /// + /// 一次复制操作中“全部覆盖 / 全部跳过”的持久状态。 + /// + private sealed class CopyConflictState + { + public bool OverwriteAll; + public bool SkipAll; } /// diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs index b16611864a..dd726e272d 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs @@ -6,6 +6,7 @@ using PCL.Core.Minecraft.Modpack; using PCL.Core.UI; using PCL.Core.Utils; +using PCL.Core.Utils.OS; using PCL.Network; using PCL.Network.Loaders; using static PCL.ModLoader; @@ -39,16 +40,45 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac if (instanceName is null) instanceName = _PromptInstanceName(manifest.Name ?? ""); - // 推荐内存询问:清单声明了推荐内存时,询问是否使用 - // (确定 → 使用推荐内存模式;取消 → 跟随全局设置,即默认选项) - var useRecommendedRam = false; - if (manifest.RecommendedRamEffective is > 0) - useRecommendedRam = ModMain.MyMsgBox( - Lang.Text("Minecraft.Download.Modpack.RecommendedRam.Message", - (manifest.RecommendedRamEffective.Value / 1024d).ToString("0.#")), - Lang.Text("Minecraft.Download.Modpack.RecommendedRam.Title"), - Lang.Text("Common.Action.Confirm"), - Lang.Text("Common.Action.Cancel")) == 1; + // 推荐内存询问:清单声明了推荐内存时,先校验其是否适合用户的电脑 + // (使用推荐 → 推荐内存模式;取消 → 跟随全局设置;不适合时自动 → 自动配置) + var recommendedRam = manifest.RecommendedRamEffective ?? 0; + var useRecommendedRam = false; // 最终是否使用推荐内存模式 + var useAutoRam = false; // 最终是否使用自动配置模式(推荐内存不适合时) + if (recommendedRam > 0) + { + var totalMemoryMb = KernelInterop.GetPhysicalMemoryBytes().Total / 1024d / 1024d; + if (recommendedRam > totalMemoryMb) + { + useAutoRam = true; + } + else + { + // 阈值 = max(总内存*80%, 总内存-6GB),推荐内存超过阈值时警告 + var threshold = Math.Max(totalMemoryMb * 0.8d, totalMemoryMb - 6d * 1024d); + if (recommendedRam > threshold) + { + // 推荐内存不合理 + useRecommendedRam = ModMain.MyMsgBox( + Lang.Text("Minecraft.Download.Modpack.RecommendedRamUnfit.Message",(recommendedRam / 1024d).ToString("0.#")), + Lang.Text("Minecraft.Download.Modpack.RecommendedRamUnfit.Title"), + Lang.Text("Minecraft.Download.Modpack.RecommendedRamUnfit.UseAuto"), + Lang.Text("Minecraft.Download.Modpack.RecommendedRamUnfit.KeepRecommended")) == 1; + if (!useRecommendedRam) + useAutoRam = true; + } + else + { + // 推荐内存合理 + useRecommendedRam = ModMain.MyMsgBox( + Lang.Text("Minecraft.Download.Modpack.RecommendedRam.Message", + (recommendedRam / 1024d).ToString("0.#")), + Lang.Text("Minecraft.Download.Modpack.RecommendedRam.Title"), + Lang.Text("Common.Action.Confirm"), + Lang.Text("Common.Action.Cancel")) == 1; + } + } + } // 获取 Mod API 版本信息 string forgeVersion = null; @@ -311,12 +341,12 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac _FinalizeInstance(versionFolder, sourcePath, logo, "CurseForge", manifest.Version, resourceId, manifest.RecommendedRamEffective ?? 0); // 应用推荐内存选择:仅当清单声明了推荐内存时写配置 - // (确定 → 使用推荐内存模式;取消 → 跟随全局设置,即默认选项) + // (使用推荐 → 推荐内存模式;不适合时自动 → 自动配置;正常取消 → 跟随全局设置) // MemorySolution 注册了 UI 观察者(ModSetup.VersionRamType),需在 UI 线程写入, // 避免在后台安装线程触发观察者导致跨线程访问 UI 控件。 - if (manifest.RecommendedRamEffective is > 0) + if (recommendedRam > 0) { - var memorySolution = useRecommendedRam ? 3 : 2; + var memorySolution = useRecommendedRam ? 3 : (useAutoRam ? 0 : 2); ModBase.RunInUi(() => Config.Instance.MemorySolution[versionFolder] = memorySolution); } // 本地安装没有外部 logo 时,尝试使用整合包内嵌的图标(manifest.json 的 image 字段) diff --git a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.cs b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.cs index 28a95689c4..65447ee413 100644 --- a/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.cs +++ b/Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.cs @@ -109,8 +109,11 @@ public void Reload() ((MyRadioBox)FindName("RadioRamType" + ramType)).Checked = true; SliderRamCustom.Value = Config.Instance.CustomMemorySize[PageInstanceLeft.McInstance.PathInstance]; RamType(ramType); - // 整合包未声明推荐内存时禁用「使用整合包推荐内存」选项 - RadioRamType3.IsEnabled = States.Instance.ModpackRam[PageInstanceLeft.McInstance.PathInstance] > 0; + // 整合包未声明推荐内存、或推荐内存大于本机物理内存时禁用「使用整合包推荐内存」选项 + // (推荐内存超过物理内存说明该推荐值不适合本机,需等物理内存变大后才可选) + var modpackRam = States.Instance.ModpackRam[PageInstanceLeft.McInstance.PathInstance]; + RadioRamType3.IsEnabled = modpackRam > 0 && + modpackRam <= KernelInterop.GetPhysicalMemoryBytes().Total / 1024d / 1024d; // 服务器 TextServerEnter.Text = Config.Instance.ServerToEnter[PageInstanceLeft.McInstance.PathInstance]; From 921205b28d9e55f7c1703c6e48ea5366e489a679 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Mon, 10 Aug 2026 22:39:26 +0800 Subject: [PATCH 11/19] =?UTF-8?q?fix:=E6=95=B4=E5=90=88=E5=8C=85=E6=8E=A8?= =?UTF-8?q?=E8=8D=90=E5=86=85=E5=AD=98=E4=B8=8D=E5=90=88=E7=90=86=E6=97=B6?= =?UTF-8?q?MyMsgBox=E7=9A=84=E9=80=BB=E8=BE=91=E6=98=AF=E5=8F=8D=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PCL.Core/App/Localization/Languages/en-US.xaml | 2 +- .../Modules/Minecraft/ModModpack.CurseForge.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PCL.Core/App/Localization/Languages/en-US.xaml b/PCL.Core/App/Localization/Languages/en-US.xaml index 53b8090b42..9997663bfe 100644 --- a/PCL.Core/App/Localization/Languages/en-US.xaml +++ b/PCL.Core/App/Localization/Languages/en-US.xaml @@ -1997,7 +1997,7 @@ PCL CE cannot handle .rar archive files. Please extract and re-archive them in .zip format and try again! This modpack recommends {0} GiB of memory. Use the modpack's recommended memory? Modpack recommended memory - The modpack author's recommended memory may not suit your computer. Auto-configuration is recommended. + This modpack recommends {0} GiB of memory, which may not suit your computer. Auto-configuration is recommended. Modpack recommended memory Keep recommended Use auto-config diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs index dd726e272d..258f65add5 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs @@ -63,7 +63,7 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac Lang.Text("Minecraft.Download.Modpack.RecommendedRamUnfit.Message",(recommendedRam / 1024d).ToString("0.#")), Lang.Text("Minecraft.Download.Modpack.RecommendedRamUnfit.Title"), Lang.Text("Minecraft.Download.Modpack.RecommendedRamUnfit.UseAuto"), - Lang.Text("Minecraft.Download.Modpack.RecommendedRamUnfit.KeepRecommended")) == 1; + Lang.Text("Minecraft.Download.Modpack.RecommendedRamUnfit.KeepRecommended")) == 2; if (!useRecommendedRam) useAutoRam = true; } From b8244f462948465ff847e45f5f1f2ada02a01c06 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Tue, 11 Aug 2026 18:56:35 +0800 Subject: [PATCH 12/19] =?UTF-8?q?fix:=E5=AF=B9=E4=BA=8E=E5=B5=8C=E5=A5=97?= =?UTF-8?q?=E6=95=B4=E5=90=88=E5=8C=85=E4=BD=BF=E7=94=A8FileInfo(sourcePat?= =?UTF-8?q?h).Length=E4=BC=9A=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Minecraft/ModModpack.Container.cs | 2 +- .../Minecraft/ModModpack.CurseForge.cs | 2 +- .../Modules/Minecraft/ModModpack.Hmcl.cs | 2 +- .../Modules/Minecraft/ModModpack.Mcbbs.cs | 2 +- .../Modules/Minecraft/ModModpack.Modrinth.cs | 2 +- .../Modules/Minecraft/ModModpack.MultiMc.cs | 2 +- .../Modules/Minecraft/ModModpack.cs | 19 +++++++++++++++++++ 7 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs index 868ce6d957..e71a6edb28 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -252,7 +252,7 @@ private static LoaderCombo _InstallCompress(string sourcePath, IModpackA ModBase.RunInUi(() => ModMain.frmMain.PageChange(FormMain.PageType.InstanceSelect)); }) { - ProgressWeight = new FileInfo(sourcePath).Length / 1024d / 1024d / 6d + ProgressWeight = _GetModpackProgressWeight(sourcePath) } // 每 6M 需要 1s }) { diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs index 258f65add5..15ae5bc070 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs @@ -140,7 +140,7 @@ private static LoaderCombo _InstallCurseForge(string sourcePath, IModpac _GetVersionFolder(instanceName), task, 0.4d); }) { - ProgressWeight = new FileInfo(sourcePath).Length / 1024d / 1024d / 6d, + ProgressWeight = _GetModpackProgressWeight(sourcePath), block = false }); // 每 6M 需要 1s // 获取 Mod 列表 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs index 88e6901cdc..6b63a194e8 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs @@ -41,7 +41,7 @@ private static LoaderCombo _InstallHmcl(string sourcePath, IModpackArchi Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4d); }) { - ProgressWeight = new FileInfo(sourcePath).Length / 1024d / 1024d / 6d, + ProgressWeight = _GetModpackProgressWeight(sourcePath), block = false }); // 每 6M 需要 1s // 构造游戏本体安装加载器 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs index dc2539a4c9..8df17facce 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs @@ -68,7 +68,7 @@ private static LoaderCombo _InstallMcbbs(string sourcePath, IModpackArch if (manifest.Version is not null) States.Instance.ModpackVersion[versionFolder] = manifest.Version; }); - unzipTask.ProgressWeight = new FileInfo(sourcePath).Length / 1024.0 / 1024.0 / 6.0; // 每 6M 需要 1s + unzipTask.ProgressWeight = _GetModpackProgressWeight(sourcePath); // 每 6M 需要 1s unzipTask.block = false; installLoaders.Add(unzipTask); diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs index 645bbdad4a..a81ffe0017 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs @@ -101,7 +101,7 @@ private static LoaderCombo _InstallModrinth(string sourcePath, IModpackA Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.1d); }) { - ProgressWeight = new FileInfo(sourcePath).Length / 1024d / 1024d / 6d, + ProgressWeight = _GetModpackProgressWeight(sourcePath), block = false }); // 每 6M 需要 1s // 获取下载文件列表 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs index 6ed94c479e..9e68adbb21 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs @@ -96,7 +96,7 @@ private static LoaderCombo _InstallMultiMc(string sourcePath, IModpackAr installTemp, archiveBaseFolder, instanceName); }) { - ProgressWeight = new FileInfo(sourcePath).Length / 1024d / 1024d / 6d, + ProgressWeight = _GetModpackProgressWeight(sourcePath), block = false }); // 每 6M 需要 1s // 构造实例安装请求 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs index 1b060e3a56..f8fecb0f0c 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.cs @@ -211,6 +211,25 @@ private static void _ExtractModpackFiles(string installTemp, string sourcePath, } } + /// + /// 计算整合包源解压阶段的进度权重(预计时间秒)。 + /// 文件来源取文件大小;文件夹来源(内层文件夹形式整合包)取目录下所有文件总大小, + /// + private static double _GetModpackProgressWeight(string sourcePath) + { + try + { + if (Directory.Exists(sourcePath)) + return Directory.EnumerateFiles(sourcePath, "*", SearchOption.AllDirectories) + .Sum(file => new FileInfo(file).Length) / 1024d / 1024d / 6d; + return new FileInfo(sourcePath).Length / 1024d / 1024d / 6d; + } + catch (Exception) + { + return 1d; + } + } + /// /// 从整合包的 override 目录复制文件,同时设置 PCL 的配置文件与版本隔离。 /// 对路径末尾是否为 \ 没有要求。 From 4c8c7cdee1c0817ceb45186488a1fa1b1c39a00e Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Tue, 11 Aug 2026 19:14:08 +0800 Subject: [PATCH 13/19] =?UTF-8?q?fix:=E6=87=92=E4=BA=BA=E5=8C=85jar=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=9C=A8=E5=AE=89=E8=A3=85=E9=87=8D=E5=90=8D?= =?UTF-8?q?=E6=97=B6=E6=94=B9=E5=90=8D=E4=B8=8D=E4=BC=9A=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Minecraft/ModModpack.Container.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs index e71a6edb28..83dffe4eb6 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -293,7 +293,7 @@ private static void _InstallLazyPackInstance(string installTemp, string archiveB _CopyFileWithConflict(item, dest, name, conflictState, overwriteAsDefault: true); } - // 再复制版本文件夹全部内容(json/jar 改名并修正 id,其余文件如 mods/config 一并复制,兼容隔离与非隔离两种布局)。 + // 再复制版本文件夹全部内容(json/jar 改名并修正 id、jar 字段,其余文件如 mods/config 一并复制,兼容隔离与非隔离两种布局)。 // 与根目录同名且内容不同的文件弹窗让用户决定,默认覆盖(实例目录内容优先)。 var versionDir = Path.Combine(mcRoot, "versions", packVersionName); if (Directory.Exists(versionDir)) @@ -304,7 +304,12 @@ private static void _InstallLazyPackInstance(string installTemp, string archiveB { var versionJson = (JsonObject)ModBase.GetJson(ModBase.ReadFile(item)); if (!string.Equals(instanceName, packVersionName, StringComparison.OrdinalIgnoreCase)) + { versionJson["id"] = instanceName; + // jar 字段指向原名时一并改写为实例名 + if (string.Equals(versionJson["jar"]?.ToString(), packVersionName, StringComparison.OrdinalIgnoreCase)) + versionJson["jar"] = instanceName; + } ModBase.WriteFile(Path.Combine(instanceFolder, instanceName + ".json"), versionJson.ToString()); } else if (name.Equals(packVersionName + ".jar", StringComparison.OrdinalIgnoreCase)) From 7ef0a93511815ca640b80eb7c416115493b3b887 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Tue, 11 Aug 2026 19:23:22 +0800 Subject: [PATCH 14/19] =?UTF-8?q?fix:=E6=87=92=E4=BA=BA=E5=8C=85=E7=9A=84.?= =?UTF-8?q?minecraft=E6=96=87=E4=BB=B6=E5=A4=B9=E9=87=8C=E7=9A=84=E4=B8=9C?= =?UTF-8?q?=E8=A5=BF=E5=9C=A8=E6=A0=B9=E7=9B=AE=E5=BD=95=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Minecraft/Modpack/ModpackArchiveDetectorTest.cs | 8 ++++++++ PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs b/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs index b1ec379663..b67172b973 100644 --- a/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs +++ b/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs @@ -105,6 +105,14 @@ public void DetectsLazyPackByMinecraftVersions() Assert.AreEqual(ModpackFormat.LazyPack, result.Format); } + [TestMethod] + public void DetectsRootLayoutLazyPack() + { + var archive = new FakeModpackArchive("versions/1.20.1/1.20.1.json", "mods/Test.jar", "README.txt"); + var result = ModpackArchiveDetector.Detect(archive); + Assert.AreEqual(ModpackFormat.LazyPack, result.Format); + } + [TestMethod] public void ReturnsUnknownForUnrecognizedArchive() { diff --git a/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs b/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs index 7713cf1f29..df764a353a 100644 --- a/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs +++ b/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs @@ -65,9 +65,9 @@ public static ModpackDetection Detect(IModpackArchiveReader archive) } } - // 懒人包:存在 .minecraft/versions/<版本>/<版本>.json 结构 + // 懒人包:存在 versions/<版本>/<版本>.json 结构。 foreach (var entryName in archive.EntryNames) - if (RegexPatterns.ModpackLazyInstance.Match(entryName).Success) + if (RegexPatterns.ModpackLazyInstance.Match("/" + entryName).Success) return new ModpackDetection(ModpackFormat.LazyPack, ""); // 嵌套整合包:更深的层级(深度 ≥ 3)存在已知整合包标记,说明压缩包内还打包了其他内容。 From 6209d9209d3aa54067277e8bbc9d89d5159c51d0 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Thu, 13 Aug 2026 11:29:28 +0800 Subject: [PATCH 15/19] =?UTF-8?q?fix:=E6=87=92=E4=BA=BA=E5=8C=85=E7=9A=84?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E7=BB=A7=E6=89=BF=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Minecraft/ModModpack.Container.cs | 115 ++++++++++++------ 1 file changed, 79 insertions(+), 36 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs index 83dffe4eb6..31be8d3370 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -267,9 +267,6 @@ private static LoaderCombo _InstallCompress(string sourcePath, IModpackA /// /// 将懒人包解压后的 .minecraft 实例内容安装到当前游戏文件夹的 versions\{实例名} 下。 - /// 仅复制实例自身内容(版本配置、mods/config/saves 等),共享目录(libraries/assets 等)由启动器在运行时自动补全。 - /// 先复制根目录内容,再复制实例目录(版本文件夹)内容,两者同名文件统一走“校验+复制”, - /// 内容不同时弹窗让用户决定(默认覆盖,即实例目录内容优先)。 /// private static void _InstallLazyPackInstance(string installTemp, string archiveBaseFolder, string packVersionName, string instanceName) @@ -292,41 +289,43 @@ private static void _InstallLazyPackInstance(string installTemp, string archiveB else _CopyFileWithConflict(item, dest, name, conflictState, overwriteAsDefault: true); } - - // 再复制版本文件夹全部内容(json/jar 改名并修正 id、jar 字段,其余文件如 mods/config 一并复制,兼容隔离与非隔离两种布局)。 - // 与根目录同名且内容不同的文件弹窗让用户决定,默认覆盖(实例目录内容优先)。 - var versionDir = Path.Combine(mcRoot, "versions", packVersionName); - if (Directory.Exists(versionDir)) - foreach (var item in Directory.GetFileSystemEntries(versionDir)) + var bundledVersionsRoot = Path.Combine(mcRoot, "versions"); + var bundledVersionFolders = Directory.Exists(bundledVersionsRoot) + ? Directory.GetDirectories(bundledVersionsRoot) + .Where(dir => File.Exists(Path.Combine(dir, Path.GetFileName(dir) + ".json"))).ToList() + : new List(); + var pclVersionsRoot = Path.Combine(ModFolder.mcFolderSelected, "versions"); + var usedNames = new HashSet(StringComparer.OrdinalIgnoreCase); + if (Directory.Exists(pclVersionsRoot)) + foreach (var existing in Directory.GetDirectories(pclVersionsRoot)) + usedNames.Add(Path.GetFileName(existing)); + usedNames.Add(instanceName); + var nameMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var versionFolder in bundledVersionFolders) + { + var sourceName = Path.GetFileName(versionFolder); + string destName; + if (sourceName.Equals(packVersionName, StringComparison.OrdinalIgnoreCase)) + destName = instanceName; + else if (usedNames.Add(sourceName)) + destName = sourceName; + else { - var name = Path.GetFileName(item); - if (name.Equals(packVersionName + ".json", StringComparison.OrdinalIgnoreCase)) - { - var versionJson = (JsonObject)ModBase.GetJson(ModBase.ReadFile(item)); - if (!string.Equals(instanceName, packVersionName, StringComparison.OrdinalIgnoreCase)) - { - versionJson["id"] = instanceName; - // jar 字段指向原名时一并改写为实例名 - if (string.Equals(versionJson["jar"]?.ToString(), packVersionName, StringComparison.OrdinalIgnoreCase)) - versionJson["jar"] = instanceName; - } - ModBase.WriteFile(Path.Combine(instanceFolder, instanceName + ".json"), versionJson.ToString()); - } - else if (name.Equals(packVersionName + ".jar", StringComparison.OrdinalIgnoreCase)) - { - ModBase.CopyFile(item, Path.Combine(instanceFolder, instanceName + ".jar")); - } - else if (Directory.Exists(item)) - { - _CopyDirectoryWithConflict(item, Path.Combine(instanceFolder, name), conflictState, - overwriteAsDefault: true); - } - else - { - _CopyFileWithConflict(item, Path.Combine(instanceFolder, name), name, conflictState, - overwriteAsDefault: true); - } + destName = sourceName; + var suffix = 1; + while (!usedNames.Add(destName + "-PCL" + (suffix == 1 ? "" : suffix.ToString()))) + suffix++; + destName += "-PCL" + (suffix == 1 ? "" : suffix.ToString()); } + nameMap[sourceName] = destName; + } + // 按目标名逐一复制,json/jar 引用随 nameMap 一并改写 + foreach (var versionFolder in bundledVersionFolders) + { + var sourceName = Path.GetFileName(versionFolder); + var destName = nameMap[sourceName]; + _CopyBundledVersion(versionFolder, Path.Combine(pclVersionsRoot, destName), sourceName, destName, nameMap); + } // 复制共享目录(libraries/assets)到游戏文件夹根目录,避免重复下载并保留整合包的特殊依赖版本; // 同名文件内容不同时由用户决定跳过还是覆盖(默认跳过)。 @@ -342,6 +341,50 @@ private static void _InstallLazyPackInstance(string installTemp, string archiveB ModBase.IniClearCache(versionIni); } + /// + /// 将懒人包内的单个版本文件夹完整复制到目标版本文件夹。 + /// 源名与目标名不同时(主实例改名、或与已有实例重名自动改名),json/jar 文件同步改名, + /// 并修正 json 的 id / jar / inheritsFrom 字段;继承引用按 + /// 一并改写,保证改名后的继承链仍能正确解析。 + /// + /// 包内源版本文件夹 + /// PCL 中目标版本文件夹 + /// 源版本名 + /// 目标版本名 + /// 包内原版本名到目标名的映射,用于改写继承引用 + private static void _CopyBundledVersion(string sourceFolder, string destFolder, string sourceName, string destName, + IReadOnlyDictionary nameMap) + { + Directory.CreateDirectory(destFolder); + foreach (var item in Directory.GetFileSystemEntries(sourceFolder)) + { + var itemName = Path.GetFileName(item); + if (itemName.Equals(sourceName + ".json", StringComparison.OrdinalIgnoreCase)) + { + var json = (JsonObject)ModBase.GetJson(ModBase.ReadFile(item)); + json["id"] = destName; + if (json["jar"]?.ToString() is string jar && nameMap.TryGetValue(jar, out var newJar)) + json["jar"] = newJar; + if (json["inheritsFrom"]?.ToString() is string inherit && + nameMap.TryGetValue(inherit, out var newInherit)) + json["inheritsFrom"] = newInherit; + ModBase.WriteFile(Path.Combine(destFolder, destName + ".json"), json.ToString()); + } + else if (itemName.Equals(sourceName + ".jar", StringComparison.OrdinalIgnoreCase)) + { + ModBase.CopyFile(item, Path.Combine(destFolder, destName + ".jar")); + } + else if (Directory.Exists(item)) + { + ModBase.CopyDirectory(item, Path.Combine(destFolder, itemName)); + } + else + { + ModBase.CopyFile(item, Path.Combine(destFolder, itemName)); + } + } + } + /// /// 复制一个目录到目标目录(含子目录),同名文件先比较内容(相同则跳过); /// 内容不同时弹窗让用户选择(跳过 / 覆盖 / 全部覆盖 / 全部跳过)。 From b61f312588732d15af5a80b00c349c7c7b57e29a Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Mon, 17 Aug 2026 21:43:42 +0800 Subject: [PATCH 16/19] =?UTF-8?q?fix:mcbbs=E6=95=B4=E5=90=88=E5=8C=85json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs b/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs index df764a353a..dc25ac8995 100644 --- a/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs +++ b/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs @@ -105,7 +105,7 @@ private static bool _HasAddons(string jsonText) { try { - var node = JsonNode.Parse(jsonText); + var node = JsonCompat.ParseNode(jsonText); return node?["addons"] is not null; } catch (JsonException) @@ -122,7 +122,7 @@ private static bool _HasMinecraft(string jsonText) { try { - var node = JsonNode.Parse(jsonText); + var node = JsonCompat.ParseNode(jsonText); return node?["minecraft"] is not null; } catch (JsonException) From e7a53e32dc4ffa099c17cfc9a495713ca8bfdefd Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Tue, 18 Aug 2026 10:39:43 +0800 Subject: [PATCH 17/19] =?UTF-8?q?fix:Modrinth=20=E6=95=B4=E5=90=88?= =?UTF-8?q?=E5=8C=85=E7=9A=84zip=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Minecraft/ModModpack.Modrinth.cs | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs index a81ffe0017..82b6dbd779 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs @@ -220,20 +220,22 @@ private static LoaderCombo _InstallModrinth(string sourcePath, IModpackA } /// - /// 判断 Modrinth 文件项的目标是否是一个文件夹(文件名无扩展名)。 + /// 判断 Modrinth 文件项的目标是否是一个文件夹(path 以斜杠结尾,或位于 saves 目录下)。 /// 若是,下载内容应为需解压的压缩包(通常用于地图存档等文件夹形式的内容)。 /// private static bool _IsDirectoryDownload(ModrinthFile file) { var path = file.Path ?? ""; - var trimmed = path.TrimEnd('/', '\\'); - return !string.IsNullOrEmpty(trimmed) && !Path.GetFileName(trimmed).Contains("."); + if (path.EndsWith("/") || path.EndsWith("\\")) + return true; + var normalizedDir = Path.GetDirectoryName(path.TrimEnd('/', '\\'))?.Replace("\\", "/").TrimEnd('/'); + return normalizedDir != null && normalizedDir.EndsWith("/saves", StringComparison.OrdinalIgnoreCase); } /// /// 将下载的压缩包解压到目标文件夹(用于地图存档等目录形式的下载项)。 /// 解压到临时暂存目录后,若存在单层嵌套的存档根目录(zip 内再包一层文件夹)则直接使用该目录; - /// 内容不是压缩包时回退为按普通文件复制。 + /// 不是有效的 Minecraft 存档(没有 level.dat)或非压缩包时回退为按普通文件复制。 /// private static void _ExtractArchiveToDirectory(string archivePath, string targetDirectory) { @@ -243,14 +245,27 @@ private static void _ExtractArchiveToDirectory(string archivePath, string target ModBase.DeleteDirectory(staging); Directory.CreateDirectory(staging); ModBase.ExtractFile(archivePath, staging, Encoding.UTF8, null); - var saveRoot = SaveImportHelper.GetSaveRootDirectory(staging) ?? staging; - ModBase.CopyDirectory(saveRoot, targetDirectory, null); - ModBase.DeleteDirectory(staging); + var saveRoot = SaveImportHelper.GetSaveRootDirectory(staging); + if (saveRoot is null) + { + ModBase.DeleteDirectory(staging); + if (Directory.Exists(targetDirectory)) + ModBase.DeleteDirectory(targetDirectory, true); + Directory.CreateDirectory(Path.GetDirectoryName(targetDirectory) ?? targetDirectory); + ModBase.CopyFile(archivePath, targetDirectory); + } + else + { + ModBase.CopyDirectory(saveRoot, targetDirectory, null); + ModBase.DeleteDirectory(staging); + } } catch (Exception ex) { // 内容不是可解压的压缩包,改为按普通文件复制到目标路径 ModBase.Log(ex, "整合包下载项按压缩包解压失败,改为直接复制文件:" + archivePath); + if (Directory.Exists(targetDirectory)) + ModBase.DeleteDirectory(targetDirectory, true); Directory.CreateDirectory(Path.GetDirectoryName(targetDirectory) ?? targetDirectory); ModBase.CopyFile(archivePath, targetDirectory); } From 085b1f69cae3c2849b4d8bc7449c46d62bb42f62 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Tue, 18 Aug 2026 12:20:38 +0800 Subject: [PATCH 18/19] =?UTF-8?q?fix:Modrinth=20=E6=95=B4=E5=90=88?= =?UTF-8?q?=E5=8C=85=E7=9A=84zip=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Modules/Minecraft/ModModpack.Modrinth.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs index 82b6dbd779..8332c3d0e1 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs @@ -229,7 +229,9 @@ private static bool _IsDirectoryDownload(ModrinthFile file) if (path.EndsWith("/") || path.EndsWith("\\")) return true; var normalizedDir = Path.GetDirectoryName(path.TrimEnd('/', '\\'))?.Replace("\\", "/").TrimEnd('/'); - return normalizedDir != null && normalizedDir.EndsWith("/saves", StringComparison.OrdinalIgnoreCase); + return normalizedDir != null && + (string.Equals(normalizedDir, "saves", StringComparison.OrdinalIgnoreCase) || + normalizedDir.EndsWith("/saves", StringComparison.OrdinalIgnoreCase)); } /// From 429b4815b17ea1f593d0a641c6e84956afe4d287 Mon Sep 17 00:00:00 2001 From: qsc1918 Date: Tue, 18 Aug 2026 13:25:23 +0800 Subject: [PATCH 19/19] =?UTF-8?q?fix:=E6=87=92=E4=BA=BA=E5=8C=85=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E7=BB=A7=E6=89=BF=E9=93=BE=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Localization/Languages/en-US.xaml | 1 + .../App/Localization/Languages/zh-CN.xaml | 1 + .../Modules/Minecraft/ModModpack.Container.cs | 93 ++++++++++++++++--- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/PCL.Core/App/Localization/Languages/en-US.xaml b/PCL.Core/App/Localization/Languages/en-US.xaml index 9997663bfe..2c1cd7c22c 100644 --- a/PCL.Core/App/Localization/Languages/en-US.xaml +++ b/PCL.Core/App/Localization/Languages/en-US.xaml @@ -2013,6 +2013,7 @@ This modpack version is too old and no longer supported! No suitable file structure was found. This may not be a Minecraft archive. Cannot install modpack, unknown mod loader {0} (version {1})! + Select the primary instance to install A same-named file has different content: {0} Skip or overwrite it? File content conflict Overwrite all diff --git a/PCL.Core/App/Localization/Languages/zh-CN.xaml b/PCL.Core/App/Localization/Languages/zh-CN.xaml index 819a89cd0e..fdaaaccb49 100644 --- a/PCL.Core/App/Localization/Languages/zh-CN.xaml +++ b/PCL.Core/App/Localization/Languages/zh-CN.xaml @@ -2013,6 +2013,7 @@ 该整合包版本过老,已不支持进行安装! 未能找到适合的文件结构,这可能不是一个 MC 压缩包 无法安装整合包,其中出现了未知的模组加载器 {0}(版本为 {1})! + 请选择要安装的主实例 发现同名文件内容不同:{0} 要跳过还是覆盖? 文件内容冲突 全部覆盖 diff --git a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs index 31be8d3370..991800af2f 100644 --- a/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -216,22 +216,24 @@ private static bool _IsModpackArchive(string filePath) private static LoaderCombo _InstallCompress(string sourcePath, IModpackArchiveReader archive) { - // 尝试定位 .minecraft 文件夹:寻找形如 “/versions/XXX/XXX.json” 的路径 - Match match = null; + Match firstMatch = null; + var versionNames = new List(); + var versionNameSet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var entryName in archive.EntryNames) { var entryMatch = RegexPatterns.ModpackLazyInstance.Match("/" + entryName); - if (entryMatch.Success) - { - match = entryMatch; - break; - } + if (!entryMatch.Success) + continue; + firstMatch ??= entryMatch; + var versionName = entryMatch.Groups[1].Value; + if (versionNameSet.Add(versionName)) + versionNames.Add(versionName); } - if (match is null) - throw new Exception(Lang.Text("Minecraft.Download.Modpack.UnknownArchiveStructure")); // 没有匹配 - var archiveBaseFolder = match.Value.Replace("/", @"\").TrimStart('\\'); // 格式例如:包裹文件夹\.minecraft\(最短为空字符串) - var packVersionName = match.Groups[1].Value; + if (firstMatch is null) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.UnknownArchiveStructure")); + var archiveBaseFolder = firstMatch.Value.Replace("/", @"\").TrimStart('\\'); + var packVersionName = _ResolveLazyPackPrimaryVersion(archive, versionNames); ModBase.Log("[ModPack] 检测到懒人包的 .minecraft 根目录:" + archiveBaseFolder + ",命中的实例名:" + packVersionName); // 实例名:与现有实例不冲突时直接用懒人包自己的,冲突时让用户改名 @@ -266,7 +268,74 @@ private static LoaderCombo _InstallCompress(string sourcePath, IModpackA } /// - /// 将懒人包解压后的 .minecraft 实例内容安装到当前游戏文件夹的 versions\{实例名} 下。 + /// 从懒人包内打包的多个版本中确定主实例(整合包本体)。 + /// 通过继承关系推断:不被任何其他版本 inheritsFrom 引用的版本即继承链顶端。 + /// 无法唯一确定时弹窗让用户选择. + /// + /// 压缩包读取器 + /// 版本名列表 + private static string _ResolveLazyPackPrimaryVersion(IModpackArchiveReader archive, IReadOnlyList versionNames) + { + if (versionNames.Count == 0) + return ""; + if (versionNames.Count == 1) + return versionNames[0]; + + var versionSet = new HashSet(versionNames, StringComparer.OrdinalIgnoreCase); + var referenced = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var entryName in archive.EntryNames) + { + var m = RegexPatterns.ModpackLazyInstance.Match("/" + entryName); + if (!m.Success) + continue; + var versionName = m.Groups[1].Value; + if (!versionSet.Contains(versionName)) + continue; + JsonObject json; + try + { + json = (JsonObject)ModBase.GetJson(archive.ReadEntryText(entryName)); + } + catch (Exception ex) + { + ModBase.Log(ex, "[ModPack] 读取懒人包版本 json 失败,跳过继承推断:" + entryName); + continue; + } + if (json["inheritsFrom"]?.ToString() is string inheritsFrom && versionSet.Contains(inheritsFrom)) + referenced.Add(inheritsFrom); + } + + var candidates = versionNames.Where(v => !referenced.Contains(v)).ToList(); + if (candidates.Count == 1) + { + ModBase.Log("[ModPack] 懒人包主实例由继承关系推断为:" + candidates[0]); + return candidates[0]; + } + + return _PromptLazyPackPrimaryVersion(versionNames); + } + + /// + /// 弹出选择框让用户确定懒人包的主实例。 + /// + /// 所有候选版本名。 + private static string _PromptLazyPackPrimaryVersion(IReadOnlyList versionNames) + { + var radioBoxes = versionNames + .Select(name => new MyRadioBox { Text = name }) + .Cast() + .ToList(); + var result = ModMain.MyMsgBoxSelect(radioBoxes, + Lang.Text("Minecraft.Download.Modpack.LazyPack.SelectPrimaryVersion.Title"), + Lang.Text("Common.Action.Confirm"), + Lang.Text("Common.Action.Cancel")); + if (result is null || result.Value < 0 || result.Value >= versionNames.Count) + throw new ModBase.CancelledException(); + return versionNames[result.Value]; + } + + /// + /// 将懒人包解压后的 .minecraft 实例内容安装到当前游戏文件夹的 versions\\{实例名} 下。 /// private static void _InstallLazyPackInstance(string installTemp, string archiveBaseFolder, string packVersionName, string instanceName)