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..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 @@ -17,9 +17,9 @@ */ 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; import org.jackhuang.hmcl.mod.ModAdviser; import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.ModpackExportInfo; @@ -34,11 +34,17 @@ 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; 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; +import java.util.Optional; import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -72,66 +78,153 @@ 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)) { + + 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()); + 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(); + 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.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; + } + 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, '/'); - files.add(new McbbsModpackManifest.AddonFile(true, relativePath, DigestUtils.digestToString("SHA-1", file))); + 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(); + 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 (int ver : info.getSupportedJavaVersions()) { + writer.value(ver); } - return true; - } else { - return false; } - }); - - 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("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.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 -> { + Path resolved = runDirectory.resolve(path); + if (Files.isDirectory(resolved)) { + return !ModAdviser.match(blackList, path, false); + } else { + return Modpack.acceptFile(path, blackList, info.getWhitelist()); + } + }); + } + } finally { + Files.deleteIfExists(tempManifest); } } @@ -145,5 +238,4 @@ public void execute() throws Exception { .requireLaunchArguments() .requireOrigins() .requireAuthor(); - } 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..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 @@ -17,25 +17,29 @@ */ 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.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.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -71,9 +75,9 @@ 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); } - LocalModFile localModFile = null; Optional modrinthVersion = Optional.empty(); Optional curseForgeVersion = Optional.empty(); @@ -130,72 +134,128 @@ 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; - } + 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); - ModrinthManifest.File fileEntry = tryGetRemoteFile(file, relativePath); - if (fileEntry != null) { - files.add(fileEntry); - filesInManifest.add(relativePath); - } + Set whitelistSet = new HashSet<>(info.getWhitelist()); + + 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()); + if (info.getDescription() != null) { + 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.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); } 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; + } + }); + } } - } - zip.putDirectory(runDirectory, "client-overrides", path -> { - String relativePath = path.replace(File.separatorChar, '/'); - if (filesInManifest.contains(relativePath)) { - return false; + 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()); + } + 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()); } - 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"); + writer.endObject(); + + writer.endObject(); + } + + try (var zip = new Zipper(modpackFile)) { + zip.putFile(tempIndex, "modrinth.index.json"); + + zip.putDirectory(runDirectory, "client-overrides", path -> { + String relativePath = path.replace(File.separatorChar, '/'); + Path resolved = runDirectory.resolve(relativePath); + if (Files.isDirectory(resolved)) { + return !ModAdviser.match(blackList, relativePath, false); + } + if (remoteFilePaths.contains(relativePath)) { + return false; + } + return Modpack.acceptFile(relativePath, blackList, info.getWhitelist()); + }); + } + } finally { + Files.deleteIfExists(tempIndex); } }