添加 Modrinth 整合包导出 (#3716)

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Zkitefly
2025-08-06 20:11:39 +08:00
committed by GitHub
parent 95bed4712c
commit 1a772e8c65
12 changed files with 339 additions and 13 deletions

View File

@@ -42,17 +42,29 @@ public interface ModAdviser {
}
List<String> MODPACK_BLACK_LIST = Lang.immutableListOf(
"regex:(.*?)\\.log",
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Old Minecraft Launcher
"launcher_accounts.json", "launcher_cef_log.txt", "launcher_log.txt", "launcher_msa_credentials.bin", "launcher_settings.json", "launcher_ui_state.json", "realms_persistence.json", "webcache2", "treatment_tags.json", // New Minecraft Launcher
"clientId.txt", "PCL.ini", // Plain Craft Launcher
"backup", "pack.json", "launcher.jar", "cache", "modpack.cfg", // HMCL
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse
".fabric", ".mixin.out", // Fabric
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
"regex:(.*?)\\.log",
"regex:.*\\.dat_old$", "regex:.*\\.old$", // Backup files
"regex:.*\\.BakaCoreInfo$", // BakaXL
"regex:.*-natives",
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Old Minecraft Launcher
"launcher_accounts.json", "launcher_cef_log.txt", "launcher_log.txt", "launcher_msa_credentials.bin", "launcher_settings.json", "launcher_ui_state.json", "realms_persistence.json", "webcache2", "treatment_tags.json", // New Minecraft Launcher
"clientId.txt", "PCL.ini", // Plain Craft Launcher
"backup", "pack.json", "launcher.jar", "cache", "modpack.cfg", "log4j2.xml", "hmclversion.cfg", // HMCL
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse
"modrinth.index.json", // Modrinth
".fabric", ".mixin.out", ".optifine", // Fabric/OptiFine
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "$natives", "server-resource-packs", "command_history.txt", // Minecraft
"downloads", "essential", // Downloads and Essential
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches", // Mods
"debug", // Debug files
".replay_cache", "replay_recordings", "replay_videos", // ReplayMod
"irisUpdateInfo.json", // Iris
"modernfix", // ModernFix
"modtranslations", // Mod translations
"schematics", // Schematics mod
"journeymap/data", // JourneyMap
"mods/.connector" // Sinytra Connector
);
List<String> MODPACK_SUGGESTED_BLACK_LIST = Lang.immutableListOf(

View File

@@ -29,6 +29,9 @@ public class ModpackExportInfo {
private List<McbbsModpackManifest.Origin> origins = new ArrayList<>();
private boolean noCreateRemoteFiles;
private boolean skipCurseForgeRemoteFiles;
public ModpackExportInfo() {}
public List<String> getWhitelist() {
@@ -186,6 +189,22 @@ public class ModpackExportInfo {
return this;
}
public boolean isNoCreateRemoteFiles() {
return noCreateRemoteFiles;
}
public void setNoCreateRemoteFiles(boolean noCreateRemoteFiles) {
this.noCreateRemoteFiles = noCreateRemoteFiles;
}
public boolean isSkipCurseForgeRemoteFiles() {
return skipCurseForgeRemoteFiles;
}
public void setSkipCurseForgeRemoteFiles(boolean skipCurseForgeRemoteFiles) {
this.skipCurseForgeRemoteFiles = skipCurseForgeRemoteFiles;
}
public ModpackExportInfo validate() throws NullPointerException {
return this;
}
@@ -200,6 +219,8 @@ public class ModpackExportInfo {
private boolean requireLaunchArguments;
private boolean requireJavaArguments;
private boolean requireOrigins;
private boolean requireNoCreateRemoteFiles;
private boolean requireSkipCurseForgeRemoteFiles;
public Options() {
}
@@ -240,6 +261,14 @@ public class ModpackExportInfo {
return requireOrigins;
}
public boolean isRequireNoCreateRemoteFiles() {
return requireNoCreateRemoteFiles;
}
public boolean isRequireSkipCurseForgeRemoteFiles() {
return requireSkipCurseForgeRemoteFiles;
}
public Options requireUrl() {
requireUrl = true;
return this;
@@ -281,5 +310,14 @@ public class ModpackExportInfo {
return this;
}
public Options requireNoCreateRemoteFiles() {
requireNoCreateRemoteFiles = true;
return this;
}
public Options requireSkipCurseForgeRemoteFiles() {
requireSkipCurseForgeRemoteFiles = true;
return this;
}
}
}

View File

@@ -0,0 +1,182 @@
package org.jackhuang.hmcl.mod.modrinth;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
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.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 static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class ModrinthModpackExportTask extends Task<Void> {
private final DefaultGameRepository repository;
private final String version;
private final ModpackExportInfo info;
private final File modpackFile;
public ModrinthModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo info, File modpackFile) {
this.repository = repository;
this.version = version;
this.info = info.validate();
this.modpackFile = modpackFile;
onDone().register(event -> {
if (event.isFailed()) modpackFile.delete();
});
}
private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath) throws IOException {
if (info.isNoCreateRemoteFiles()) {
return null;
}
boolean isDisabled = repository.getModManager(version).isDisabled(file);
if (isDisabled) {
relativePath = repository.getModManager(version).enableMod(Paths.get(relativePath)).toString();
}
LocalModFile localModFile = null;
Optional<RemoteMod.Version> modrinthVersion = Optional.empty();
Optional<RemoteMod.Version> curseForgeVersion = Optional.empty();
try {
modrinthVersion = ModrinthRemoteModRepository.MODS.getRemoteVersionByLocalFile(localModFile, 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(localModFile, file);
} catch (IOException e) {
LOG.warning("Failed to get remote file from CurseForge for: " + file, e);
}
}
if (!modrinthVersion.isPresent() && !curseForgeVersion.isPresent()) {
return null;
}
Map<String, String> hashes = new HashMap<>();
hashes.put("sha1", DigestUtils.digestToString("SHA-1", file));
hashes.put("sha512", DigestUtils.digestToString("SHA-512", file));
Map<String, String> env = null;
if (isDisabled) {
env = new HashMap<>();
env.put("client", "optional");
}
List<URL> downloads = new ArrayList<>();
if (modrinthVersion.isPresent())
downloads.add(new URL(modrinthVersion.get().getFile().getUrl()));
if (curseForgeVersion.isPresent())
downloads.add(new URL(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<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
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 (Zipper zip = new Zipper(modpackFile.toPath())) {
Path runDirectory = repository.getRunDirectory(version).toPath();
List<ModrinthManifest.File> files = new ArrayList<>();
Set<String> 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);
}
} catch (IOException e) {
LOG.warning("Failed to process file: " + file, e);
}
});
}
}
zip.putDirectory(runDirectory, "client-overrides", path -> {
String relativePath = path.toString().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<String, String> 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");
}
}
public static final ModpackExportInfo.Options OPTION = new ModpackExportInfo.Options()
.requireNoCreateRemoteFiles()
.requireSkipCurseForgeRemoteFiles();
}