From 138b35310b3d32e1cc3ade0d5d115947da53ff63 Mon Sep 17 00:00:00 2001 From: 2012hzy <3681596340p@qq.com> Date: Sun, 14 Jun 2026 11:04:10 +0800 Subject: [PATCH 01/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mod/mcbbs/McbbsModpackExportTask.java | 241 +++++++++++++----- .../modrinth/ModrinthModpackExportTask.java | 204 +++++++++------ 2 files changed, 311 insertions(+), 134 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 2f8eccd2ae7..ea87d3a59eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.mod.mcbbs; +import com.google.gson.stream.JsonWriter; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.Library; @@ -34,6 +35,8 @@ import java.io.File; import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -72,66 +75,185 @@ public void execute() throws Exception { blackList.add(version + ".jar"); blackList.add(version + ".json"); LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker"); - try (var zip = new Zipper(modpackFile)) { - Path runDirectory = repository.getRunDirectory(version); - List files = new ArrayList<>(); - zip.putDirectory(runDirectory, "overrides", path -> { - if (Modpack.acceptFile(path, blackList, info.getWhitelist())) { - Path file = runDirectory.resolve(path); - if (Files.isRegularFile(file)) { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - files.add(new McbbsModpackManifest.AddonFile(true, relativePath, DigestUtils.digestToString("SHA-1", file))); + + Path runDirectory = repository.getRunDirectory(version); + String gameVersion = repository.getGameVersion(version) + .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); + + Path tempManifest = Files.createTempFile("mcbbs_packmeta_", ".json"); + try { + try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(Files.newOutputStream(tempManifest), StandardCharsets.UTF_8))) { + writer.setIndent(" "); + writer.beginObject(); + + writer.name("manifestType").value(McbbsModpackManifest.MANIFEST_TYPE); + writer.name("manifestVersion").value(2); + writer.name("name").value(info.getName()); + writer.name("version").value(info.getVersion()); + writer.name("author").value(info.getAuthor()); + writer.name("description").value(info.getDescription()); + writer.name("fileApi").value(info.getFileApi() == null ? null : StringUtils.removeSuffix(info.getFileApi(), "/")); + writer.name("url").value(info.getUrl()); + writer.name("forceUpdate").value(info.isForceUpdate()); + + writer.name("origins").beginArray(); + writer.endArray(); + + writer.name("addons").beginArray(); + writer.beginObject(); + writer.name("id").value(MINECRAFT.getPatchId()); + writer.name("version").value(gameVersion); + writer.endObject(); + analyzer.getVersion(FORGE).ifPresent(forgeVersion -> { + try { + writer.beginObject(); + writer.name("id").value(FORGE.getPatchId()); + writer.name("version").value(forgeVersion); + writer.endObject(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + analyzer.getVersion(CLEANROOM).ifPresent(cleanroomVersion -> { + try { + writer.beginObject(); + writer.name("id").value(CLEANROOM.getPatchId()); + writer.name("version").value(cleanroomVersion); + writer.endObject(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion -> { + try { + writer.beginObject(); + writer.name("id").value(NEO_FORGE.getPatchId()); + writer.name("version").value(neoForgeVersion); + writer.endObject(); + } catch (IOException e) { + throw new RuntimeException(e); } - return true; - } else { - return false; + }); + analyzer.getVersion(LITELOADER).ifPresent(liteLoaderVersion -> { + try { + writer.beginObject(); + writer.name("id").value(LITELOADER.getPatchId()); + writer.name("version").value(liteLoaderVersion); + writer.endObject(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + analyzer.getVersion(OPTIFINE).ifPresent(optifineVersion -> { + try { + writer.beginObject(); + writer.name("id").value(OPTIFINE.getPatchId()); + writer.name("version").value(optifineVersion); + writer.endObject(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> { + try { + writer.beginObject(); + writer.name("id").value(FABRIC.getPatchId()); + writer.name("version").value(fabricVersion); + writer.endObject(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + analyzer.getVersion(QUILT).ifPresent(quiltVersion -> { + try { + writer.beginObject(); + writer.name("id").value(QUILT.getPatchId()); + writer.name("version").value(quiltVersion); + writer.endObject(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + analyzer.getVersion(LEGACY_FABRIC).ifPresent(legacyfabricVersion -> { + try { + writer.beginObject(); + writer.name("id").value(LEGACY_FABRIC.getPatchId()); + writer.name("version").value(legacyfabricVersion); + writer.endObject(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + writer.endArray(); + + writer.name("libraries").beginArray(); + writer.endArray(); + + writer.name("files").beginArray(); + Files.walk(runDirectory) + .filter(Files::isRegularFile) + .forEach(file -> { + try { + Path relative = runDirectory.relativize(file); + String relativePath = relative.toString().replace(File.separatorChar, '/'); + if (Modpack.acceptFile(relativePath, blackList, info.getWhitelist())) { + String sha1 = DigestUtils.digestToString("SHA-1", file); + writer.beginObject(); + writer.name("type").value(true); + writer.name("path").value(relativePath); + writer.name("hash").value(sha1); + writer.endObject(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + writer.endArray(); + + writer.name("settings").beginObject(); + writer.endObject(); + + writer.name("launchInfo").beginObject(); + writer.name("minMemory").value(info.getMinMemory()); + + writer.name("supportedJavaVersions").beginArray(); + for (int ver : info.getSupportedJavaVersions()) { + writer.value(ver); } - }); - - String gameVersion = repository.getGameVersion(version) - .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); - - // Mcbbs manifest - List addons = new ArrayList<>(); - addons.add(new McbbsModpackManifest.Addon(MINECRAFT.getPatchId(), gameVersion)); - analyzer.getVersion(FORGE).ifPresent(forgeVersion -> - addons.add(new McbbsModpackManifest.Addon(FORGE.getPatchId(), forgeVersion))); - analyzer.getVersion(CLEANROOM).ifPresent(cleanroomVersion -> - addons.add(new McbbsModpackManifest.Addon(CLEANROOM.getPatchId(), cleanroomVersion))); - analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion -> - addons.add(new McbbsModpackManifest.Addon(NEO_FORGE.getPatchId(), neoForgeVersion))); - analyzer.getVersion(LITELOADER).ifPresent(liteLoaderVersion -> - addons.add(new McbbsModpackManifest.Addon(LITELOADER.getPatchId(), liteLoaderVersion))); - analyzer.getVersion(OPTIFINE).ifPresent(optifineVersion -> - addons.add(new McbbsModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion))); - analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> - addons.add(new McbbsModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion))); - analyzer.getVersion(QUILT).ifPresent(quiltVersion -> - addons.add(new McbbsModpackManifest.Addon(QUILT.getPatchId(), quiltVersion))); - analyzer.getVersion(LEGACY_FABRIC).ifPresent(legacyfabricVersion -> - addons.add(new McbbsModpackManifest.Addon(LEGACY_FABRIC.getPatchId(), legacyfabricVersion))); - - List libraries = new ArrayList<>(); - // TODO libraries - - List origins = new ArrayList<>(); - // TODO origins - - McbbsModpackManifest.Settings settings = new McbbsModpackManifest.Settings(); - McbbsModpackManifest.LaunchInfo launchInfo = new McbbsModpackManifest.LaunchInfo(info.getMinMemory(), info.getSupportedJavaVersions(), StringUtils.tokenize(info.getLaunchArguments()), StringUtils.tokenize(info.getJavaArguments())); - - McbbsModpackManifest mcbbsManifest = new McbbsModpackManifest(McbbsModpackManifest.MANIFEST_TYPE, 2, info.getName(), info.getVersion(), info.getAuthor(), info.getDescription(), info.getFileApi() == null ? null : StringUtils.removeSuffix(info.getFileApi(), "/"), info.getUrl(), info.isForceUpdate(), origins, addons, libraries, files, settings, launchInfo); - zip.putTextFile(JsonUtils.GSON.toJson(mcbbsManifest), "mcbbs.packmeta"); - - // CurseForge manifest - List modLoaders = new ArrayList<>(); - analyzer.getVersion(FORGE).ifPresent(forgeVersion -> modLoaders.add(new CurseManifestModLoader("forge-" + forgeVersion, true))); - analyzer.getVersion(NEO_FORGE).ifPresent(forgeVersion -> modLoaders.add(new CurseManifestModLoader("neoforge-" + forgeVersion, true))); - analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> modLoaders.add(new CurseManifestModLoader("fabric-" + fabricVersion, true))); - // OptiFine and LiteLoader are not supported by CurseForge modpack. - CurseManifest curseManifest = new CurseManifest(CurseManifest.MINECRAFT_MODPACK, 1, info.getName(), info.getVersion(), info.getAuthor(), "overrides", new CurseManifestMinecraft(gameVersion, modLoaders), Collections.emptyList()); - zip.putTextFile(JsonUtils.GSON.toJson(curseManifest), "manifest.json"); + writer.endArray(); + + writer.name("launchArguments").beginArray(); + for (String arg : StringUtils.tokenize(info.getLaunchArguments())) { + writer.value(arg); + } + writer.endArray(); + writer.name("javaArguments").beginArray(); + for (String arg : StringUtils.tokenize(info.getJavaArguments())) { + writer.value(arg); + } + writer.endArray(); + writer.endObject(); + + writer.endObject(); + } + + try (var zip = new Zipper(modpackFile)) { + zip.putFile(tempManifest, "mcbbs.packmeta"); + + List modLoaders = new ArrayList<>(); + analyzer.getVersion(FORGE).ifPresent(forgeVersion -> modLoaders.add(new CurseManifestModLoader("forge-" + forgeVersion, true))); + analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion -> modLoaders.add(new CurseManifestModLoader("neoforge-" + neoForgeVersion, true))); + analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> modLoaders.add(new CurseManifestModLoader("fabric-" + fabricVersion, true))); + CurseManifest curseManifest = new CurseManifest(CurseManifest.MINECRAFT_MODPACK, 1, info.getName(), info.getVersion(), info.getAuthor(), "overrides", new CurseManifestMinecraft(gameVersion, modLoaders), Collections.emptyList()); + zip.putTextFile(JsonUtils.GSON.toJson(curseManifest), "manifest.json"); + + zip.putDirectory(runDirectory, "overrides", path -> { + return Modpack.acceptFile(path, blackList, info.getWhitelist()); + }); + } + } finally { + Files.deleteIfExists(tempManifest); } } @@ -145,5 +267,4 @@ public void execute() throws Exception { .requireLaunchArguments() .requireOrigins() .requireAuthor(); - -} +} \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index b4f7d9fc29b..b2ce651fc1a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -17,25 +17,25 @@ */ package org.jackhuang.hmcl.mod.modrinth; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.*; - +import com.google.gson.stream.JsonWriter; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.mod.ModAdviser; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.ModpackExportInfo; +import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; -import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.Zipper; -import org.jackhuang.hmcl.mod.LocalModFile; -import org.jackhuang.hmcl.mod.RemoteMod; -import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -73,7 +73,6 @@ private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath) t relativePath = repository.getModManager(version).enableMod(Paths.get(relativePath)).toString(); } - LocalModFile localModFile = null; Optional modrinthVersion = Optional.empty(); Optional curseForgeVersion = Optional.empty(); @@ -130,76 +129,133 @@ public void execute() throws Exception { blackList.add(version + ".jar"); blackList.add(version + ".json"); LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker"); - try (var zip = new Zipper(modpackFile)) { - Path runDirectory = repository.getRunDirectory(version); - List files = new ArrayList<>(); - Set filesInManifest = new HashSet<>(); - - String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; - for (String dir : resourceDirs) { - Path dirPath = runDirectory.resolve(dir); - if (Files.exists(dirPath)) { - Files.walk(dirPath) - .filter(Files::isRegularFile) - .forEach(file -> { - try { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - - if (!info.getWhitelist().contains(relativePath)) { - return; - } - ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); - if (fileEntry != null) { - files.add(fileEntry); - filesInManifest.add(relativePath); + Path runDirectory = repository.getRunDirectory(version); + String gameVersion = repository.getGameVersion(version) + .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); + + String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; + Set remoteFilePaths = new HashSet<>(); + + Path tempIndex = Files.createTempFile("modrinth_index_", ".json"); + try { + try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(Files.newOutputStream(tempIndex), StandardCharsets.UTF_8))) { + writer.setIndent(" "); + writer.beginObject(); + + writer.name("formatVersion").value(1); + writer.name("game").value("minecraft"); + writer.name("versionId").value(info.getVersion()); + writer.name("name").value(info.getName()); + writer.name("summary").value(info.getDescription()); + + writer.name("files").beginArray(); + + Set processedPaths = new HashSet<>(); + + for (String dir : resourceDirs) { + Path dirPath = runDirectory.resolve(dir); + if (Files.exists(dirPath)) { + Files.walk(dirPath) + .filter(Files::isRegularFile) + .forEach(file -> { + try { + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + if (!info.getWhitelist().contains(relativePath)) { + return; + } + if (processedPaths.contains(relativePath)) { + return; + } + processedPaths.add(relativePath); + + ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); + if (fileEntry != null) { + remoteFilePaths.add(relativePath); + writer.beginObject(); + writer.name("path").value(fileEntry.getPath()); + writer.name("hashes").beginObject(); + for (Map.Entry hash : fileEntry.getHashes().entrySet()) { + writer.name(hash.getKey()).value(hash.getValue()); + } + writer.endObject(); + if (fileEntry.getEnv() != null) { + writer.name("env").beginObject(); + for (Map.Entry env : fileEntry.getEnv().entrySet()) { + writer.name(env.getKey()).value(env.getValue()); + } + writer.endObject(); + } + writer.name("downloads").beginArray(); + for (String url : fileEntry.getDownloads()) { + writer.value(url); + } + writer.endArray(); + writer.name("fileSize").value(fileEntry.getFileSize()); + writer.endObject(); + } + } catch (IOException e) { + throw new RuntimeException(e); } - } catch (IOException e) { - LOG.warning("Failed to process file: " + file, e); - } - }); + }); + } } + + writer.endArray(); + + writer.name("dependencies").beginObject(); + writer.name("minecraft").value(gameVersion); + analyzer.getVersion(FORGE).ifPresent(forgeVersion -> { + try { + writer.name("forge").value(forgeVersion); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion -> { + try { + writer.name("neoforge").value(neoForgeVersion); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> { + try { + writer.name("fabric-loader").value(fabricVersion); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + analyzer.getVersion(QUILT).ifPresent(quiltVersion -> { + try { + writer.name("quilt-loader").value(quiltVersion); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + writer.endObject(); + + writer.endObject(); } - zip.putDirectory(runDirectory, "client-overrides", path -> { - String relativePath = path.replace(File.separatorChar, '/'); - if (filesInManifest.contains(relativePath)) { - return false; - } - return Modpack.acceptFile(path, blackList, info.getWhitelist()); - }); - - String gameVersion = repository.getGameVersion(version) - .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); - - Map dependencies = new HashMap<>(); - dependencies.put("minecraft", gameVersion); - - analyzer.getVersion(FORGE).ifPresent(forgeVersion -> - dependencies.put("forge", forgeVersion)); - analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion -> - dependencies.put("neoforge", neoForgeVersion)); - analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> - dependencies.put("fabric-loader", fabricVersion)); - analyzer.getVersion(QUILT).ifPresent(quiltVersion -> - dependencies.put("quilt-loader", quiltVersion)); - - ModrinthManifest manifest = new ModrinthManifest( - "minecraft", - 1, - info.getVersion(), - info.getName(), - info.getDescription(), - files, - dependencies - ); - - zip.putTextFile(JsonUtils.GSON.toJson(manifest), "modrinth.index.json"); + try (var zip = new Zipper(modpackFile)) { + zip.putFile(tempIndex, "modrinth.index.json"); + + zip.putDirectory(runDirectory, "client-overrides", path -> { + String relativePath = path.replace(File.separatorChar, '/'); + if (remoteFilePaths.contains(relativePath)) { + return false; + } + return Modpack.acceptFile(path, blackList, info.getWhitelist()); + }); + } + } finally { + Files.deleteIfExists(tempIndex); } } public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options() .requireNoCreateRemoteFiles() .requireSkipCurseForgeRemoteFiles(); -} +} \ No newline at end of file From f6a1a1ab8d8628381d6fcbb130a9122316df0b74 Mon Sep 17 00:00:00 2001 From: 2012hzy <3681596340p@qq.com> Date: Sun, 14 Jun 2026 11:26:27 +0800 Subject: [PATCH 02/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mod/mcbbs/McbbsModpackExportTask.java | 44 +++++------ .../modrinth/ModrinthModpackExportTask.java | 76 ++++++++++--------- 2 files changed, 61 insertions(+), 59 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index ea87d3a59eb..1cd35f7b2aa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -20,7 +20,6 @@ import com.google.gson.stream.JsonWriter; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.DefaultGameRepository; -import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.mod.ModAdviser; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.ModpackExportInfo; @@ -191,24 +190,25 @@ public void execute() throws Exception { writer.endArray(); writer.name("files").beginArray(); - Files.walk(runDirectory) - .filter(Files::isRegularFile) - .forEach(file -> { - try { - Path relative = runDirectory.relativize(file); - String relativePath = relative.toString().replace(File.separatorChar, '/'); - if (Modpack.acceptFile(relativePath, blackList, info.getWhitelist())) { - String sha1 = DigestUtils.digestToString("SHA-1", file); - writer.beginObject(); - writer.name("type").value(true); - writer.name("path").value(relativePath); - writer.name("hash").value(sha1); - writer.endObject(); + try (var stream = Files.walk(runDirectory)) { + stream.filter(Files::isRegularFile) + .forEach(file -> { + try { + Path relative = runDirectory.relativize(file); + String relativePath = relative.toString().replace(File.separatorChar, '/'); + if (Modpack.acceptFile(relativePath, blackList, info.getWhitelist())) { + String sha1 = DigestUtils.digestToString("SHA-1", file); + writer.beginObject(); + writer.name("type").value(true); + writer.name("path").value(relativePath); + writer.name("hash").value(sha1); + writer.endObject(); + } + } catch (IOException e) { + throw new RuntimeException(e); } - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + }); + } writer.endArray(); writer.name("settings").beginObject(); @@ -216,13 +216,13 @@ public void execute() throws Exception { writer.name("launchInfo").beginObject(); writer.name("minMemory").value(info.getMinMemory()); - writer.name("supportedJavaVersions").beginArray(); - for (int ver : info.getSupportedJavaVersions()) { - writer.value(ver); + if (info.getSupportedJavaVersions() != null) { + for (int ver : info.getSupportedJavaVersions()) { + writer.value(ver); + } } writer.endArray(); - writer.name("launchArguments").beginArray(); for (String arg : StringUtils.tokenize(info.getLaunchArguments())) { writer.value(arg); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index b2ce651fc1a..fb5bca8cfe0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -28,6 +28,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.io.Zipper; + import java.io.File; import java.io.IOException; import java.io.OutputStreamWriter; @@ -157,48 +158,49 @@ public void execute() throws Exception { for (String dir : resourceDirs) { Path dirPath = runDirectory.resolve(dir); if (Files.exists(dirPath)) { - Files.walk(dirPath) - .filter(Files::isRegularFile) - .forEach(file -> { - try { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - if (!info.getWhitelist().contains(relativePath)) { - return; - } - if (processedPaths.contains(relativePath)) { - return; - } - processedPaths.add(relativePath); - - ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); - if (fileEntry != null) { - remoteFilePaths.add(relativePath); - writer.beginObject(); - writer.name("path").value(fileEntry.getPath()); - writer.name("hashes").beginObject(); - for (Map.Entry hash : fileEntry.getHashes().entrySet()) { - writer.name(hash.getKey()).value(hash.getValue()); + try (var stream = Files.walk(dirPath)) { + stream.filter(Files::isRegularFile) + .forEach(file -> { + try { + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + if (!info.getWhitelist().contains(relativePath)) { + return; + } + if (processedPaths.contains(relativePath)) { + return; } - writer.endObject(); - if (fileEntry.getEnv() != null) { - writer.name("env").beginObject(); - for (Map.Entry env : fileEntry.getEnv().entrySet()) { - writer.name(env.getKey()).value(env.getValue()); + processedPaths.add(relativePath); + + ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); + if (fileEntry != null) { + remoteFilePaths.add(relativePath); + writer.beginObject(); + writer.name("path").value(fileEntry.getPath()); + writer.name("hashes").beginObject(); + for (Map.Entry hash : fileEntry.getHashes().entrySet()) { + writer.name(hash.getKey()).value(hash.getValue()); } writer.endObject(); + if (fileEntry.getEnv() != null) { + writer.name("env").beginObject(); + for (Map.Entry env : fileEntry.getEnv().entrySet()) { + writer.name(env.getKey()).value(env.getValue()); + } + writer.endObject(); + } + writer.name("downloads").beginArray(); + for (String url : fileEntry.getDownloads()) { + writer.value(url); + } + writer.endArray(); + writer.name("fileSize").value(fileEntry.getFileSize()); + writer.endObject(); } - writer.name("downloads").beginArray(); - for (String url : fileEntry.getDownloads()) { - writer.value(url); - } - writer.endArray(); - writer.name("fileSize").value(fileEntry.getFileSize()); - writer.endObject(); + } catch (IOException e) { + throw new RuntimeException(e); } - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + }); + } } } From 49ee0c003107b57bd74da8af4690c2ab583fb1d1 Mon Sep 17 00:00:00 2001 From: 2012hzy <3681596340p@qq.com> Date: Sun, 14 Jun 2026 11:29:48 +0800 Subject: [PATCH 03/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java | 2 +- .../jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 1cd35f7b2aa..910aca17e5c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -267,4 +267,4 @@ public void execute() throws Exception { .requireLaunchArguments() .requireOrigins() .requireAuthor(); -} \ No newline at end of file +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index fb5bca8cfe0..673ebb0098e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -260,4 +260,4 @@ public void execute() throws Exception { public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options() .requireNoCreateRemoteFiles() .requireSkipCurseForgeRemoteFiles(); -} \ No newline at end of file +} From b7d815e731c245832f8ef889a4cad5115047665f Mon Sep 17 00:00:00 2001 From: 2012hzy <3681596340p@qq.com> Date: Sun, 14 Jun 2026 11:40:15 +0800 Subject: [PATCH 04/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mod/mcbbs/McbbsModpackExportTask.java | 103 ++++-------------- .../modrinth/ModrinthModpackExportTask.java | 48 ++++---- 2 files changed, 39 insertions(+), 112 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 910aca17e5c..7576990fad3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -41,6 +41,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -96,7 +97,7 @@ public void execute() throws Exception { writer.name("url").value(info.getUrl()); writer.name("forceUpdate").value(info.isForceUpdate()); - writer.name("origins").beginArray(); + writer.name("origin").beginArray(); writer.endArray(); writer.name("addons").beginArray(); @@ -104,86 +105,19 @@ public void execute() throws Exception { writer.name("id").value(MINECRAFT.getPatchId()); writer.name("version").value(gameVersion); writer.endObject(); - analyzer.getVersion(FORGE).ifPresent(forgeVersion -> { - try { - writer.beginObject(); - writer.name("id").value(FORGE.getPatchId()); - writer.name("version").value(forgeVersion); - writer.endObject(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - analyzer.getVersion(CLEANROOM).ifPresent(cleanroomVersion -> { - try { - writer.beginObject(); - writer.name("id").value(CLEANROOM.getPatchId()); - writer.name("version").value(cleanroomVersion); - writer.endObject(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion -> { - try { - writer.beginObject(); - writer.name("id").value(NEO_FORGE.getPatchId()); - writer.name("version").value(neoForgeVersion); - writer.endObject(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - analyzer.getVersion(LITELOADER).ifPresent(liteLoaderVersion -> { - try { - writer.beginObject(); - writer.name("id").value(LITELOADER.getPatchId()); - writer.name("version").value(liteLoaderVersion); - writer.endObject(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - analyzer.getVersion(OPTIFINE).ifPresent(optifineVersion -> { - try { - writer.beginObject(); - writer.name("id").value(OPTIFINE.getPatchId()); - writer.name("version").value(optifineVersion); - writer.endObject(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> { - try { - writer.beginObject(); - writer.name("id").value(FABRIC.getPatchId()); - writer.name("version").value(fabricVersion); - writer.endObject(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - analyzer.getVersion(QUILT).ifPresent(quiltVersion -> { - try { - writer.beginObject(); - writer.name("id").value(QUILT.getPatchId()); - writer.name("version").value(quiltVersion); - writer.endObject(); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - analyzer.getVersion(LEGACY_FABRIC).ifPresent(legacyfabricVersion -> { - try { + + LibraryAnalyzer.LibraryType[] addonTypes = { + FORGE, CLEANROOM, NEO_FORGE, LITELOADER, OPTIFINE, FABRIC, QUILT, LEGACY_FABRIC + }; + for (LibraryAnalyzer.LibraryType type : addonTypes) { + Optional addonVersion = analyzer.getVersion(type); + if (addonVersion.isPresent()) { writer.beginObject(); - writer.name("id").value(LEGACY_FABRIC.getPatchId()); - writer.name("version").value(legacyfabricVersion); + writer.name("id").value(type.getPatchId()); + writer.name("version").value(addonVersion.get()); writer.endObject(); - } catch (IOException e) { - throw new RuntimeException(e); } - }); + } writer.endArray(); writer.name("libraries").beginArray(); @@ -194,12 +128,13 @@ public void execute() throws Exception { stream.filter(Files::isRegularFile) .forEach(file -> { try { - Path relative = runDirectory.relativize(file); + Path relative = runDirectory.relativize(file).normalize(); String relativePath = relative.toString().replace(File.separatorChar, '/'); if (Modpack.acceptFile(relativePath, blackList, info.getWhitelist())) { String sha1 = DigestUtils.digestToString("SHA-1", file); writer.beginObject(); - writer.name("type").value(true); + writer.name("type").value("addon"); + writer.name("force").value(true); writer.name("path").value(relativePath); writer.name("hash").value(sha1); writer.endObject(); @@ -212,23 +147,25 @@ public void execute() throws Exception { writer.endArray(); writer.name("settings").beginObject(); + writer.name("install_mods").value(true); + writer.name("install_resourcepack").value(true); writer.endObject(); writer.name("launchInfo").beginObject(); writer.name("minMemory").value(info.getMinMemory()); - writer.name("supportedJavaVersions").beginArray(); + writer.name("supportJava").beginArray(); if (info.getSupportedJavaVersions() != null) { for (int ver : info.getSupportedJavaVersions()) { writer.value(ver); } } writer.endArray(); - writer.name("launchArguments").beginArray(); + writer.name("launchArgument").beginArray(); for (String arg : StringUtils.tokenize(info.getLaunchArguments())) { writer.value(arg); } writer.endArray(); - writer.name("javaArguments").beginArray(); + writer.name("javaArgument").beginArray(); for (String arg : StringUtils.tokenize(info.getJavaArguments())) { writer.value(arg); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 673ebb0098e..36169a66fef 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -197,9 +197,11 @@ public void execute() throws Exception { writer.endObject(); } } catch (IOException e) { - throw new RuntimeException(e); + LOG.warning("Failed to process file: " + file, e); } }); + } catch (IOException e) { + LOG.warning("Failed to walk directory: " + dirPath, e); } } } @@ -208,34 +210,22 @@ public void execute() throws Exception { writer.name("dependencies").beginObject(); writer.name("minecraft").value(gameVersion); - analyzer.getVersion(FORGE).ifPresent(forgeVersion -> { - try { - writer.name("forge").value(forgeVersion); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion -> { - try { - writer.name("neoforge").value(neoForgeVersion); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> { - try { - writer.name("fabric-loader").value(fabricVersion); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - analyzer.getVersion(QUILT).ifPresent(quiltVersion -> { - try { - writer.name("quilt-loader").value(quiltVersion); - } catch (IOException e) { - throw new RuntimeException(e); - } - }); + Optional forgeVersion = analyzer.getVersion(FORGE); + if (forgeVersion.isPresent()) { + writer.name("forge").value(forgeVersion.get()); + } + Optional neoForgeVersion = analyzer.getVersion(NEO_FORGE); + if (neoForgeVersion.isPresent()) { + writer.name("neoforge").value(neoForgeVersion.get()); + } + Optional fabricVersion = analyzer.getVersion(FABRIC); + if (fabricVersion.isPresent()) { + writer.name("fabric-loader").value(fabricVersion.get()); + } + Optional quiltVersion = analyzer.getVersion(QUILT); + if (quiltVersion.isPresent()) { + writer.name("quilt-loader").value(quiltVersion.get()); + } writer.endObject(); writer.endObject(); From af6c4b946d33943309b7576a5f908d480d16b9de Mon Sep 17 00:00:00 2001 From: 2012hzy <3681596340p@qq.com> Date: Sun, 14 Jun 2026 11:48:32 +0800 Subject: [PATCH 05/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mod/mcbbs/McbbsModpackExportTask.java | 60 ++++++++----- .../modrinth/ModrinthModpackExportTask.java | 90 +++++++++---------- 2 files changed, 84 insertions(+), 66 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 7576990fad3..b5b91ffb3e5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -36,8 +36,11 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -93,11 +96,21 @@ public void execute() throws Exception { writer.name("version").value(info.getVersion()); writer.name("author").value(info.getAuthor()); writer.name("description").value(info.getDescription()); - writer.name("fileApi").value(info.getFileApi() == null ? null : StringUtils.removeSuffix(info.getFileApi(), "/")); + if (info.getFileApi() != null) { + writer.name("fileApi").value(StringUtils.removeSuffix(info.getFileApi(), "/")); + } writer.name("url").value(info.getUrl()); writer.name("forceUpdate").value(info.isForceUpdate()); writer.name("origin").beginArray(); + if (info.getOrigins() != null) { + for (McbbsModpackManifest.Origin origin : info.getOrigins()) { + writer.beginObject(); + writer.name("type").value(origin.getType()); + writer.name("id").value(origin.getId()); + writer.endObject(); + } + } writer.endArray(); writer.name("addons").beginArray(); @@ -124,26 +137,31 @@ public void execute() throws Exception { writer.endArray(); writer.name("files").beginArray(); - try (var stream = Files.walk(runDirectory)) { - stream.filter(Files::isRegularFile) - .forEach(file -> { - try { - Path relative = runDirectory.relativize(file).normalize(); - String relativePath = relative.toString().replace(File.separatorChar, '/'); - if (Modpack.acceptFile(relativePath, blackList, info.getWhitelist())) { - String sha1 = DigestUtils.digestToString("SHA-1", file); - writer.beginObject(); - writer.name("type").value("addon"); - writer.name("force").value(true); - writer.name("path").value(relativePath); - writer.name("hash").value(sha1); - writer.endObject(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - }); - } + Files.walkFileTree(runDirectory, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + String relativePath = runDirectory.relativize(dir).normalize().toString().replace(File.separatorChar, '/'); + if (!Modpack.acceptFile(relativePath, blackList, info.getWhitelist())) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + if (Modpack.acceptFile(relativePath, blackList, info.getWhitelist())) { + String sha1 = DigestUtils.digestToString("SHA-1", file); + writer.beginObject(); + writer.name("type").value("addon"); + writer.name("force").value(true); + writer.name("path").value(relativePath); + writer.name("hash").value(sha1); + writer.endObject(); + } + return FileVisitResult.CONTINUE; + } + }); writer.endArray(); writer.name("settings").beginObject(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 36169a66fef..156a6874194 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -33,9 +33,12 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.*; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; @@ -149,7 +152,9 @@ public void execute() throws Exception { writer.name("game").value("minecraft"); writer.name("versionId").value(info.getVersion()); writer.name("name").value(info.getName()); - writer.name("summary").value(info.getDescription()); + if (info.getDescription() != null) { + writer.name("summary").value(info.getDescription()); + } writer.name("files").beginArray(); @@ -158,51 +163,46 @@ public void execute() throws Exception { for (String dir : resourceDirs) { Path dirPath = runDirectory.resolve(dir); if (Files.exists(dirPath)) { - try (var stream = Files.walk(dirPath)) { - stream.filter(Files::isRegularFile) - .forEach(file -> { - try { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - if (!info.getWhitelist().contains(relativePath)) { - return; - } - if (processedPaths.contains(relativePath)) { - return; - } - processedPaths.add(relativePath); - - ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); - if (fileEntry != null) { - remoteFilePaths.add(relativePath); - writer.beginObject(); - writer.name("path").value(fileEntry.getPath()); - writer.name("hashes").beginObject(); - for (Map.Entry hash : fileEntry.getHashes().entrySet()) { - writer.name(hash.getKey()).value(hash.getValue()); - } - writer.endObject(); - if (fileEntry.getEnv() != null) { - writer.name("env").beginObject(); - for (Map.Entry env : fileEntry.getEnv().entrySet()) { - writer.name(env.getKey()).value(env.getValue()); - } - writer.endObject(); - } - writer.name("downloads").beginArray(); - for (String url : fileEntry.getDownloads()) { - writer.value(url); - } - writer.endArray(); - writer.name("fileSize").value(fileEntry.getFileSize()); - writer.endObject(); - } - } catch (IOException e) { - LOG.warning("Failed to process file: " + file, e); + Files.walkFileTree(dirPath, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + if (!info.getWhitelist().contains(relativePath)) { + return FileVisitResult.CONTINUE; + } + if (processedPaths.contains(relativePath)) { + return FileVisitResult.CONTINUE; + } + processedPaths.add(relativePath); + + ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); + if (fileEntry != null) { + remoteFilePaths.add(relativePath); + writer.beginObject(); + writer.name("path").value(fileEntry.getPath()); + writer.name("hashes").beginObject(); + for (Map.Entry hash : fileEntry.getHashes().entrySet()) { + writer.name(hash.getKey()).value(hash.getValue()); + } + writer.endObject(); + if (fileEntry.getEnv() != null) { + writer.name("env").beginObject(); + for (Map.Entry env : fileEntry.getEnv().entrySet()) { + writer.name(env.getKey()).value(env.getValue()); } - }); - } catch (IOException e) { - LOG.warning("Failed to walk directory: " + dirPath, e); - } + writer.endObject(); + } + writer.name("downloads").beginArray(); + for (String url : fileEntry.getDownloads()) { + writer.value(url); + } + writer.endArray(); + writer.name("fileSize").value(fileEntry.getFileSize()); + writer.endObject(); + } + return FileVisitResult.CONTINUE; + } + }); } } From b823a745a23809881b8d08eb2a5e86c8cf2d46ef Mon Sep 17 00:00:00 2001 From: 2012hzy <3681596340p@qq.com> Date: Sun, 14 Jun 2026 12:02:20 +0800 Subject: [PATCH 06/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/mod/modrinth/ModrinthModpackExportTask.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 156a6874194..4c14cab1984 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -175,7 +175,12 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO } processedPaths.add(relativePath); - ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); + ModrinthManifest.File fileEntry = null; + try { + fileEntry = tryGetRemoteFile(file, relativePath); + } catch (IOException e) { + LOG.warning("Failed to process file: " + file, e); + } if (fileEntry != null) { remoteFilePaths.add(relativePath); writer.beginObject(); From 46fc5aecf19c6a4d3fac5c49f9d007c659d25b9b Mon Sep 17 00:00:00 2001 From: 2012hzy <3681596340p@qq.com> Date: Sun, 14 Jun 2026 12:09:19 +0800 Subject: [PATCH 07/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/mod/mcbbs/McbbsModpackExportTask.java | 16 ++++++++++++---- .../mod/modrinth/ModrinthModpackExportTask.java | 5 ++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index b5b91ffb3e5..2de23747a86 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -178,14 +178,22 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO } } writer.endArray(); + writer.name("launchArgument").beginArray(); - for (String arg : StringUtils.tokenize(info.getLaunchArguments())) { - writer.value(arg); + List launchArgs = StringUtils.tokenize(info.getLaunchArguments()); + if (launchArgs != null) { + for (String arg : launchArgs) { + writer.value(arg); + } } writer.endArray(); + writer.name("javaArgument").beginArray(); - for (String arg : StringUtils.tokenize(info.getJavaArguments())) { - writer.value(arg); + List javaArgs = StringUtils.tokenize(info.getJavaArguments()); + if (javaArgs != null) { + for (String arg : javaArgs) { + writer.value(arg); + } } writer.endArray(); writer.endObject(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 4c14cab1984..f498cdba29f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -139,6 +139,9 @@ public void execute() throws Exception { .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); + // Convert whitelist List to HashSet for O(1) lookups + Set whitelistSet = new HashSet<>(info.getWhitelist()); + String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; Set remoteFilePaths = new HashSet<>(); @@ -167,7 +170,7 @@ public void execute() throws Exception { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - if (!info.getWhitelist().contains(relativePath)) { + if (!whitelistSet.contains(relativePath)) { return FileVisitResult.CONTINUE; } if (processedPaths.contains(relativePath)) { From a37feb7a45e020cf3fd6038d009e5c1d3accd776 Mon Sep 17 00:00:00 2001 From: 2012hzy <3681596340p@qq.com> Date: Sun, 14 Jun 2026 12:24:10 +0800 Subject: [PATCH 08/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/mod/mcbbs/McbbsModpackExportTask.java | 14 ++++++++++++-- .../mod/modrinth/ModrinthModpackExportTask.java | 8 ++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 2de23747a86..4b895b8edd8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -141,7 +141,11 @@ public void execute() throws Exception { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { String relativePath = runDirectory.relativize(dir).normalize().toString().replace(File.separatorChar, '/'); - if (!Modpack.acceptFile(relativePath, blackList, info.getWhitelist())) { + if (relativePath.isEmpty()) { + return FileVisitResult.CONTINUE; + } + // Only skip directories that match blacklist (e.g., assets, libraries) + if (ModAdviser.match(blackList, relativePath, false)) { return FileVisitResult.SKIP_SUBTREE; } return FileVisitResult.CONTINUE; @@ -212,7 +216,13 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO zip.putTextFile(JsonUtils.GSON.toJson(curseManifest), "manifest.json"); zip.putDirectory(runDirectory, "overrides", path -> { - return Modpack.acceptFile(path, blackList, info.getWhitelist()); + Path resolved = runDirectory.resolve(path); + if (Files.isDirectory(resolved)) { + // For directories, only check blacklist, never skip because of whitelist + return !ModAdviser.match(blackList, path, false); + } else { + return Modpack.acceptFile(path, blackList, info.getWhitelist()); + } }); } } finally { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index f498cdba29f..f90a1dad003 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -139,7 +139,6 @@ public void execute() throws Exception { .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); - // Convert whitelist List to HashSet for O(1) lookups Set whitelistSet = new HashSet<>(info.getWhitelist()); String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; @@ -244,10 +243,15 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO zip.putDirectory(runDirectory, "client-overrides", path -> { String relativePath = path.replace(File.separatorChar, '/'); + Path resolved = runDirectory.resolve(relativePath); + if (Files.isDirectory(resolved)) { + // For directories, only check blacklist, never skip due to whitelist + return !ModAdviser.match(blackList, relativePath, false); + } if (remoteFilePaths.contains(relativePath)) { return false; } - return Modpack.acceptFile(path, blackList, info.getWhitelist()); + return Modpack.acceptFile(relativePath, blackList, info.getWhitelist()); }); } } finally { From dd16443640a3ad24549b8d51e1bf532bba2b48c6 Mon Sep 17 00:00:00 2001 From: 2012hzy <3681596340p@qq.com> Date: Sun, 14 Jun 2026 16:17:13 +0800 Subject: [PATCH 09/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java | 2 -- .../jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 4b895b8edd8..dd4d0418db0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -144,7 +144,6 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th if (relativePath.isEmpty()) { return FileVisitResult.CONTINUE; } - // Only skip directories that match blacklist (e.g., assets, libraries) if (ModAdviser.match(blackList, relativePath, false)) { return FileVisitResult.SKIP_SUBTREE; } @@ -218,7 +217,6 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO zip.putDirectory(runDirectory, "overrides", path -> { Path resolved = runDirectory.resolve(path); if (Files.isDirectory(resolved)) { - // For directories, only check blacklist, never skip because of whitelist return !ModAdviser.match(blackList, path, false); } else { return Modpack.acceptFile(path, blackList, info.getWhitelist()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index f90a1dad003..bb69a3803d8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -75,6 +75,7 @@ private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath) t boolean isDisabled = repository.getModManager(version).isDisabled(file); if (isDisabled) { relativePath = repository.getModManager(version).enableMod(Paths.get(relativePath)).toString(); + file = repository.getRunDirectory(version).resolve(relativePath); } Optional modrinthVersion = Optional.empty(); @@ -245,7 +246,6 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO String relativePath = path.replace(File.separatorChar, '/'); Path resolved = runDirectory.resolve(relativePath); if (Files.isDirectory(resolved)) { - // For directories, only check blacklist, never skip due to whitelist return !ModAdviser.match(blackList, relativePath, false); } if (remoteFilePaths.contains(relativePath)) { From d23af3cd5901386c7b7f0eb44ad6fff5d95e1eed Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sat, 20 Jun 2026 19:03:39 +0800 Subject: [PATCH 10/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCLBoot/bin/main/assets/lang/boot.properties | 22 +++++++++++++++ .../bin/main/assets/lang/boot_es.properties | 19 +++++++++++++ .../bin/main/assets/lang/boot_zh.properties | 22 +++++++++++++++ .../main/assets/lang/boot_zh_Hant.properties | 22 +++++++++++++++ .../mod/mcbbs/McbbsModpackExportTask.java | 27 ++++++++++++++++--- .../modrinth/ModrinthModpackExportTask.java | 26 +++++++++++++----- git | 0 7 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 HMCLBoot/bin/main/assets/lang/boot.properties create mode 100644 HMCLBoot/bin/main/assets/lang/boot_es.properties create mode 100644 HMCLBoot/bin/main/assets/lang/boot_zh.properties create mode 100644 HMCLBoot/bin/main/assets/lang/boot_zh_Hant.properties create mode 100644 git diff --git a/HMCLBoot/bin/main/assets/lang/boot.properties b/HMCLBoot/bin/main/assets/lang/boot.properties new file mode 100644 index 00000000000..c6b43070eff --- /dev/null +++ b/HMCLBoot/bin/main/assets/lang/boot.properties @@ -0,0 +1,22 @@ +# +# Hello Minecraft! Launcher +# Copyright (C) 2025 huangyuhui and contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +boot.unsupported_java_version=HMCL requires Java 17 or later to run, but still supports launching games with Java 6~16. Please install the latest version of Java and try opening HMCL again.\nYou can keep your old version of Java. HMCL can detect and manage multiple Java installations, and will automatically select the appropriate Java version for your game. +boot.manual_update=HMCL cannot complete automatic updates in the current environment. Please download the latest version of HMCL manually.\nWould you like to go to the download page? + +boot.message.error=Error \ No newline at end of file diff --git a/HMCLBoot/bin/main/assets/lang/boot_es.properties b/HMCLBoot/bin/main/assets/lang/boot_es.properties new file mode 100644 index 00000000000..e12d99cd79c --- /dev/null +++ b/HMCLBoot/bin/main/assets/lang/boot_es.properties @@ -0,0 +1,19 @@ +# +# Hello Minecraft! Launcher +# Copyright (C) 2025 huangyuhui and contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +boot.unsupported_java_version=HMCL requiere Java 17 o posterior para ejecutarse, pero sigue admitiendo el inicio de juegos con Java 6~16.\nPor favor, instala la última versión de Java e intenta ejecutar HMCL de nuevo.\nPuedes conservar tu versión antigua de Java. HMCL puede detectar y gestionar múltiples instalaciones de Java,\ny seleccionará automáticamente la versión de Java adecuada para tu juego. diff --git a/HMCLBoot/bin/main/assets/lang/boot_zh.properties b/HMCLBoot/bin/main/assets/lang/boot_zh.properties new file mode 100644 index 00000000000..6ac05b2c26a --- /dev/null +++ b/HMCLBoot/bin/main/assets/lang/boot_zh.properties @@ -0,0 +1,22 @@ +# +# Hello Minecraft! Launcher +# Copyright (C) 2025 huangyuhui and contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +boot.unsupported_java_version=HMCL 需要 Java 17 或更高版本才能运行,但依然支持使用 Java 6~16 启动游戏。请安装最新版本的 Java 再尝试启动 HMCL。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。 +boot.manual_update=HMCL 在当前环境无法完成自动更新,请手动下载最新版本的 HMCL。\n是否前往下载页面? + +boot.message.error=错误 \ No newline at end of file diff --git a/HMCLBoot/bin/main/assets/lang/boot_zh_Hant.properties b/HMCLBoot/bin/main/assets/lang/boot_zh_Hant.properties new file mode 100644 index 00000000000..78c25c41491 --- /dev/null +++ b/HMCLBoot/bin/main/assets/lang/boot_zh_Hant.properties @@ -0,0 +1,22 @@ +# +# Hello Minecraft! Launcher +# Copyright (C) 2025 huangyuhui and contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +boot.unsupported_java_version=HMCL 需要 Java 17 或更高版本才能執行,但依然支援使用 Java 6~16 啟動遊戲。請安裝最新版本的 Java 再嘗試開啟 HMCL。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。 +boot.manual_update=HMCL 在當前環境無法完成自動更新,請手動下載最新版本的 HMCL。\n是否前往下載頁面? + +boot.message.error=錯誤 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index dd4d0418db0..d9b8ed23a77 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -36,6 +36,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -49,6 +50,20 @@ import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; +/** + * Export task for MCBBS modpack format. + *

+ * Note: This implementation performs two passes over the game directory: + * 1. First pass: walks the file tree to generate the manifest JSON (calculating SHA‑1 hashes). + * 2. Second pass: compresses the files into the final ZIP. + *

+ * This double traversal is a deliberate trade‑off to avoid holding all file information in memory, + * which would cause OutOfMemoryError on very large modpacks. The streaming JSON writer writes the + * manifest to a temporary file, keeping memory usage constant regardless of file count. + *

+ * SHA‑1 hashes are computed using {@link DigestUtils#digestToString(String, Path)} which uses + * a streaming {@code DigestInputStream}, making it safe for large files without OOM risk. + */ public class McbbsModpackExportTask extends Task { private final DefaultGameRepository repository; private final String version; @@ -85,6 +100,7 @@ public void execute() throws Exception { LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); Path tempManifest = Files.createTempFile("mcbbs_packmeta_", ".json"); + tempManifest.toFile().deleteOnExit(); try { try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(Files.newOutputStream(tempManifest), StandardCharsets.UTF_8))) { writer.setIndent(" "); @@ -144,6 +160,7 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th if (relativePath.isEmpty()) { return FileVisitResult.CONTINUE; } + // Consistent with zip.putDirectory filter: only skip blacklisted directories if (ModAdviser.match(blackList, relativePath, false)) { return FileVisitResult.SKIP_SUBTREE; } @@ -215,11 +232,15 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO zip.putTextFile(JsonUtils.GSON.toJson(curseManifest), "manifest.json"); zip.putDirectory(runDirectory, "overrides", path -> { - Path resolved = runDirectory.resolve(path); + if (path == null || path.isEmpty()) { + return true; // Root directory, always include + } + String normalizedPath = Path.of(path).normalize().toString().replace(File.separatorChar, '/'); + Path resolved = runDirectory.resolve(normalizedPath); if (Files.isDirectory(resolved)) { - return !ModAdviser.match(blackList, path, false); + return !ModAdviser.match(blackList, normalizedPath, false); } else { - return Modpack.acceptFile(path, blackList, info.getWhitelist()); + return Modpack.acceptFile(normalizedPath, blackList, info.getWhitelist()); } }); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index bb69a3803d8..ab01be4d126 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -33,6 +33,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; +import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -44,6 +45,15 @@ import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; +/** + * Export task for Modrinth modpack format. + *

+ * This implementation streams the index JSON directly to a temporary file using {@link JsonWriter}, + * keeping memory usage low regardless of file count. + *

+ * SHA‑1 and SHA‑512 hashes are computed using {@link DigestUtils#digestToString(String, Path)} + * which uses a streaming {@code DigestInputStream}, safe for large files without OOM risk. + */ public class ModrinthModpackExportTask extends Task { private final DefaultGameRepository repository; private final String version; @@ -75,7 +85,7 @@ private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath) t boolean isDisabled = repository.getModManager(version).isDisabled(file); if (isDisabled) { relativePath = repository.getModManager(version).enableMod(Paths.get(relativePath)).toString(); - file = repository.getRunDirectory(version).resolve(relativePath); + file = repository.getRunDirectory(version).resolve(relativePath).normalize(); } Optional modrinthVersion = Optional.empty(); @@ -146,6 +156,7 @@ public void execute() throws Exception { Set remoteFilePaths = new HashSet<>(); Path tempIndex = Files.createTempFile("modrinth_index_", ".json"); + tempIndex.toFile().deleteOnExit(); try { try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(Files.newOutputStream(tempIndex), StandardCharsets.UTF_8))) { writer.setIndent(" "); @@ -243,15 +254,18 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO zip.putFile(tempIndex, "modrinth.index.json"); zip.putDirectory(runDirectory, "client-overrides", path -> { - String relativePath = path.replace(File.separatorChar, '/'); - Path resolved = runDirectory.resolve(relativePath); + if (path == null || path.isEmpty()) { + return true; + } + String normalizedPath = Path.of(path).normalize().toString().replace(File.separatorChar, '/'); + Path resolved = runDirectory.resolve(normalizedPath); if (Files.isDirectory(resolved)) { - return !ModAdviser.match(blackList, relativePath, false); + return !ModAdviser.match(blackList, normalizedPath, false); } - if (remoteFilePaths.contains(relativePath)) { + if (remoteFilePaths.contains(normalizedPath)) { return false; } - return Modpack.acceptFile(relativePath, blackList, info.getWhitelist()); + return Modpack.acceptFile(normalizedPath, blackList, info.getWhitelist()); }); } } finally { diff --git a/git b/git new file mode 100644 index 00000000000..e69de29bb2d From a4155c5982c23c1a25ccb419dea4638c9daaf98b Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sat, 20 Jun 2026 19:14:04 +0800 Subject: [PATCH 11/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/mod/mcbbs/McbbsModpackExportTask.java | 10 ++++------ .../hmcl/mod/modrinth/ModrinthModpackExportTask.java | 11 ++++------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index d9b8ed23a77..187f68d501d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -36,7 +36,6 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -233,14 +232,13 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO zip.putDirectory(runDirectory, "overrides", path -> { if (path == null || path.isEmpty()) { - return true; // Root directory, always include + return true; } - String normalizedPath = Path.of(path).normalize().toString().replace(File.separatorChar, '/'); - Path resolved = runDirectory.resolve(normalizedPath); + Path resolved = runDirectory.resolve(path); if (Files.isDirectory(resolved)) { - return !ModAdviser.match(blackList, normalizedPath, false); + return !ModAdviser.match(blackList, path, false); } else { - return Modpack.acceptFile(normalizedPath, blackList, info.getWhitelist()); + return Modpack.acceptFile(path, blackList, info.getWhitelist()); } }); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index ab01be4d126..ddc0b7dd023 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -29,11 +29,9 @@ import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.io.Zipper; -import java.io.File; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; -import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -257,15 +255,14 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO if (path == null || path.isEmpty()) { return true; } - String normalizedPath = Path.of(path).normalize().toString().replace(File.separatorChar, '/'); - Path resolved = runDirectory.resolve(normalizedPath); + Path resolved = runDirectory.resolve(path); if (Files.isDirectory(resolved)) { - return !ModAdviser.match(blackList, normalizedPath, false); + return !ModAdviser.match(blackList, path, false); } - if (remoteFilePaths.contains(normalizedPath)) { + if (remoteFilePaths.contains(path)) { return false; } - return Modpack.acceptFile(normalizedPath, blackList, info.getWhitelist()); + return Modpack.acceptFile(path, blackList, info.getWhitelist()); }); } } finally { From 7378b7c4fb747053333e4eca3ea3b83047f8222c Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sat, 20 Jun 2026 19:16:40 +0800 Subject: [PATCH 12/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index ddc0b7dd023..b0452ff91e0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -29,6 +29,7 @@ import org.jackhuang.hmcl.util.DigestUtils; import org.jackhuang.hmcl.util.io.Zipper; +import java.io.File; import java.io.IOException; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; From 7b8e8404422754a967693de018dd7dc23110714e Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sat, 20 Jun 2026 19:25:40 +0800 Subject: [PATCH 13/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mod/mcbbs/McbbsModpackExportTask.java | 9 +++--- .../modrinth/ModrinthModpackExportTask.java | 30 ++++++++++++++++--- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 187f68d501d..11689a5e18d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -34,7 +34,6 @@ import java.io.File; import java.io.IOException; -import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -101,7 +100,7 @@ public void execute() throws Exception { Path tempManifest = Files.createTempFile("mcbbs_packmeta_", ".json"); tempManifest.toFile().deleteOnExit(); try { - try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(Files.newOutputStream(tempManifest), StandardCharsets.UTF_8))) { + try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempManifest, StandardCharsets.UTF_8))) { writer.setIndent(" "); writer.beginObject(); @@ -192,8 +191,10 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO writer.name("minMemory").value(info.getMinMemory()); writer.name("supportJava").beginArray(); if (info.getSupportedJavaVersions() != null) { - for (int ver : info.getSupportedJavaVersions()) { - writer.value(ver); + for (Integer ver : info.getSupportedJavaVersions()) { + if (ver != null) { + writer.value(ver); + } } } writer.endArray(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index b0452ff91e0..bd6654d9bba 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -31,7 +31,6 @@ import java.io.File; import java.io.IOException; -import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -76,13 +75,14 @@ public ModrinthModpackExportTask(DefaultGameRepository repository, String versio }); } - private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath) throws IOException { + private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath, Set temporarilyEnabledFiles) throws IOException { if (info.isNoCreateRemoteFiles()) { return null; } boolean isDisabled = repository.getModManager(version).isDisabled(file); if (isDisabled) { + temporarilyEnabledFiles.add(file); // record original disabled file relativePath = repository.getModManager(version).enableMod(Paths.get(relativePath)).toString(); file = repository.getRunDirectory(version).resolve(relativePath).normalize(); } @@ -153,11 +153,12 @@ public void execute() throws Exception { String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; Set remoteFilePaths = new HashSet<>(); + Set temporarilyEnabledFiles = new HashSet<>(); // track disabled mods that were temporarily enabled Path tempIndex = Files.createTempFile("modrinth_index_", ".json"); tempIndex.toFile().deleteOnExit(); try { - try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(Files.newOutputStream(tempIndex), StandardCharsets.UTF_8))) { + try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempIndex, StandardCharsets.UTF_8))) { writer.setIndent(" "); writer.beginObject(); @@ -190,7 +191,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO ModrinthManifest.File fileEntry = null; try { - fileEntry = tryGetRemoteFile(file, relativePath); + fileEntry = tryGetRemoteFile(file, relativePath, temporarilyEnabledFiles); } catch (IOException e) { LOG.warning("Failed to process file: " + file, e); } @@ -268,6 +269,27 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO } } finally { Files.deleteIfExists(tempIndex); + // Restore disabled mods to their original disabled state + for (Path disabledFile : temporarilyEnabledFiles) { + try { + // disabledFile is the original .disabled file path (e.g., mods/SomeMod.jar.disabled) + // The enabled version is the same path without the .disabled suffix + String fileName = disabledFile.getFileName().toString(); + if (fileName.endsWith(".disabled")) { + String enabledName = fileName.substring(0, fileName.length() - 9); // remove ".disabled" + Path enabledFile = disabledFile.resolveSibling(enabledName); + if (Files.exists(enabledFile)) { + // Move enabled file back to .disabled (overwrite if exists) + Files.move(enabledFile, disabledFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + LOG.info("Restored disabled mod: " + disabledFile); + } else { + LOG.warning("Enabled file not found, cannot restore: " + enabledFile); + } + } + } catch (IOException e) { + LOG.warning("Failed to restore disabled mod: " + disabledFile, e); + } + } } } From d2ab31a34e7623dc5dd4950daefd3a73f655985b Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sat, 20 Jun 2026 22:04:02 +0800 Subject: [PATCH 14/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D=E2=80=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java | 2 +- .../jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 11689a5e18d..77dad3012d0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -98,7 +98,7 @@ public void execute() throws Exception { LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); Path tempManifest = Files.createTempFile("mcbbs_packmeta_", ".json"); - tempManifest.toFile().deleteOnExit(); + try { try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempManifest, StandardCharsets.UTF_8))) { writer.setIndent(" "); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index bd6654d9bba..5d25bf412a9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -156,7 +156,6 @@ public void execute() throws Exception { Set temporarilyEnabledFiles = new HashSet<>(); // track disabled mods that were temporarily enabled Path tempIndex = Files.createTempFile("modrinth_index_", ".json"); - tempIndex.toFile().deleteOnExit(); try { try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempIndex, StandardCharsets.UTF_8))) { writer.setIndent(" "); From 0af3dd7df116758852d20d937e0f19d4d22cdca7 Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sat, 20 Jun 2026 22:10:07 +0800 Subject: [PATCH 15/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D=E2=80=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modrinth/ModrinthModpackExportTask.java | 110 +++++++++--------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 5d25bf412a9..38d1d3a131b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -82,7 +82,7 @@ private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath, S boolean isDisabled = repository.getModManager(version).isDisabled(file); if (isDisabled) { - temporarilyEnabledFiles.add(file); // record original disabled file + temporarilyEnabledFiles.add(file); relativePath = repository.getModManager(version).enableMod(Paths.get(relativePath)).toString(); file = repository.getRunDirectory(version).resolve(relativePath).normalize(); } @@ -153,7 +153,22 @@ public void execute() throws Exception { String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; Set remoteFilePaths = new HashSet<>(); - Set temporarilyEnabledFiles = new HashSet<>(); // track disabled mods that were temporarily enabled + Set temporarilyEnabledFiles = new HashSet<>(); + + // First, collect all files to avoid modifying directory structure during walkFileTree + List allFiles = new ArrayList<>(); + for (String dir : resourceDirs) { + Path dirPath = runDirectory.resolve(dir); + if (Files.exists(dirPath)) { + Files.walkFileTree(dirPath, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + allFiles.add(file); + return FileVisitResult.CONTINUE; + } + }); + } + } Path tempIndex = Files.createTempFile("modrinth_index_", ".json"); try { @@ -173,54 +188,46 @@ public void execute() throws Exception { Set processedPaths = new HashSet<>(); - for (String dir : resourceDirs) { - Path dirPath = runDirectory.resolve(dir); - if (Files.exists(dirPath)) { - Files.walkFileTree(dirPath, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - if (!whitelistSet.contains(relativePath)) { - return FileVisitResult.CONTINUE; - } - if (processedPaths.contains(relativePath)) { - return FileVisitResult.CONTINUE; - } - processedPaths.add(relativePath); - - ModrinthManifest.File fileEntry = null; - try { - fileEntry = tryGetRemoteFile(file, relativePath, temporarilyEnabledFiles); - } catch (IOException e) { - LOG.warning("Failed to process file: " + file, e); - } - if (fileEntry != null) { - remoteFilePaths.add(relativePath); - writer.beginObject(); - writer.name("path").value(fileEntry.getPath()); - writer.name("hashes").beginObject(); - for (Map.Entry hash : fileEntry.getHashes().entrySet()) { - writer.name(hash.getKey()).value(hash.getValue()); - } - writer.endObject(); - if (fileEntry.getEnv() != null) { - writer.name("env").beginObject(); - for (Map.Entry env : fileEntry.getEnv().entrySet()) { - writer.name(env.getKey()).value(env.getValue()); - } - writer.endObject(); - } - writer.name("downloads").beginArray(); - for (String url : fileEntry.getDownloads()) { - writer.value(url); - } - writer.endArray(); - writer.name("fileSize").value(fileEntry.getFileSize()); - writer.endObject(); - } - return FileVisitResult.CONTINUE; + // Now process the collected files safely + for (Path file : allFiles) { + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + if (!whitelistSet.contains(relativePath)) { + continue; + } + if (processedPaths.contains(relativePath)) { + continue; + } + processedPaths.add(relativePath); + + ModrinthManifest.File fileEntry = null; + try { + fileEntry = tryGetRemoteFile(file, relativePath, temporarilyEnabledFiles); + } catch (IOException e) { + LOG.warning("Failed to process file: " + file, e); + } + if (fileEntry != null) { + remoteFilePaths.add(relativePath); + writer.beginObject(); + writer.name("path").value(fileEntry.getPath()); + writer.name("hashes").beginObject(); + for (Map.Entry hash : fileEntry.getHashes().entrySet()) { + writer.name(hash.getKey()).value(hash.getValue()); + } + writer.endObject(); + if (fileEntry.getEnv() != null) { + writer.name("env").beginObject(); + for (Map.Entry env : fileEntry.getEnv().entrySet()) { + writer.name(env.getKey()).value(env.getValue()); } - }); + writer.endObject(); + } + writer.name("downloads").beginArray(); + for (String url : fileEntry.getDownloads()) { + writer.value(url); + } + writer.endArray(); + writer.name("fileSize").value(fileEntry.getFileSize()); + writer.endObject(); } } @@ -271,14 +278,11 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO // Restore disabled mods to their original disabled state for (Path disabledFile : temporarilyEnabledFiles) { try { - // disabledFile is the original .disabled file path (e.g., mods/SomeMod.jar.disabled) - // The enabled version is the same path without the .disabled suffix String fileName = disabledFile.getFileName().toString(); if (fileName.endsWith(".disabled")) { - String enabledName = fileName.substring(0, fileName.length() - 9); // remove ".disabled" + String enabledName = fileName.substring(0, fileName.length() - 9); Path enabledFile = disabledFile.resolveSibling(enabledName); if (Files.exists(enabledFile)) { - // Move enabled file back to .disabled (overwrite if exists) Files.move(enabledFile, disabledFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); LOG.info("Restored disabled mod: " + disabledFile); } else { From ff37c4a67edc6d4cbbcec930fd7d124547f35183 Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sat, 20 Jun 2026 22:20:15 +0800 Subject: [PATCH 16/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D=E2=80=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modrinth/ModrinthModpackExportTask.java | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 38d1d3a131b..061fae75cf5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -82,8 +82,10 @@ private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath, S boolean isDisabled = repository.getModManager(version).isDisabled(file); if (isDisabled) { - temporarilyEnabledFiles.add(file); - relativePath = repository.getModManager(version).enableMod(Paths.get(relativePath)).toString(); + // Enable the mod and record the new path for later restoration + Path enabledPath = repository.getModManager(version).enableMod(Paths.get(relativePath)); + temporarilyEnabledFiles.add(enabledPath); + relativePath = enabledPath.toString(); file = repository.getRunDirectory(version).resolve(relativePath).normalize(); } @@ -149,11 +151,12 @@ public void execute() throws Exception { .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); - Set whitelistSet = new HashSet<>(info.getWhitelist()); + // Defensive handling: if whitelist is null, treat as empty + Set whitelistSet = info.getWhitelist() != null ? new HashSet<>(info.getWhitelist()) : Collections.emptySet(); String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; Set remoteFilePaths = new HashSet<>(); - Set temporarilyEnabledFiles = new HashSet<>(); + Set temporarilyEnabledFiles = new HashSet<>(); // store enabled file paths // First, collect all files to avoid modifying directory structure during walkFileTree List allFiles = new ArrayList<>(); @@ -275,22 +278,13 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO } } finally { Files.deleteIfExists(tempIndex); - // Restore disabled mods to their original disabled state - for (Path disabledFile : temporarilyEnabledFiles) { + // Restore disabled mods using the ModManager API + for (Path enabledPath : temporarilyEnabledFiles) { try { - String fileName = disabledFile.getFileName().toString(); - if (fileName.endsWith(".disabled")) { - String enabledName = fileName.substring(0, fileName.length() - 9); - Path enabledFile = disabledFile.resolveSibling(enabledName); - if (Files.exists(enabledFile)) { - Files.move(enabledFile, disabledFile, java.nio.file.StandardCopyOption.REPLACE_EXISTING); - LOG.info("Restored disabled mod: " + disabledFile); - } else { - LOG.warning("Enabled file not found, cannot restore: " + enabledFile); - } - } + repository.getModManager(version).disableMod(enabledPath); + LOG.info("Restored disabled mod: " + enabledPath); } catch (IOException e) { - LOG.warning("Failed to restore disabled mod: " + disabledFile, e); + LOG.warning("Failed to restore disabled mod: " + enabledPath, e); } } } From 7062ce891c667e5830ac2b0c5abb5632a625821e Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sat, 20 Jun 2026 22:28:38 +0800 Subject: [PATCH 17/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D=E2=80=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mod/mcbbs/McbbsModpackExportTask.java | 11 +- .../modrinth/ModrinthModpackExportTask.java | 114 +++++++++--------- 2 files changed, 61 insertions(+), 64 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 77dad3012d0..3f80622ae1b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -38,6 +38,7 @@ import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; @@ -98,7 +99,7 @@ public void execute() throws Exception { LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); Path tempManifest = Files.createTempFile("mcbbs_packmeta_", ".json"); - + tempManifest.toFile().deleteOnExit(); // final backup cleanup try { try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempManifest, StandardCharsets.UTF_8))) { writer.setIndent(" "); @@ -235,11 +236,13 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO if (path == null || path.isEmpty()) { return true; } - Path resolved = runDirectory.resolve(path); + // Normalize path for consistency with other parts + String normalizedPath = Paths.get(path).normalize().toString().replace(File.separatorChar, '/'); + Path resolved = runDirectory.resolve(normalizedPath); if (Files.isDirectory(resolved)) { - return !ModAdviser.match(blackList, path, false); + return !ModAdviser.match(blackList, normalizedPath, false); } else { - return Modpack.acceptFile(path, blackList, info.getWhitelist()); + return Modpack.acceptFile(normalizedPath, blackList, info.getWhitelist()); } }); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 061fae75cf5..bb6a8155c4e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -35,7 +35,6 @@ import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; @@ -82,11 +81,12 @@ private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath, S boolean isDisabled = repository.getModManager(version).isDisabled(file); if (isDisabled) { - // Enable the mod and record the new path for later restoration - Path enabledPath = repository.getModManager(version).enableMod(Paths.get(relativePath)); + // Use absolute path to avoid working directory issues + Path enabledPath = repository.getModManager(version).enableMod(file); temporarilyEnabledFiles.add(enabledPath); - relativePath = enabledPath.toString(); - file = repository.getRunDirectory(version).resolve(relativePath).normalize(); + file = enabledPath; + relativePath = repository.getRunDirectory(version).relativize(file) + .normalize().toString().replace(File.separatorChar, '/'); } Optional modrinthVersion = Optional.empty(); @@ -156,24 +156,10 @@ public void execute() throws Exception { String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; Set remoteFilePaths = new HashSet<>(); - Set temporarilyEnabledFiles = new HashSet<>(); // store enabled file paths - - // First, collect all files to avoid modifying directory structure during walkFileTree - List allFiles = new ArrayList<>(); - for (String dir : resourceDirs) { - Path dirPath = runDirectory.resolve(dir); - if (Files.exists(dirPath)) { - Files.walkFileTree(dirPath, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - allFiles.add(file); - return FileVisitResult.CONTINUE; - } - }); - } - } + Set temporarilyEnabledFiles = new HashSet<>(); Path tempIndex = Files.createTempFile("modrinth_index_", ".json"); + tempIndex.toFile().deleteOnExit(); // final backup cleanup try { try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempIndex, StandardCharsets.UTF_8))) { writer.setIndent(" "); @@ -191,46 +177,54 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO Set processedPaths = new HashSet<>(); - // Now process the collected files safely - for (Path file : allFiles) { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - if (!whitelistSet.contains(relativePath)) { - continue; - } - if (processedPaths.contains(relativePath)) { - continue; - } - processedPaths.add(relativePath); - - ModrinthManifest.File fileEntry = null; - try { - fileEntry = tryGetRemoteFile(file, relativePath, temporarilyEnabledFiles); - } catch (IOException e) { - LOG.warning("Failed to process file: " + file, e); - } - if (fileEntry != null) { - remoteFilePaths.add(relativePath); - writer.beginObject(); - writer.name("path").value(fileEntry.getPath()); - writer.name("hashes").beginObject(); - for (Map.Entry hash : fileEntry.getHashes().entrySet()) { - writer.name(hash.getKey()).value(hash.getValue()); - } - writer.endObject(); - if (fileEntry.getEnv() != null) { - writer.name("env").beginObject(); - for (Map.Entry env : fileEntry.getEnv().entrySet()) { - writer.name(env.getKey()).value(env.getValue()); + for (String dir : resourceDirs) { + Path dirPath = runDirectory.resolve(dir); + if (Files.exists(dirPath)) { + Files.walkFileTree(dirPath, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + if (!whitelistSet.contains(relativePath)) { + return FileVisitResult.CONTINUE; + } + if (processedPaths.contains(relativePath)) { + return FileVisitResult.CONTINUE; + } + processedPaths.add(relativePath); + + ModrinthManifest.File fileEntry = null; + try { + fileEntry = tryGetRemoteFile(file, relativePath, temporarilyEnabledFiles); + } catch (IOException e) { + LOG.warning("Failed to process file: " + file, e); + } + if (fileEntry != null) { + remoteFilePaths.add(relativePath); + writer.beginObject(); + writer.name("path").value(fileEntry.getPath()); + writer.name("hashes").beginObject(); + for (Map.Entry hash : fileEntry.getHashes().entrySet()) { + writer.name(hash.getKey()).value(hash.getValue()); + } + writer.endObject(); + if (fileEntry.getEnv() != null) { + writer.name("env").beginObject(); + for (Map.Entry env : fileEntry.getEnv().entrySet()) { + writer.name(env.getKey()).value(env.getValue()); + } + writer.endObject(); + } + writer.name("downloads").beginArray(); + for (String url : fileEntry.getDownloads()) { + writer.value(url); + } + writer.endArray(); + writer.name("fileSize").value(fileEntry.getFileSize()); + writer.endObject(); + } + return FileVisitResult.CONTINUE; } - writer.endObject(); - } - writer.name("downloads").beginArray(); - for (String url : fileEntry.getDownloads()) { - writer.value(url); - } - writer.endArray(); - writer.name("fileSize").value(fileEntry.getFileSize()); - writer.endObject(); + }); } } From 8b78f7236feeac1a0c26f71ed15a3c6140b69ecb Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sat, 20 Jun 2026 22:35:51 +0800 Subject: [PATCH 18/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D=E2=80=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modrinth/ModrinthModpackExportTask.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index bb6a8155c4e..038b9513a6d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -253,6 +253,17 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO writer.endObject(); } + // CRITICAL: Restore disabled mods BEFORE zipping, so they remain .disabled in the final pack + for (Path enabledPath : temporarilyEnabledFiles) { + try { + repository.getModManager(version).disableMod(enabledPath); + LOG.info("Restored disabled mod: " + enabledPath); + } catch (IOException e) { + LOG.warning("Failed to restore disabled mod: " + enabledPath, e); + } + } + temporarilyEnabledFiles.clear(); + try (var zip = new Zipper(modpackFile)) { zip.putFile(tempIndex, "modrinth.index.json"); @@ -272,15 +283,16 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO } } finally { Files.deleteIfExists(tempIndex); - // Restore disabled mods using the ModManager API + // Safety net: restore any mods that might still be enabled if an exception occurred after enabling but before restoration for (Path enabledPath : temporarilyEnabledFiles) { try { repository.getModManager(version).disableMod(enabledPath); - LOG.info("Restored disabled mod: " + enabledPath); + LOG.info("Restored disabled mod (finally): " + enabledPath); } catch (IOException e) { - LOG.warning("Failed to restore disabled mod: " + enabledPath, e); + LOG.warning("Failed to restore disabled mod (finally): " + enabledPath, e); } } + temporarilyEnabledFiles.clear(); } } From 24d023993717356215d5ec55b0c4923e5ccc9da3 Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sat, 20 Jun 2026 23:33:05 +0800 Subject: [PATCH 19/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D=E2=80=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mod/mcbbs/McbbsModpackExportTask.java | 13 +- .../modrinth/ModrinthModpackExportTask.java | 341 ++++++++---------- git | 0 3 files changed, 157 insertions(+), 197 deletions(-) delete mode 100644 git diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 3f80622ae1b..9b1107ae6fd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -99,7 +99,7 @@ public void execute() throws Exception { LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); Path tempManifest = Files.createTempFile("mcbbs_packmeta_", ".json"); - tempManifest.toFile().deleteOnExit(); // final backup cleanup + tempManifest.toFile().deleteOnExit(); try { try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempManifest, StandardCharsets.UTF_8))) { writer.setIndent(" "); @@ -107,14 +107,14 @@ public void execute() throws Exception { writer.name("manifestType").value(McbbsModpackManifest.MANIFEST_TYPE); writer.name("manifestVersion").value(2); - writer.name("name").value(info.getName()); - writer.name("version").value(info.getVersion()); - writer.name("author").value(info.getAuthor()); - writer.name("description").value(info.getDescription()); + if (info.getName() != null) writer.name("name").value(info.getName()); + if (info.getVersion() != null) writer.name("version").value(info.getVersion()); + if (info.getAuthor() != null) writer.name("author").value(info.getAuthor()); + if (info.getDescription() != null) writer.name("description").value(info.getDescription()); if (info.getFileApi() != null) { writer.name("fileApi").value(StringUtils.removeSuffix(info.getFileApi(), "/")); } - writer.name("url").value(info.getUrl()); + if (info.getUrl() != null) writer.name("url").value(info.getUrl()); writer.name("forceUpdate").value(info.isForceUpdate()); writer.name("origin").beginArray(); @@ -236,7 +236,6 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO if (path == null || path.isEmpty()) { return true; } - // Normalize path for consistency with other parts String normalizedPath = Paths.get(path).normalize().toString().replace(File.separatorChar, '/'); Path resolved = runDirectory.resolve(normalizedPath); if (Files.isDirectory(resolved)) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 038b9513a6d..9b1107ae6fd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2026 huangyuhui and contributors + * Copyright (C) 2020 huangyuhui and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.mod.modrinth; +package org.jackhuang.hmcl.mod.mcbbs; import com.google.gson.stream.JsonWriter; import org.jackhuang.hmcl.download.LibraryAnalyzer; @@ -23,10 +23,13 @@ import org.jackhuang.hmcl.mod.ModAdviser; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.ModpackExportInfo; -import org.jackhuang.hmcl.mod.RemoteMod; -import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; +import org.jackhuang.hmcl.mod.curse.CurseManifest; +import org.jackhuang.hmcl.mod.curse.CurseManifestMinecraft; +import org.jackhuang.hmcl.mod.curse.CurseManifestModLoader; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.Zipper; import java.io.File; @@ -35,29 +38,38 @@ import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; /** - * Export task for Modrinth modpack format. + * Export task for MCBBS modpack format. *

- * This implementation streams the index JSON directly to a temporary file using {@link JsonWriter}, - * keeping memory usage low regardless of file count. + * Note: This implementation performs two passes over the game directory: + * 1. First pass: walks the file tree to generate the manifest JSON (calculating SHA‑1 hashes). + * 2. Second pass: compresses the files into the final ZIP. *

- * SHA‑1 and SHA‑512 hashes are computed using {@link DigestUtils#digestToString(String, Path)} - * which uses a streaming {@code DigestInputStream}, safe for large files without OOM risk. + * This double traversal is a deliberate trade‑off to avoid holding all file information in memory, + * which would cause OutOfMemoryError on very large modpacks. The streaming JSON writer writes the + * manifest to a temporary file, keeping memory usage constant regardless of file count. + *

+ * SHA‑1 hashes are computed using {@link DigestUtils#digestToString(String, Path)} which uses + * a streaming {@code DigestInputStream}, making it safe for large files without OOM risk. */ -public class ModrinthModpackExportTask extends Task { +public class McbbsModpackExportTask extends Task { private final DefaultGameRepository repository; private final String version; private final ModpackExportInfo info; private final Path modpackFile; - public ModrinthModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo info, Path modpackFile) { + public McbbsModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo info, Path modpackFile) { this.repository = repository; this.version = version; this.info = info.validate(); @@ -74,71 +86,6 @@ public ModrinthModpackExportTask(DefaultGameRepository repository, String versio }); } - private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath, Set temporarilyEnabledFiles) throws IOException { - if (info.isNoCreateRemoteFiles()) { - return null; - } - - boolean isDisabled = repository.getModManager(version).isDisabled(file); - if (isDisabled) { - // Use absolute path to avoid working directory issues - Path enabledPath = repository.getModManager(version).enableMod(file); - temporarilyEnabledFiles.add(enabledPath); - file = enabledPath; - relativePath = repository.getRunDirectory(version).relativize(file) - .normalize().toString().replace(File.separatorChar, '/'); - } - - Optional modrinthVersion = Optional.empty(); - Optional curseForgeVersion = Optional.empty(); - - try { - modrinthVersion = ModrinthRemoteModRepository.MODS.getRemoteVersionByLocalFile(file); - } catch (IOException e) { - LOG.warning("Failed to get remote file from Modrinth for: " + file, e); - } - - if (!info.isSkipCurseForgeRemoteFiles() && CurseForgeRemoteModRepository.isAvailable()) { - try { - curseForgeVersion = CurseForgeRemoteModRepository.MODS.getRemoteVersionByLocalFile(file); - } catch (IOException e) { - LOG.warning("Failed to get remote file from CurseForge for: " + file, e); - } - } - - if (modrinthVersion.isEmpty() && curseForgeVersion.isEmpty()) { - return null; - } - - Map hashes = new HashMap<>(); - hashes.put("sha1", DigestUtils.digestToString("SHA-1", file)); - hashes.put("sha512", DigestUtils.digestToString("SHA-512", file)); - - Map env = null; - if (isDisabled) { - env = new HashMap<>(); - env.put("client", "optional"); - } - - List downloads = new ArrayList<>(); - if (modrinthVersion.isPresent()) - downloads.add(modrinthVersion.get().getFile().getUrl()); - if (curseForgeVersion.isPresent()) - downloads.add(curseForgeVersion.get().getFile().getUrl()); - - long fileSize = Files.size(file); - if (fileSize > Integer.MAX_VALUE) { - LOG.warning("File " + relativePath + " is too large (size: " + fileSize + " bytes), precision may be lost when converting to int"); - } - return new ModrinthManifest.File( - relativePath, - hashes, - env, - downloads, - (int) fileSize - ); - } - @Override public void execute() throws Exception { ArrayList blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST); @@ -151,152 +98,166 @@ public void execute() throws Exception { .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); - // Defensive handling: if whitelist is null, treat as empty - Set whitelistSet = info.getWhitelist() != null ? new HashSet<>(info.getWhitelist()) : Collections.emptySet(); - - String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; - Set remoteFilePaths = new HashSet<>(); - Set temporarilyEnabledFiles = new HashSet<>(); - - Path tempIndex = Files.createTempFile("modrinth_index_", ".json"); - tempIndex.toFile().deleteOnExit(); // final backup cleanup + Path tempManifest = Files.createTempFile("mcbbs_packmeta_", ".json"); + tempManifest.toFile().deleteOnExit(); try { - try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempIndex, StandardCharsets.UTF_8))) { + try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempManifest, StandardCharsets.UTF_8))) { writer.setIndent(" "); writer.beginObject(); - writer.name("formatVersion").value(1); - writer.name("game").value("minecraft"); - writer.name("versionId").value(info.getVersion()); - writer.name("name").value(info.getName()); - if (info.getDescription() != null) { - writer.name("summary").value(info.getDescription()); + writer.name("manifestType").value(McbbsModpackManifest.MANIFEST_TYPE); + writer.name("manifestVersion").value(2); + if (info.getName() != null) writer.name("name").value(info.getName()); + if (info.getVersion() != null) writer.name("version").value(info.getVersion()); + if (info.getAuthor() != null) writer.name("author").value(info.getAuthor()); + if (info.getDescription() != null) writer.name("description").value(info.getDescription()); + if (info.getFileApi() != null) { + writer.name("fileApi").value(StringUtils.removeSuffix(info.getFileApi(), "/")); } + if (info.getUrl() != null) writer.name("url").value(info.getUrl()); + writer.name("forceUpdate").value(info.isForceUpdate()); + + writer.name("origin").beginArray(); + if (info.getOrigins() != null) { + for (McbbsModpackManifest.Origin origin : info.getOrigins()) { + writer.beginObject(); + writer.name("type").value(origin.getType()); + writer.name("id").value(origin.getId()); + writer.endObject(); + } + } + writer.endArray(); - writer.name("files").beginArray(); + writer.name("addons").beginArray(); + writer.beginObject(); + writer.name("id").value(MINECRAFT.getPatchId()); + writer.name("version").value(gameVersion); + writer.endObject(); - Set processedPaths = new HashSet<>(); + LibraryAnalyzer.LibraryType[] addonTypes = { + FORGE, CLEANROOM, NEO_FORGE, LITELOADER, OPTIFINE, FABRIC, QUILT, LEGACY_FABRIC + }; + for (LibraryAnalyzer.LibraryType type : addonTypes) { + Optional addonVersion = analyzer.getVersion(type); + if (addonVersion.isPresent()) { + writer.beginObject(); + writer.name("id").value(type.getPatchId()); + writer.name("version").value(addonVersion.get()); + writer.endObject(); + } + } + writer.endArray(); - for (String dir : resourceDirs) { - Path dirPath = runDirectory.resolve(dir); - if (Files.exists(dirPath)) { - Files.walkFileTree(dirPath, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - if (!whitelistSet.contains(relativePath)) { - return FileVisitResult.CONTINUE; - } - if (processedPaths.contains(relativePath)) { - return FileVisitResult.CONTINUE; - } - processedPaths.add(relativePath); + writer.name("libraries").beginArray(); + writer.endArray(); - ModrinthManifest.File fileEntry = null; - try { - fileEntry = tryGetRemoteFile(file, relativePath, temporarilyEnabledFiles); - } catch (IOException e) { - LOG.warning("Failed to process file: " + file, e); - } - if (fileEntry != null) { - remoteFilePaths.add(relativePath); - writer.beginObject(); - writer.name("path").value(fileEntry.getPath()); - writer.name("hashes").beginObject(); - for (Map.Entry hash : fileEntry.getHashes().entrySet()) { - writer.name(hash.getKey()).value(hash.getValue()); - } - writer.endObject(); - if (fileEntry.getEnv() != null) { - writer.name("env").beginObject(); - for (Map.Entry env : fileEntry.getEnv().entrySet()) { - writer.name(env.getKey()).value(env.getValue()); - } - writer.endObject(); - } - writer.name("downloads").beginArray(); - for (String url : fileEntry.getDownloads()) { - writer.value(url); - } - writer.endArray(); - writer.name("fileSize").value(fileEntry.getFileSize()); - writer.endObject(); - } - return FileVisitResult.CONTINUE; - } - }); + writer.name("files").beginArray(); + Files.walkFileTree(runDirectory, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + String relativePath = runDirectory.relativize(dir).normalize().toString().replace(File.separatorChar, '/'); + if (relativePath.isEmpty()) { + return FileVisitResult.CONTINUE; + } + // Consistent with zip.putDirectory filter: only skip blacklisted directories + if (ModAdviser.match(blackList, relativePath, false)) { + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; } - } + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + if (Modpack.acceptFile(relativePath, blackList, info.getWhitelist())) { + String sha1 = DigestUtils.digestToString("SHA-1", file); + writer.beginObject(); + writer.name("type").value("addon"); + writer.name("force").value(true); + writer.name("path").value(relativePath); + writer.name("hash").value(sha1); + writer.endObject(); + } + return FileVisitResult.CONTINUE; + } + }); writer.endArray(); - writer.name("dependencies").beginObject(); - writer.name("minecraft").value(gameVersion); - Optional forgeVersion = analyzer.getVersion(FORGE); - if (forgeVersion.isPresent()) { - writer.name("forge").value(forgeVersion.get()); - } - Optional neoForgeVersion = analyzer.getVersion(NEO_FORGE); - if (neoForgeVersion.isPresent()) { - writer.name("neoforge").value(neoForgeVersion.get()); + writer.name("settings").beginObject(); + writer.name("install_mods").value(true); + writer.name("install_resourcepack").value(true); + writer.endObject(); + + writer.name("launchInfo").beginObject(); + writer.name("minMemory").value(info.getMinMemory()); + writer.name("supportJava").beginArray(); + if (info.getSupportedJavaVersions() != null) { + for (Integer ver : info.getSupportedJavaVersions()) { + if (ver != null) { + writer.value(ver); + } + } } - Optional fabricVersion = analyzer.getVersion(FABRIC); - if (fabricVersion.isPresent()) { - writer.name("fabric-loader").value(fabricVersion.get()); + writer.endArray(); + + writer.name("launchArgument").beginArray(); + List launchArgs = StringUtils.tokenize(info.getLaunchArguments()); + if (launchArgs != null) { + for (String arg : launchArgs) { + writer.value(arg); + } } - Optional quiltVersion = analyzer.getVersion(QUILT); - if (quiltVersion.isPresent()) { - writer.name("quilt-loader").value(quiltVersion.get()); + writer.endArray(); + + writer.name("javaArgument").beginArray(); + List javaArgs = StringUtils.tokenize(info.getJavaArguments()); + if (javaArgs != null) { + for (String arg : javaArgs) { + writer.value(arg); + } } + writer.endArray(); writer.endObject(); writer.endObject(); } - // CRITICAL: Restore disabled mods BEFORE zipping, so they remain .disabled in the final pack - for (Path enabledPath : temporarilyEnabledFiles) { - try { - repository.getModManager(version).disableMod(enabledPath); - LOG.info("Restored disabled mod: " + enabledPath); - } catch (IOException e) { - LOG.warning("Failed to restore disabled mod: " + enabledPath, e); - } - } - temporarilyEnabledFiles.clear(); - try (var zip = new Zipper(modpackFile)) { - zip.putFile(tempIndex, "modrinth.index.json"); + zip.putFile(tempManifest, "mcbbs.packmeta"); + + List modLoaders = new ArrayList<>(); + analyzer.getVersion(FORGE).ifPresent(forgeVersion -> modLoaders.add(new CurseManifestModLoader("forge-" + forgeVersion, true))); + analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion -> modLoaders.add(new CurseManifestModLoader("neoforge-" + neoForgeVersion, true))); + analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> modLoaders.add(new CurseManifestModLoader("fabric-" + fabricVersion, true))); + CurseManifest curseManifest = new CurseManifest(CurseManifest.MINECRAFT_MODPACK, 1, info.getName(), info.getVersion(), info.getAuthor(), "overrides", new CurseManifestMinecraft(gameVersion, modLoaders), Collections.emptyList()); + zip.putTextFile(JsonUtils.GSON.toJson(curseManifest), "manifest.json"); - zip.putDirectory(runDirectory, "client-overrides", path -> { + zip.putDirectory(runDirectory, "overrides", path -> { if (path == null || path.isEmpty()) { return true; } - Path resolved = runDirectory.resolve(path); + String normalizedPath = Paths.get(path).normalize().toString().replace(File.separatorChar, '/'); + Path resolved = runDirectory.resolve(normalizedPath); if (Files.isDirectory(resolved)) { - return !ModAdviser.match(blackList, path, false); + return !ModAdviser.match(blackList, normalizedPath, false); + } else { + return Modpack.acceptFile(normalizedPath, blackList, info.getWhitelist()); } - if (remoteFilePaths.contains(path)) { - return false; - } - return Modpack.acceptFile(path, blackList, info.getWhitelist()); }); } } finally { - Files.deleteIfExists(tempIndex); - // Safety net: restore any mods that might still be enabled if an exception occurred after enabling but before restoration - for (Path enabledPath : temporarilyEnabledFiles) { - try { - repository.getModManager(version).disableMod(enabledPath); - LOG.info("Restored disabled mod (finally): " + enabledPath); - } catch (IOException e) { - LOG.warning("Failed to restore disabled mod (finally): " + enabledPath, e); - } - } - temporarilyEnabledFiles.clear(); + Files.deleteIfExists(tempManifest); } } public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options() - .requireNoCreateRemoteFiles() - .requireSkipCurseForgeRemoteFiles(); + .requireFileApi(true) + .requireUrl() + .requireForceUpdate() + .requireMinMemory() + .requireAuthlibInjectorServer() + .requireJavaArguments() + .requireLaunchArguments() + .requireOrigins() + .requireAuthor(); } diff --git a/git b/git deleted file mode 100644 index e69de29bb2d..00000000000 From 409a62b59a6db3b0f52aed248170b0b57bfe534d Mon Sep 17 00:00:00 2001 From: hzy_2012 <3681596340@qq.com> Date: Sun, 21 Jun 2026 10:19:00 +0800 Subject: [PATCH 20/20] =?UTF-8?q?=E9=92=88=E5=AF=B9#issues6086=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=95=B4=E5=90=88=E5=8C=85=E6=97=B6=E5=86=85=E5=AD=98?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mod/mcbbs/McbbsModpackExportTask.java | 15 +- .../modrinth/ModrinthModpackExportTask.java | 347 ++++++++++-------- 2 files changed, 210 insertions(+), 152 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java index 9b1107ae6fd..e06623eb0e8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/mcbbs/McbbsModpackExportTask.java @@ -99,7 +99,7 @@ public void execute() throws Exception { LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); Path tempManifest = Files.createTempFile("mcbbs_packmeta_", ".json"); - tempManifest.toFile().deleteOnExit(); + final boolean hasWhitelist = info.getWhitelist() != null && !info.getWhitelist().isEmpty(); try { try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempManifest, StandardCharsets.UTF_8))) { writer.setIndent(" "); @@ -159,7 +159,12 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th if (relativePath.isEmpty()) { return FileVisitResult.CONTINUE; } - // Consistent with zip.putDirectory filter: only skip blacklisted directories + // If whitelist is used, we must not skip any directory based on blacklist, + // because files inside blacklisted directories might be whitelisted. + if (hasWhitelist) { + return FileVisitResult.CONTINUE; + } + // Otherwise, skip blacklisted directories for performance. if (ModAdviser.match(blackList, relativePath, false)) { return FileVisitResult.SKIP_SUBTREE; } @@ -232,6 +237,8 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO CurseManifest curseManifest = new CurseManifest(CurseManifest.MINECRAFT_MODPACK, 1, info.getName(), info.getVersion(), info.getAuthor(), "overrides", new CurseManifestMinecraft(gameVersion, modLoaders), Collections.emptyList()); zip.putTextFile(JsonUtils.GSON.toJson(curseManifest), "manifest.json"); + // Directory filter for zipping: if whitelist is used, always allow directories to enter. + // File-level filtering is handled by Modpack.acceptFile inside the filter. zip.putDirectory(runDirectory, "overrides", path -> { if (path == null || path.isEmpty()) { return true; @@ -239,6 +246,10 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO String normalizedPath = Paths.get(path).normalize().toString().replace(File.separatorChar, '/'); Path resolved = runDirectory.resolve(normalizedPath); if (Files.isDirectory(resolved)) { + // If whitelist is non-empty, we must not skip any directory. + if (hasWhitelist) { + return true; + } return !ModAdviser.match(blackList, normalizedPath, false); } else { return Modpack.acceptFile(normalizedPath, blackList, info.getWhitelist()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java index 9b1107ae6fd..f258fa3664d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/modrinth/ModrinthModpackExportTask.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2026 huangyuhui and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.mod.mcbbs; +package org.jackhuang.hmcl.mod.modrinth; import com.google.gson.stream.JsonWriter; import org.jackhuang.hmcl.download.LibraryAnalyzer; @@ -23,13 +23,10 @@ import org.jackhuang.hmcl.mod.ModAdviser; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.ModpackExportInfo; -import org.jackhuang.hmcl.mod.curse.CurseManifest; -import org.jackhuang.hmcl.mod.curse.CurseManifestMinecraft; -import org.jackhuang.hmcl.mod.curse.CurseManifestModLoader; +import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.Zipper; import java.io.File; @@ -38,38 +35,29 @@ import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Optional; +import java.util.*; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; /** - * Export task for MCBBS modpack format. + * Export task for Modrinth modpack format. *

- * Note: This implementation performs two passes over the game directory: - * 1. First pass: walks the file tree to generate the manifest JSON (calculating SHA‑1 hashes). - * 2. Second pass: compresses the files into the final ZIP. + * This implementation streams the index JSON directly to a temporary file using {@link JsonWriter}, + * keeping memory usage low regardless of file count. *

- * This double traversal is a deliberate trade‑off to avoid holding all file information in memory, - * which would cause OutOfMemoryError on very large modpacks. The streaming JSON writer writes the - * manifest to a temporary file, keeping memory usage constant regardless of file count. - *

- * SHA‑1 hashes are computed using {@link DigestUtils#digestToString(String, Path)} which uses - * a streaming {@code DigestInputStream}, making it safe for large files without OOM risk. + * SHA‑1 and SHA‑512 hashes are computed using {@link DigestUtils#digestToString(String, Path)} + * which uses a streaming {@code DigestInputStream}, safe for large files without OOM risk. */ -public class McbbsModpackExportTask extends Task { +public class ModrinthModpackExportTask extends Task { private final DefaultGameRepository repository; private final String version; private final ModpackExportInfo info; private final Path modpackFile; - public McbbsModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo info, Path modpackFile) { + public ModrinthModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo info, Path modpackFile) { this.repository = repository; this.version = version; this.info = info.validate(); @@ -86,6 +74,73 @@ public McbbsModpackExportTask(DefaultGameRepository repository, String version, }); } + private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath, Set temporarilyEnabledFiles) throws IOException { + if (info.isNoCreateRemoteFiles()) { + return null; + } + + boolean isDisabled = repository.getModManager(version).isDisabled(file); + if (isDisabled) { + // Use absolute path to avoid working directory issues + Path enabledPath = repository.getModManager(version).enableMod(file); + temporarilyEnabledFiles.add(enabledPath); + file = enabledPath; + relativePath = repository.getRunDirectory(version).relativize(file) + .normalize().toString().replace(File.separatorChar, '/'); + } + + Optional modrinthVersion = Optional.empty(); + Optional curseForgeVersion = Optional.empty(); + + try { + modrinthVersion = ModrinthRemoteModRepository.MODS.getRemoteVersionByLocalFile(file); + } catch (IOException e) { + LOG.warning("Failed to get remote file from Modrinth for: " + file, e); + } + + if (!info.isSkipCurseForgeRemoteFiles() && CurseForgeRemoteModRepository.isAvailable()) { + try { + curseForgeVersion = CurseForgeRemoteModRepository.MODS.getRemoteVersionByLocalFile(file); + } catch (IOException e) { + LOG.warning("Failed to get remote file from CurseForge for: " + file, e); + } + } + + if (modrinthVersion.isEmpty() && curseForgeVersion.isEmpty()) { + return null; + } + + Map hashes = new HashMap<>(); + hashes.put("sha1", DigestUtils.digestToString("SHA-1", file)); + hashes.put("sha512", DigestUtils.digestToString("SHA-512", file)); + + Map env = null; + if (isDisabled) { + env = new HashMap<>(); + env.put("client", "optional"); + } + + List downloads = new ArrayList<>(); + if (modrinthVersion.isPresent()) + downloads.add(modrinthVersion.get().getFile().getUrl()); + if (curseForgeVersion.isPresent()) + downloads.add(curseForgeVersion.get().getFile().getUrl()); + + long fileSize = Files.size(file); + // Modrinth's manifest fileSize field is int. If a file is >2GiB, we cannot represent it. + // Throw an exception to avoid silent truncation. + if (fileSize > Integer.MAX_VALUE) { + throw new IOException("File " + relativePath + " is too large (" + fileSize + " bytes) to be included in Modrinth manifest (max 2GiB)."); + } + return new ModrinthManifest.File( + relativePath, + hashes, + env, + downloads, + (int) fileSize + ); + } + @Override public void execute() throws Exception { ArrayList blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST); @@ -98,166 +153,158 @@ public void execute() throws Exception { .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); - Path tempManifest = Files.createTempFile("mcbbs_packmeta_", ".json"); - tempManifest.toFile().deleteOnExit(); - try { - try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempManifest, StandardCharsets.UTF_8))) { - writer.setIndent(" "); - writer.beginObject(); + // Defensive handling: if whitelist is null, treat as empty + Set whitelistSet = info.getWhitelist() != null ? new HashSet<>(info.getWhitelist()) : Collections.emptySet(); + final boolean hasWhitelist = info.getWhitelist() != null && !info.getWhitelist().isEmpty(); - writer.name("manifestType").value(McbbsModpackManifest.MANIFEST_TYPE); - writer.name("manifestVersion").value(2); - if (info.getName() != null) writer.name("name").value(info.getName()); - if (info.getVersion() != null) writer.name("version").value(info.getVersion()); - if (info.getAuthor() != null) writer.name("author").value(info.getAuthor()); - if (info.getDescription() != null) writer.name("description").value(info.getDescription()); - if (info.getFileApi() != null) { - writer.name("fileApi").value(StringUtils.removeSuffix(info.getFileApi(), "/")); - } - if (info.getUrl() != null) writer.name("url").value(info.getUrl()); - writer.name("forceUpdate").value(info.isForceUpdate()); - - writer.name("origin").beginArray(); - if (info.getOrigins() != null) { - for (McbbsModpackManifest.Origin origin : info.getOrigins()) { - writer.beginObject(); - writer.name("type").value(origin.getType()); - writer.name("id").value(origin.getId()); - writer.endObject(); - } - } - writer.endArray(); + String[] resourceDirs = {"resourcepacks", "shaderpacks", "mods"}; + Set remoteFilePaths = new HashSet<>(); + Set temporarilyEnabledFiles = new HashSet<>(); - writer.name("addons").beginArray(); + Path tempIndex = Files.createTempFile("modrinth_index_", ".json"); + try { + try (JsonWriter writer = new JsonWriter(Files.newBufferedWriter(tempIndex, StandardCharsets.UTF_8))) { + writer.setIndent(" "); writer.beginObject(); - writer.name("id").value(MINECRAFT.getPatchId()); - writer.name("version").value(gameVersion); - writer.endObject(); - LibraryAnalyzer.LibraryType[] addonTypes = { - FORGE, CLEANROOM, NEO_FORGE, LITELOADER, OPTIFINE, FABRIC, QUILT, LEGACY_FABRIC - }; - for (LibraryAnalyzer.LibraryType type : addonTypes) { - Optional addonVersion = analyzer.getVersion(type); - if (addonVersion.isPresent()) { - writer.beginObject(); - writer.name("id").value(type.getPatchId()); - writer.name("version").value(addonVersion.get()); - writer.endObject(); - } + writer.name("formatVersion").value(1); + writer.name("game").value("minecraft"); + writer.name("versionId").value(info.getVersion()); + writer.name("name").value(info.getName()); + if (info.getDescription() != null) { + writer.name("summary").value(info.getDescription()); } - writer.endArray(); - - writer.name("libraries").beginArray(); - writer.endArray(); writer.name("files").beginArray(); - Files.walkFileTree(runDirectory, new SimpleFileVisitor() { - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - String relativePath = runDirectory.relativize(dir).normalize().toString().replace(File.separatorChar, '/'); - if (relativePath.isEmpty()) { - return FileVisitResult.CONTINUE; - } - // Consistent with zip.putDirectory filter: only skip blacklisted directories - if (ModAdviser.match(blackList, relativePath, false)) { - return FileVisitResult.SKIP_SUBTREE; - } - return FileVisitResult.CONTINUE; - } - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); - if (Modpack.acceptFile(relativePath, blackList, info.getWhitelist())) { - String sha1 = DigestUtils.digestToString("SHA-1", file); - writer.beginObject(); - writer.name("type").value("addon"); - writer.name("force").value(true); - writer.name("path").value(relativePath); - writer.name("hash").value(sha1); - writer.endObject(); - } - return FileVisitResult.CONTINUE; - } - }); - writer.endArray(); + Set processedPaths = new HashSet<>(); - writer.name("settings").beginObject(); - writer.name("install_mods").value(true); - writer.name("install_resourcepack").value(true); - writer.endObject(); + for (String dir : resourceDirs) { + Path dirPath = runDirectory.resolve(dir); + if (Files.exists(dirPath)) { + Files.walkFileTree(dirPath, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); + if (!whitelistSet.contains(relativePath)) { + return FileVisitResult.CONTINUE; + } + if (processedPaths.contains(relativePath)) { + return FileVisitResult.CONTINUE; + } + processedPaths.add(relativePath); - writer.name("launchInfo").beginObject(); - writer.name("minMemory").value(info.getMinMemory()); - writer.name("supportJava").beginArray(); - if (info.getSupportedJavaVersions() != null) { - for (Integer ver : info.getSupportedJavaVersions()) { - if (ver != null) { - writer.value(ver); - } + ModrinthManifest.File fileEntry = null; + try { + fileEntry = tryGetRemoteFile(file, relativePath, temporarilyEnabledFiles); + } catch (IOException e) { + LOG.warning("Failed to process file: " + file, e); + } + if (fileEntry != null) { + remoteFilePaths.add(relativePath); + writer.beginObject(); + writer.name("path").value(fileEntry.getPath()); + writer.name("hashes").beginObject(); + for (Map.Entry hash : fileEntry.getHashes().entrySet()) { + writer.name(hash.getKey()).value(hash.getValue()); + } + writer.endObject(); + if (fileEntry.getEnv() != null) { + writer.name("env").beginObject(); + for (Map.Entry env : fileEntry.getEnv().entrySet()) { + writer.name(env.getKey()).value(env.getValue()); + } + writer.endObject(); + } + writer.name("downloads").beginArray(); + for (String url : fileEntry.getDownloads()) { + writer.value(url); + } + writer.endArray(); + writer.name("fileSize").value(fileEntry.getFileSize()); + writer.endObject(); + } + return FileVisitResult.CONTINUE; + } + }); } } - writer.endArray(); - writer.name("launchArgument").beginArray(); - List launchArgs = StringUtils.tokenize(info.getLaunchArguments()); - if (launchArgs != null) { - for (String arg : launchArgs) { - writer.value(arg); - } - } writer.endArray(); - writer.name("javaArgument").beginArray(); - List javaArgs = StringUtils.tokenize(info.getJavaArguments()); - if (javaArgs != null) { - for (String arg : javaArgs) { - writer.value(arg); - } + writer.name("dependencies").beginObject(); + writer.name("minecraft").value(gameVersion); + Optional forgeVersion = analyzer.getVersion(FORGE); + if (forgeVersion.isPresent()) { + writer.name("forge").value(forgeVersion.get()); + } + Optional neoForgeVersion = analyzer.getVersion(NEO_FORGE); + if (neoForgeVersion.isPresent()) { + writer.name("neoforge").value(neoForgeVersion.get()); + } + Optional fabricVersion = analyzer.getVersion(FABRIC); + if (fabricVersion.isPresent()) { + writer.name("fabric-loader").value(fabricVersion.get()); + } + Optional quiltVersion = analyzer.getVersion(QUILT); + if (quiltVersion.isPresent()) { + writer.name("quilt-loader").value(quiltVersion.get()); } - writer.endArray(); writer.endObject(); writer.endObject(); } - try (var zip = new Zipper(modpackFile)) { - zip.putFile(tempManifest, "mcbbs.packmeta"); + // Restore disabled mods BEFORE zipping so they remain .disabled in final pack + for (Path enabledPath : temporarilyEnabledFiles) { + try { + repository.getModManager(version).disableMod(enabledPath); + LOG.info("Restored disabled mod: " + enabledPath); + } catch (IOException e) { + LOG.warning("Failed to restore disabled mod: " + enabledPath, e); + } + } + temporarilyEnabledFiles.clear(); - List modLoaders = new ArrayList<>(); - analyzer.getVersion(FORGE).ifPresent(forgeVersion -> modLoaders.add(new CurseManifestModLoader("forge-" + forgeVersion, true))); - analyzer.getVersion(NEO_FORGE).ifPresent(neoForgeVersion -> modLoaders.add(new CurseManifestModLoader("neoforge-" + neoForgeVersion, true))); - analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> modLoaders.add(new CurseManifestModLoader("fabric-" + fabricVersion, true))); - CurseManifest curseManifest = new CurseManifest(CurseManifest.MINECRAFT_MODPACK, 1, info.getName(), info.getVersion(), info.getAuthor(), "overrides", new CurseManifestMinecraft(gameVersion, modLoaders), Collections.emptyList()); - zip.putTextFile(JsonUtils.GSON.toJson(curseManifest), "manifest.json"); + try (var zip = new Zipper(modpackFile)) { + zip.putFile(tempIndex, "modrinth.index.json"); - zip.putDirectory(runDirectory, "overrides", path -> { + zip.putDirectory(runDirectory, "client-overrides", path -> { if (path == null || path.isEmpty()) { return true; } - String normalizedPath = Paths.get(path).normalize().toString().replace(File.separatorChar, '/'); - Path resolved = runDirectory.resolve(normalizedPath); + // Normalize path separator to match remoteFilePaths (which uses '/') + String normalizedPath = path.replace(File.separatorChar, '/'); + Path resolved = runDirectory.resolve(path); // resolve uses the path string directly, OK if (Files.isDirectory(resolved)) { + // If whitelist is used, we must allow all directories to pass so that whitelisted files inside can be reached. + if (hasWhitelist) { + return true; + } return !ModAdviser.match(blackList, normalizedPath, false); - } else { - return Modpack.acceptFile(normalizedPath, blackList, info.getWhitelist()); } + if (remoteFilePaths.contains(normalizedPath)) { + return false; + } + return Modpack.acceptFile(normalizedPath, blackList, info.getWhitelist()); }); } } finally { - Files.deleteIfExists(tempManifest); + Files.deleteIfExists(tempIndex); + // Safety net: restore any mods still enabled if exception occurred after enabling but before restoration + for (Path enabledPath : temporarilyEnabledFiles) { + try { + repository.getModManager(version).disableMod(enabledPath); + LOG.info("Restored disabled mod (finally): " + enabledPath); + } catch (IOException e) { + LOG.warning("Failed to restore disabled mod (finally): " + enabledPath, e); + } + } + temporarilyEnabledFiles.clear(); } } public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options() - .requireFileApi(true) - .requireUrl() - .requireForceUpdate() - .requireMinMemory() - .requireAuthlibInjectorServer() - .requireJavaArguments() - .requireLaunchArguments() - .requireOrigins() - .requireAuthor(); + .requireNoCreateRemoteFiles() + .requireSkipCurseForgeRemoteFiles(); }