diff --git a/PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs b/PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs new file mode 100644 index 0000000000..5bbe9b6c92 --- /dev/null +++ b/PCL.Core.Test/Minecraft/Modpack/CurseForgeManifestTest.cs @@ -0,0 +1,114 @@ +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", + "recommendedRam": 8192, + "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(8192, manifest.RecommendedRam); + 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 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() + { + 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); + Assert.IsNull(manifest.RecommendedRam); + Assert.IsNull(manifest.RecommendedRamEffective); + } + + [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..021c630639 --- /dev/null +++ b/PCL.Core.Test/Minecraft/Modpack/FakeModpackArchive.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.IO; +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]; + } + + 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.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.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs b/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs new file mode 100644 index 0000000000..b67172b973 --- /dev/null +++ b/PCL.Core.Test/Minecraft/Modpack/ModpackArchiveDetectorTest.cs @@ -0,0 +1,180 @@ +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 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() + { + var archive = new FakeModpackArchive("README.txt", "data/something.dat"); + var result = ModpackArchiveDetector.Detect(archive); + 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() + { + 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..8c4e5945de --- /dev/null +++ b/PCL.Core.Test/Minecraft/Modpack/ModrinthManifestTest.cs @@ -0,0 +1,106 @@ +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 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() + { + Assert.IsNull(ModrinthManifest.Parse(null)); + } +} 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/App/Localization/Languages/en-US.xaml b/PCL.Core/App/Localization/Languages/en-US.xaml index c090e7a13e..2c1cd7c22c 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,12 @@ 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 + 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 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. @@ -2006,6 +2013,11 @@ 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 + Skip all Failed to open the modpack file. The file may be corrupted or in an unsupported archive format. @@ -2275,6 +2287,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 9bae29f93c..fdaaaccb49 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,12 @@ 文件路径校验失败 无法安装整合包:当前启动器已不再支持 Quilt 加载器。 PCL CE 无法处理 rar 格式的压缩包,请在解压后重新压缩为 zip 格式再试 + 该整合包有推荐内存 {0} GiB,是否使用整合包推荐的内存? + 整合包推荐内存 + 该整合包有推荐内存 {0} GiB,可能不适合你的电脑,建议使用自动配置 + 整合包推荐内存 + 继续用整合包的推荐内存 + 使用自动配置 接下来请选择一个空文件夹,它会被安装到这个文件夹里。 选择安装目标(必须是一个空文件夹) 整合包中的部分模组版本已被模组作者删除,所以没法继续安装了,请向整合包作者反馈该问题 @@ -2006,6 +2013,11 @@ 该整合包版本过老,已不支持进行安装! 未能找到适合的文件结构,这可能不是一个 MC 压缩包 无法安装整合包,其中出现了未知的模组加载器 {0}(版本为 {1})! + 请选择要安装的主实例 + 发现同名文件内容不同:{0} 要跳过还是覆盖? + 文件内容冲突 + 全部覆盖 + 全部跳过 打开整合包文件失败,文件可能损坏或为不支持的压缩包格式 @@ -2275,6 +2287,10 @@ 当前实例要求使用正版验证,请使用正版验证档案启动游戏! 当前实例要求使用正版验证或第三方验证,请使用符合要求的档案启动游戏! 当前实例要求使用第三方验证,请使用第三方验证档案启动游戏! + 整合包作者的推荐内存可能不适合你的电脑,是否切换自动配置? + 整合包推荐内存 + 切换自动配置 + 继续用整合包的推荐内存 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 new file mode 100644 index 0000000000..26dce63edb --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/CurseForgeManifest.cs @@ -0,0 +1,141 @@ +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; } + + /// + /// 整合包推荐的启动内存(单位 MB,可选),如 4096 表示 4 GiB;缺失时表示未声明推荐内存。 + /// + public int? RecommendedRam { get; init; } + + /// + /// 有效的推荐内存:根级 recommendedRam 优先,其次 minecraft.recommendedRam; + /// 两者都不存在时表示未声明推荐内存。 + /// + public int? RecommendedRamEffective => RecommendedRam ?? Minecraft?.RecommendedRam; + + /// + /// 作者名称(可选)。 + /// + public string? Author { get; init; } + + /// + /// 整合包图标路径(可选),通常是压缩包内的相对路径,如 profileImage/xxx.jpg。 + /// + public string? Image { 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; } + + /// + /// 推荐内存(单位 MB,可选);部分整合包把 recommendedRam 放在 minecraft 对象内而非根级。 + /// + public int? RecommendedRam { 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..6563a5a11c --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/IModpackArchiveReader.cs @@ -0,0 +1,34 @@ +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); + + /// + /// 将指定条目按原始字节提取到目标文件(用于探测二进制文件,如附带启动器的 exe)。 + /// + /// 条目名。 + /// 目标文件路径。 + void ExtractEntryToFile(string entryName, string destinationPath); +} 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/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs b/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs new file mode 100644 index 0000000000..dc25ac8995 --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/ModpackArchiveDetector.cs @@ -0,0 +1,133 @@ +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); + } + } + + // 懒人包:存在 versions/<版本>/<版本>.json 结构。 + foreach (var entryName in archive.EntryNames) + 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, ""); + } + + /// + /// 判断 manifest.json 是否包含 addons 字段(带 addons 视为 MCBBS 格式)。 + /// 清单无法解析时按不含 addons 处理,交由对应格式的安装流程抛出更具体的错误。 + /// + private static bool _HasAddons(string jsonText) + { + try + { + var node = JsonCompat.ParseNode(jsonText); + return node?["addons"] is not null; + } + catch (JsonException) + { + return false; + } + } + + /// + /// 判断 manifest.json 是否包含 minecraft 字段(CurseForge 整合包清单的必要字段)。 + /// 模组的 manifest.json(如 Fabric 模组)不含该字段,可用于区分嵌套整合包与普通文件。 + /// + private static bool _HasMinecraft(string jsonText) + { + try + { + var node = JsonCompat.ParseNode(jsonText); + return node?["minecraft"] 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..7610c07332 --- /dev/null +++ b/PCL.Core/Minecraft/Modpack/ModrinthManifest.cs @@ -0,0 +1,137 @@ +using System; +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; } + + /// + /// 按键名(大小写不敏感)获取依赖版本,如 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 清单。 + /// + /// 清单 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/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/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 } 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/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 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..68db8213ed --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/FolderModpackArchiveReader.cs @@ -0,0 +1,44 @@ +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)); + } + + 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/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 new file mode 100644 index 0000000000..991800af2f --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Container.cs @@ -0,0 +1,566 @@ +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; +using PCL.Core.UI; +using PCL.Core.Utils; +using static PCL.ModLoader; + +namespace PCL; + +public static partial class ModModpack +{ + #region 带启动器的压缩包 + + /// + /// 懒人包 .minecraft 根目录下属于共享/系统目录、应由启动器在运行时自动补全的内容, + /// 安装时不需要复制进实例文件夹。 + /// + private static readonly HashSet _LazyPackSharedFolders = new(StringComparer.OrdinalIgnoreCase) + { + "versions", "libraries", "assets", "crash-reports", "logs", + "launcher_profiles.json", "usercache.json", "usernamecache.json" + }; + + private static LoaderCombo _InstallLauncherPack(string sourcePath, IModpackArchiveReader archive) + { + // 阶段 0:只探测压缩包是否附带启动器(仅提取顶层 exe 读取产品名,不完整解压) + string bundledLauncherEntry = null; + var probeFolder = ModMain.RequestTaskTempFolder(); + try + { + foreach (var entryName in archive.EntryNames) + { + if (entryName.Contains("/") || !entryName.EndsWithF(".exe", true)) + continue; // 只探测顶层的 *.exe + try + { + 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 Community Edition" || + productName == "Plain Craft Launcher" || + (productName is not null && + (productName.ContainsF("Launcher", true) || productName.ContainsF("启动", true)) && + productName != "Plain Craft Launcher Admin Manager")) //我不知道这个”PCL 管理助手“是什么,原来在重构前的ModModpack.cs第1150行 + { + bundledLauncherEntry = entryName; + break; + } + } + catch (Exception ex) + { + ModBase.Log(ex, "探测压缩包内附带启动器失败:" + entryName); + } + } + } + finally + { + ModBase.DeleteDirectory(probeFolder); + } + + // 询问用户是否换用附带启动器 + 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[] + { + new LoaderTask(Lang.Text("Minecraft.Download.Modpack.Stage.ExtractArchive"), task => + { + _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); + }) + }); + 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) + { + ModBase.Log("[Modpack] 调用内层整合包继续安装:" + innerPackPath); + if (Directory.Exists(innerPackPath)) + _InstallSource(new FolderModpackArchiveReader(innerPackPath), innerPackPath); + else + ModpackInstall(innerPackPath); + } + else + { + HintService.Hint(Lang.Text("Minecraft.Download.Modpack.UnknownArchiveStructure"), HintType.Error); + ModBase.Log("[Modpack] 未在压缩包中找到内层整合包"); + } + }) + }); + installLoader.Start(installTemp); + LoaderTaskbarAdd(installLoader); + ModMain.frmMain.BtnExtraDownload.ShowRefresh(); + ModMain.frmMain.BtnExtraDownload.Ribble(); + return installLoader; + } + + /// + /// 在解压后的目录中寻找内层整合包:优先 modpack.* 压缩包文件, + /// 其次寻找包含已知清单的文件夹,可识别 CurseForge / Modrinth / MultiMC / MCBBS / HMCL 等任意格式。 + /// + /// 解压后的外层目录。 + /// 内层整合包的文件路径或文件夹路径;未找到时返回 null。 + private static string? _FindInnerModpack(string rootFolder) + { + // 优先: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)) + { + 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; + } + + /// + /// 判断一个压缩包文件是否是整合包:使用格式识别器检查其根目录或一级目录是否存在已知整合包清单。 + /// 资源包等普通 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 懒人包(完整实例压缩包) + + private static LoaderCombo _InstallCompress(string sourcePath, IModpackArchiveReader archive) + { + 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) + continue; + firstMatch ??= entryMatch; + var versionName = entryMatch.Groups[1].Value; + if (versionNameSet.Add(versionName)) + versionNames.Add(versionName); + } + + 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); + + // 实例名:与现有实例不冲突时直接用懒人包自己的,冲突时让用户改名 + 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(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 = _GetModpackProgressWeight(sourcePath) + } // 每 6M 需要 1s + }) + { + OnStateChanged = ModDownloadLib.McInstallState + }; + loader.Start(_GetVersionFolder(instanceName)); + LoaderTaskbarAdd(loader); + ModMain.frmMain.BtnExtraDownload.ShowRefresh(); + ModMain.frmMain.BtnExtraDownload.Ribble(); + return loader; + } + + /// + /// 从懒人包内打包的多个版本中确定主实例(整合包本体)。 + /// 通过继承关系推断:不被任何其他版本 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) + { + var mcRoot = Path.Combine(installTemp, archiveBaseFolder.TrimEnd('\\')); + var instanceFolder = _GetVersionFolder(instanceName); + Directory.CreateDirectory(instanceFolder); + // 本次安装的冲突处理状态(“全部覆盖 / 全部跳过”在两个复制阶段间保持) + var conflictState = new CopyConflictState(); + + // 先复制 .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); + } + 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 + { + 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)到游戏文件夹根目录,避免重复下载并保留整合包的特殊依赖版本; + // 同名文件内容不同时由用户决定跳过还是覆盖(默认跳过)。 + _CopyDirectoryWithConflict(Path.Combine(mcRoot, "libraries"), + Path.Combine(ModFolder.mcFolderSelected, "libraries")); + _CopyDirectoryWithConflict(Path.Combine(mcRoot, "assets"), + Path.Combine(ModFolder.mcFolderSelected, "assets")); + + // 开启版本隔离 + var versionIni = instanceFolder + @"PCL\Setup.ini"; + ModBase.WriteIni(versionIni, "VersionArgumentIndie", 1.ToString()); + ModBase.WriteIni(versionIni, "VersionArgumentIndieV2", true.ToString()); + 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)); + } + } + } + + /// + /// 复制一个目录到目标目录(含子目录),同名文件先比较内容(相同则跳过); + /// 内容不同时弹窗让用户选择(跳过 / 覆盖 / 全部覆盖 / 全部跳过)。 + /// + /// 源目录。 + /// 目标目录。 + /// 本次复制操作的冲突处理状态;未提供时本次复制独立新建。 + /// 弹窗默认选项是否为“覆盖”;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); + foreach (var sourceFile in Directory.EnumerateFiles(sourceFolder, "*", SearchOption.AllDirectories)) + { + var relativePath = Path.GetRelativePath(sourceFolder, sourceFile); + _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) + { + 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) + { + 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); + } + + /// + /// 一次复制操作中“全部覆盖 / 全部跳过”的持久状态。 + /// + private sealed class CopyConflictState + { + public bool OverwriteAll; + public bool SkipAll; + } + + /// + /// 比较两个文件内容是否相同(先比大小,再比 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/Minecraft/ModModpack.CurseForge.cs b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs new file mode 100644 index 0000000000..15ae5bc070 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.CurseForge.cs @@ -0,0 +1,371 @@ +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; +using PCL.Core.Utils; +using PCL.Core.Utils.OS; +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 sourcePath, IModpackArchiveReader archive, + string archiveBaseFolder, string instanceName = null, string logo = null, string resourceId = null, + bool isOnlineInstall = false) + { + // 读取 Json 文件 + JsonObject json; + try + { + json = (JsonObject)ModBase.GetJson(archive.ReadEntryText(archiveBaseFolder + "manifest.json")); + } + 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 ?? ""); + + // 推荐内存询问:清单声明了推荐内存时,先校验其是否适合用户的电脑 + // (使用推荐 → 推荐内存模式;取消 → 跟随全局设置;不适合时自动 → 自动配置) + 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")) == 2; + 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; + 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(); + // 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, sourcePath, task, 0.6d); + _CopyOverrideDirectory( + Path.Combine(installTemp, archiveBaseFolder, overrideHome == "." || overrideHome == "./" ? "" : overrideHome), + _GetVersionFolder(instanceName), task, 0.4d); + }) + { + ProgressWeight = _GetModpackProgressWeight(sourcePath), + 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 + // 地图(World)文件:下载到临时目录,随后解压成 saves\地图名\ 文件夹 + var worldExtracts = new List<(string archivePath, string targetDirectory)>(); + 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(); + var fileName = modJson?["FileName"]?.ToString() ?? ""; + var gameVersions = ((JsonArray)modJson?["gameVersions"])?.Select(v => v.ToString()).ToList() ?? []; + if (moduleNames.Contains("META-INF") || moduleNames.Contains("mcmod.info") || + fileName.EndsWithF(".jar", true)) + { + targetFolder = "mods"; + type = ModComp.CompType.Mod; + } + else if (moduleNames.Contains("pack.mcmeta")) + { + targetFolder = "resourcepacks"; + type = ModComp.CompType.ResourcePack; + } + // 光影包:含 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"; + type = ModComp.CompType.Shader; + } + } + else + { + targetFolder = "mods"; + type = ModComp.CompType.Mod; + } + + // 建立 CompFile + var file = new ModComp.CompFile((JsonObject)modJson, type); + if (!file.Available) + continue; + 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); + } + + 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 + // 解压地图存档(下载到临时目录的地图,解压成 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"), + 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 => + { + var versionFolder = _GetVersionFolder(instanceName); + _FinalizeInstance(versionFolder, sourcePath, logo, "CurseForge", + manifest.Version, resourceId, manifest.RecommendedRamEffective ?? 0); + // 应用推荐内存选择:仅当清单声明了推荐内存时写配置 + // (使用推荐 → 推荐内存模式;不适合时自动 → 自动配置;正常取消 → 跟随全局设置) + // MemorySolution 注册了 UI 观察者(ModSetup.VersionRamType),需在 UI 线程写入, + // 避免在后台安装线程触发观察者导致跨线程访问 UI 控件。 + if (recommendedRam > 0) + { + var memorySolution = useRecommendedRam ? 3 : (useAutoRam ? 0 : 2); + ModBase.RunInUi(() => Config.Instance.MemorySolution[versionFolder] = memorySolution); + } + // 本地安装没有外部 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, + 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..6b63a194e8 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Hmcl.cs @@ -0,0 +1,72 @@ +using System.IO; +using System.IO.Compression; +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; + +namespace PCL; + +public static partial class ModModpack +{ + #region HMCL + + private static LoaderCombo _InstallHmcl(string sourcePath, IModpackArchiveReader archive, + string archiveBaseFolder) + { + // 读取 Json 文件 + JsonObject json; + try + { + json = (JsonObject)ModBase.GetJson(archive.ReadEntryText(archiveBaseFolder + "modpack.json")); + } + 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, sourcePath, task, 0.6d); + _CopyOverrideDirectory(Path.Combine(installTemp, archiveBaseFolder, "minecraft"), + Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.4d); + }) + { + ProgressWeight = _GetModpackProgressWeight(sourcePath), + 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..8df17facce --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Mcbbs.cs @@ -0,0 +1,128 @@ +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 MCBBS + + private static LoaderCombo _InstallMcbbs(string sourcePath, IModpackArchiveReader archive, + string archiveBaseFolder, string instanceName = null) + { + // 读取 Json 文件 + JsonObject json; + try + { + 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(manifest.Name ?? ""); + + // 解压与路径准备 + 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, sourcePath, task, 0.6); + _CopyOverrideDirectory( + Path.Combine(installTemp, archiveBaseFolder, "overrides"), + Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), + task, 0.4); + + // JVM 参数处理 + if (manifest.LaunchInfo is not null) + { + Config.Instance.JvmArgs[versionFolder] = + string.Join(" ", manifest.LaunchInfo.JavaArgument ?? new JsonArray()); + Config.Instance.GameArgs[versionFolder] = + string.Join(" ", manifest.LaunchInfo.LaunchArgument ?? new JsonArray()); + } + + // 整合包版本 + if (manifest.Version is not null) States.Instance.ModpackVersion[versionFolder] = manifest.Version; + }); + + unzipTask.ProgressWeight = _GetModpackProgressWeight(sourcePath); // 每 6M 需要 1s + unzipTask.block = false; + installLoaders.Add(unzipTask); + + // 构造加载器 + if (manifest.Addons is null) + throw new Exception(Lang.Text("Minecraft.Download.Modpack.MissingGameVersion.McbbsAddons")); + + var addons = new Dictionary(); + foreach (var addon in manifest.Addons) + if (addon.Id is not null) + addons.Add(addon.Id, addon.Version ?? ""); + + 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..8332c3d0e1 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.Modrinth.cs @@ -0,0 +1,282 @@ +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; +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 sourcePath, IModpackArchiveReader archive, + string archiveBaseFolder, string instanceName = null, string logo = null, string resourceId = null, + bool isOnlineInstall = false) + { + // 读取 Json 文件 + JsonObject json; + try + { + json = (JsonObject)ModBase.GetJson(archive.ReadEntryText(archiveBaseFolder + "modrinth.index.json")); + } + 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, 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 = _GetModpackProgressWeight(sourcePath), + block = false + }); // 每 6M 需要 1s + // 获取下载文件列表 + var fileList = new List(); + // 目录形式的下载项(地图存档等):先下载到临时目录,再解压到目标文件夹 + var archiveExtracts = new List<(string archivePath, string targetDirectory)>(); + 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(); + } + + 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 + { + 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), sourcePath, 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); + } + + /// + /// 判断 Modrinth 文件项的目标是否是一个文件夹(path 以斜杠结尾,或位于 saves 目录下)。 + /// 若是,下载内容应为需解压的压缩包(通常用于地图存档等文件夹形式的内容)。 + /// + private static bool _IsDirectoryDownload(ModrinthFile file) + { + var path = file.Path ?? ""; + if (path.EndsWith("/") || path.EndsWith("\\")) + return true; + var normalizedDir = Path.GetDirectoryName(path.TrimEnd('/', '\\'))?.Replace("\\", "/").TrimEnd('/'); + return normalizedDir != null && + (string.Equals(normalizedDir, "saves", StringComparison.OrdinalIgnoreCase) || + normalizedDir.EndsWith("/saves", StringComparison.OrdinalIgnoreCase)); + } + + /// + /// 将下载的压缩包解压到目标文件夹(用于地图存档等目录形式的下载项)。 + /// 解压到临时暂存目录后,若存在单层嵌套的存档根目录(zip 内再包一层文件夹)则直接使用该目录; + /// 不是有效的 Minecraft 存档(没有 level.dat)或非压缩包时回退为按普通文件复制。 + /// + 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); + 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); + } + 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 new file mode 100644 index 0000000000..9e68adbb21 --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ModModpack.MultiMc.cs @@ -0,0 +1,487 @@ +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 sourcePath, IModpackArchiveReader archive, + string archiveBaseFolder) + { + // 读取 Json 文件 + JsonObject packJson; + string packInstance; + try + { + packJson = (JsonObject)ModBase.GetJson(archive.ReadEntryText(archiveBaseFolder + "mmc-pack.json")); + packInstance = archive.ReadEntryText(archiveBaseFolder + "instance.cfg"); + } + 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, 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"), + Path.Combine(ModFolder.mcFolderSelected, "versions", instanceName), task, 0.2d); + + _MigrateMultiMcInstanceCfg(Path.Combine(installTemp, archiveBaseFolder, "instance.cfg"), versionFolder, + installTemp, archiveBaseFolder, instanceName); + }) + { + ProgressWeight = _GetModpackProgressWeight(sourcePath), + 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(IModpackArchiveReader archive, string archiveBaseFolder, + JsonObject packJson) + { + MMCPackInfo packInfo = null; + try + { + // 部分压缩工具不会写入 "patches/" 目录条目,因此不能只检查目录项, + // 而应检查是否存在任何位于 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 entryName in archive.EntryNames) + if (!entryName.EndsWith("/") && entryName.StartsWith(archiveBaseFolder + "patches/")) + { + var patch = (JsonObject)ModBase.GetJson(archive.ReadEntryText(entryName)); + 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..f8fecb0f0c 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,191 +54,22 @@ public static LoaderCombo ModpackInstall(string file, string instanceNam { ModBase.Log("[ModPack] 整合包安装请求:" + (file ?? "null")); ZipArchive archive = null; - var archiveBaseFolder = ""; 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 - var packType = -1; - do - { - 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); - - // 执行对应的安装方法 - switch (packType) - { - case 0: - { - ModBase.Log("[ModPack] 整合包种类:CurseForge"); - return InstallPackCurseForge(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, - isOnlineInstall); - } - case 9: - { - ModBase.Log("[ModPack] 整合包种类:带启动器的压缩包"); - return InstallPackLauncherPack(file, archive, archiveBaseFolder); - } + 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); + } - default: - { - ModBase.Log("[ModPack] 整合包种类:未能识别,假定为压缩包"); - return InstallPackCompress(file, archive); - } - } + try + { + return _InstallSource(new ZipModpackArchiveReader(archive), file, instanceName, logo, resourceId, + isOnlineInstall); } finally { @@ -249,9 +78,93 @@ public static LoaderCombo ModpackInstall(string file, string instanceNam } } - private static void ExtractModpackFiles(string installTemp, string fileAddress, LoaderBase loader, + /// + /// 从统一的整合包来源(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, source); + 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 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"); @@ -266,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); // 解压成功,更新进度并退出循环 @@ -298,11 +211,30 @@ private static void ExtractModpackFiles(string installTemp, string fileAddress, } } + /// + /// 计算整合包源解压阶段的进度权重(预计时间秒)。 + /// 文件来源取文件大小;文件夹来源(内层文件夹形式整合包)取目录下所有文件总大小, + /// + 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 的配置文件与版本隔离。 /// 对路径末尾是否为 \ 没有要求。 /// - 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,546 +272,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 - }); + return $@"{ModFolder.mcFolderSelected}versions\{instanceName}\"; + } + /// + /// 组装顶层安装加载器:重复任务检查、启动、加入任务栏、刷新下载按钮并跳转任务管理器页。 + /// + private static LoaderCombo _StartInstall(string loaderName, List loaders, string input, + bool isOnlineInstall) + { // 重复任务检查 - 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); @@ -888,7 +312,7 @@ private static LoaderCombo InstallPackModrinth(string fileAddress, ZipAr // 启动 var loader = new LoaderCombo(loaderName, loaders) { OnStateChanged = ModDownloadLib.McInstallState }; - loader.Start(request.targetInstanceFolder); + loader.Start(input); LoaderTaskbarAdd(loader); ModMain.frmMain.BtnExtraDownload.ShowRefresh(); if (!isOnlineInstall) @@ -896,828 +320,65 @@ private static LoaderCombo InstallPackModrinth(string fileAddress, ZipAr return loader; } - #endregion - - #region HMCL - - private static LoaderCombo InstallPackHMCL(string fileAddress, ZipArchive archive, string archiveBaseFolder) + /// + /// 整合包安装收尾:设置图标、删除原始整合包文件、写入整合包版本/来源/ID/推荐内存信息并尝试获取整合包描述。 + /// + /// 整合包推荐的启动内存(单位 MB);0 表示未声明。 + private static void _FinalizeInstance(string versionFolder, string fileAddress, string logo, string modpackSource, + string modpackVersion, string resourceId, int recommendedRam = 0) { - // 读取 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); - throw new ModBase.CancelledException(); - } + // 设置图标 + if (logo is not null) + _SetInstanceIcon(versionFolder, logo); - // 启动 - 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; - } - - #endregion - - #region MCBBS - - private static LoaderCombo InstallPackMCBBS(string fileAddress, ZipArchive archive, - string archiveBaseFolder, string instanceName = null) - { - // 读取 Json 文件 - JsonObject json; - try - { - // VB 的 If(a, b) 在 C# 中如果是 null 合并则用 ??,如果是三元运算则用 ?: - var entry = archive.GetEntry(archiveBaseFolder + "mcbbs.packmeta") ?? - archive.GetEntry(archiveBaseFolder + "manifest.json"); - using (var stream = entry.Open()) + // 删除原始整合包文件 + foreach (var target in new[] { Path.Combine(versionFolder, "原始整合包.zip"), Path.Combine(versionFolder, "原始整合包.mrpack") }) + if (File.Exists(target)) { - json = (JsonObject)ModBase.GetJson(ModBase.ReadFile(stream, Encoding.UTF8)); + ModBase.Log("[ModPack] 删除原始整合包文件:" + target); + File.Delete(target); } - } - catch (Exception ex) - { - throw new Exception("MCBBS 整合包安装信息存在问题", ex); - } - // 获取实例名 - if (instanceName is null) + if (File.Exists(fileAddress) && ModBase.GetFileNameWithoutExtentionFromPath(fileAddress) == "modpack") { - 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(); + ModBase.Log("[ModPack] 删除安装整合包文件:" + fileAddress); + File.Delete(fileAddress); } - // 解压与路径准备 - 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 => + // 整合包版本 + 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 { - 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 = $"{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) - { - 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(); - } - - // 启动任务 - 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) - { - 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)"); + ModBase.Log(ex, "[ModPack] 获取整合包描述文本失败"); } + } while (false); + } - #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")); - } - } - - 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; + /// + /// 将图标文件复制到实例的 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 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..9896d6798a --- /dev/null +++ b/Plain Craft Launcher 2/Modules/Minecraft/ZipModpackArchiveReader.cs @@ -0,0 +1,49 @@ +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); + } + + 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); + } +} 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()) 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"); 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 && + modpackRam <= KernelInterop.GetPhysicalMemoryBytes().Total / 1024d / 1024d; // 服务器 TextServerEnter.Text = Config.Instance.ServerToEnter[PageInstanceLeft.McInstance.PathInstance]; @@ -445,13 +451,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 =