refactor: deduplicate classpath, improve forge jar detection, clean up comments

This commit is contained in:
SashegDev
2026-07-13 20:29:53 +00:00
parent f58db7f941
commit b9bd642605
@@ -1,21 +1,22 @@
package me.sashegdev.zernmc.launcher.minecraft.launch; package me.sashegdev.zernmc.launcher.minecraft.launch;
import me.sashegdev.zernmc.launcher.minecraft.Instance;
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import org.json.JSONObject;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import org.json.JSONObject;
import me.sashegdev.zernmc.launcher.minecraft.Instance;
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
public class LaunchCommandBuilder { public class LaunchCommandBuilder {
@@ -31,6 +32,22 @@ public class LaunchCommandBuilder {
private final Instance instance; private final Instance instance;
private String buildDeduplicatedClasspath(String... classpaths) {
String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
Set<String> unique = new HashSet<>();
for (String cp : classpaths) {
if (cp == null || cp.isEmpty()) continue;
for (String entry : cp.split(sep)) {
if (!entry.isEmpty()) {
unique.add(entry);
}
}
}
return String.join(sep, unique);
}
public LaunchCommandBuilder(Instance instance) { public LaunchCommandBuilder(Instance instance) {
this.instance = instance; this.instance = instance;
} }
@@ -57,103 +74,79 @@ public class LaunchCommandBuilder {
VersionManifest manifest = resolveVersionManifest(); VersionManifest manifest = resolveVersionManifest();
// For modloaders, use vanilla classpath but read main class from manifest when available
if (isModloader) { if (isModloader) {
// For Forge/NeoForge: use recursive scan of libraries/ for classpath,
// always use ModLauncher as main class (version.json might have
// BootstrapLauncher which breaks with Java module system)
if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) { if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) {
// Build classpath from manifest libraries. System.out.println(ZAnsi.cyan(" Forge/NeoForge - deduplicated classpath"));
String classpath;
if (manifest != null) {
classpath = buildClasspathFromManifest(manifest, false);
} else {
classpath = "";
}
if (classpath == null || classpath.isEmpty()) {
classpath = buildVanillaClasspath();
}
// The forge client jar (forge-*-client.jar) is NOT listed in the ensureVersionJarForForge();
// version JSON's libraries array, so it's NOT on the classpath.
// FML discovers it by scanning the library directory, but String vanillaCp = buildVanillaClasspath();
// securejarhandler only processes classpath entries. Path forgeJar = findPatchedForgeJar();
// We add it explicitly so securejarhandler creates the "minecraft"
// module from its Automatic-Module-Name: minecraft. String classpath;
String forgeVersion = instance.getMinecraftVersion() + "-" + instance.getLoaderVersion(); if (forgeJar != null && Files.exists(forgeJar) && isValidJar(forgeJar)) {
Path forgeClientJar = instance.getPath().resolve("libraries") classpath = buildDeduplicatedClasspath(forgeJar.toAbsolutePath().toString(), vanillaCp);
.resolve("net/minecraftforge/forge") System.out.println(ZAnsi.green(" Added Forge jar: " + forgeJar.getFileName()));
.resolve(forgeVersion)
.resolve("forge-" + forgeVersion + "-client.jar");
if (Files.exists(forgeClientJar) && isValidJar(forgeClientJar)) {
String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
classpath = forgeClientJar.toAbsolutePath().toString() + sep + classpath;
System.out.println(ZAnsi.green(" Added forge client jar: " + forgeClientJar.getFileName()));
} else { } else {
System.out.println(ZAnsi.yellow(" Forge client jar not found at " + forgeClientJar)); classpath = vanillaCp;
} }
command.add("-cp"); command.add("-cp");
String cpFile = writeClasspathFile(classpath); String cpFile = writeClasspathFile(classpath);
command.add(cpFile); command.add(cpFile);
// Add JVM arguments from version.json manifest.
// Forge's -p is a HARDCODED list of 8 bootstrap JARs
// (bootstraplauncher, securejarhandler, asm-*, JarJarFileSystems).
// Intercept -DignoreList to remove "forge-" pattern, which
// blocks the forge client jar (forge-*-client.jar) from being
// processed by securejarhandler into the "minecraft" module.
if (manifest != null) { if (manifest != null) {
Map<String, String> vars = buildVariableMap(options); Map<String, String> vars = buildVariableMap(options);
vars.put("classpath", cpFile); vars.put("classpath", cpFile);
for (String arg : manifest.getJvmArguments()) { for (String arg : manifest.getJvmArguments()) {
String resolved = resolveVariable(arg, vars); String resolved = resolveVariable(arg, vars);
if (resolved.startsWith("-DignoreList=")) { if (resolved.startsWith("-DignoreList=")) {
String value = resolved.substring("-DignoreList=".length()); String value = resolved.substring("-DignoreList=".length())
// Remove forge- from the comma-separated list: .replaceAll("forge[^,]*", "")
// - ,forge-, in the middle → just the comma .replaceAll(",+", ",")
// - ,forge- at the end → remove .replaceAll("^,|,$", "");
// - forge-, at the start → remove resolved = "-DignoreList=" + value;
String modified = value
.replace(",forge-,", ",")
.replace(",forge-", "")
.replace("forge-,", "");
if (modified.equals("forge-")) modified = "";
if (!modified.equals(value)) {
System.out.println(ZAnsi.green(" Removed 'forge-' from ignoreList"));
} }
resolved = "-DignoreList=" + modified;
if (resolved.startsWith("-p ") || resolved.startsWith("--module-path")) {
System.out.println(ZAnsi.yellow(" Skipped module-path"));
continue;
} }
command.add(resolved); command.add(resolved);
} }
} }
command.add("-DlibraryDirectory=" + instance.getPath().resolve("libraries").toAbsolutePath()); command.add("-DlibraryDirectory=" + instance.getPath().resolve("libraries").toAbsolutePath());
command.add(getVanillaMainClass()); command.add(getVanillaMainClass());
command.addAll(getVanillaGameArguments(options));
command.addAll(getModloaderLaunchArgs()); command.addAll(getModloaderLaunchArgs());
command.addAll(getVanillaGameArguments(options));
} else { } else {
// Fabric и остальные модлоадеры
System.out.println(ZAnsi.cyan(" Modloader detected (" + loaderType + "), using vanilla classpath")); System.out.println(ZAnsi.cyan(" Modloader detected (" + loaderType + "), using vanilla classpath"));
command.add("-cp"); command.add("-cp");
command.add(writeClasspathFile(buildVanillaClasspath())); command.add(writeClasspathFile(buildVanillaClasspath()));
String mainClass = null; String mainClass = null;
if (manifest != null) { if (manifest != null) {
mainClass = manifest.getMainClass(); mainClass = manifest.getMainClass();
System.out.println(ZAnsi.cyan(" Main class from manifest: " + mainClass));
} }
if (mainClass == null || mainClass.isEmpty()) { if (mainClass == null || mainClass.isEmpty()) {
mainClass = getVanillaMainClass(); mainClass = getVanillaMainClass();
System.out.println(ZAnsi.yellow(" Using fallback main class: " + mainClass));
} }
command.add(mainClass); command.add(mainClass);
command.addAll(getVanillaGameArguments(options)); command.addAll(getVanillaGameArguments(options));
} }
} else if (manifest != null) { }
else if (manifest != null) {
// Vanilla
String classpath = buildClasspathFromManifest(manifest, true); String classpath = buildClasspathFromManifest(manifest, true);
// Fallback if classpath is empty if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions")
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions").resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) { .resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) {
System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath"));
command.add("-cp"); command.add("-cp");
command.add(writeClasspathFile(buildVanillaClasspath())); command.add(writeClasspathFile(buildVanillaClasspath()));
command.add(getVanillaMainClass()); command.add(getVanillaMainClass());
@@ -161,13 +154,12 @@ public class LaunchCommandBuilder {
} else { } else {
command.add("-cp"); command.add("-cp");
command.add(writeClasspathFile(classpath)); command.add(writeClasspathFile(classpath));
command.add(resolveMainClass(manifest));
String mainClass = resolveMainClass(manifest);
command.add(mainClass);
command.addAll(resolveGameArguments(manifest, options)); command.addAll(resolveGameArguments(manifest, options));
} }
} else { }
else {
// Полный fallback
command.add("-cp"); command.add("-cp");
command.add(writeClasspathFile(buildVanillaClasspath())); command.add(writeClasspathFile(buildVanillaClasspath()));
command.add(getVanillaMainClass()); command.add(getVanillaMainClass());
@@ -519,24 +511,27 @@ public class LaunchCommandBuilder {
List<String> paths = new ArrayList<>(); List<String> paths = new ArrayList<>();
Path librariesDir = instance.getPath().resolve("libraries"); Path librariesDir = instance.getPath().resolve("libraries");
System.out.println(ZAnsi.cyan(" buildClasspathFromManifest: " + manifest.getAllLibraries().size() + " libraries in manifest (including inherited)")); System.out.println(ZAnsi.cyan(" buildClasspathFromManifest: " + manifest.getAllLibraries().size() + " libraries in manifest"));
String loaderType = instance.getLoaderType().toLowerCase(); String loaderType = instance.getLoaderType().toLowerCase();
boolean isForgeLike = "forge".equals(loaderType) || "neoforge".equals(loaderType); boolean isForgeLike = "forge".equals(loaderType) || "neoforge".equals(loaderType);
for (VersionManifest.Library lib : manifest.getAllLibraries()) { for (VersionManifest.Library lib : manifest.getAllLibraries()) {
// For Forge/NeoForge: include ALL libraries including net.minecraft:* String libName = lib.name != null ? lib.name.toLowerCase() : "";
// in the classpath string. Forge version.json uses "-p ${classpath}" to
// set BOTH classpath and module path to the same string. The Minecraft // ИСКЛЮЧАЕМ forge/neoforge библиотеки, чтобы не было дубликатов
// client JAR must be on the module path so securejarhandler can load it if (libName.contains("net.minecraftforge:forge") || libName.contains("net.neoforged:neoforge")) {
// as a proper module. Skipping it causes ClassNotFoundException. System.out.println(ZAnsi.yellow(" Skipped forge/neoforge library from manifest to prevent duplicate"));
continue;
}
Path libPath = lib.artifactPath != null ? librariesDir.resolve(lib.artifactPath) : null; Path libPath = lib.artifactPath != null ? librariesDir.resolve(lib.artifactPath) : null;
if (libPath != null && Files.exists(libPath)) { if (libPath != null && Files.exists(libPath)) {
if (isValidJar(libPath)) { if (isValidJar(libPath)) {
paths.add(libPath.toAbsolutePath().toString()); paths.add(libPath.toAbsolutePath().toString());
} else { } else {
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name)); System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
Files.delete(libPath); Files.deleteIfExists(libPath);
} }
} else { } else {
String mavenPath = mavenToPath(lib.name); String mavenPath = mavenToPath(lib.name);
@@ -546,43 +541,30 @@ public class LaunchCommandBuilder {
paths.add(fallbackPath.toAbsolutePath().toString()); paths.add(fallbackPath.toAbsolutePath().toString());
} else { } else {
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name)); System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
Files.delete(fallbackPath); Files.deleteIfExists(fallbackPath);
} }
} else { } else {
// Last resort: scan libraries dir for a JAR matching this artifact
String artifactName = lib.name.split(":")[1]; String artifactName = lib.name.split(":")[1];
Path found = scanForJar(librariesDir, artifactName); Path found = scanForJar(librariesDir, artifactName);
if (found != null) { if (found != null) {
if (isValidJar(found)) { if (isValidJar(found)) {
System.out.println(ZAnsi.green(" Found by scan: " + lib.name + "" + librariesDir.relativize(found))); System.out.println(ZAnsi.green(" Found by scan: " + lib.name));
paths.add(found.toAbsolutePath().toString()); paths.add(found.toAbsolutePath().toString());
} else { } else {
System.out.println(ZAnsi.yellow(" Corrupt library (scan), deleting: " + found.getFileName())); Files.deleteIfExists(found);
Files.delete(found);
} }
} else { } else {
System.out.println(ZAnsi.yellow(" Library not found (even after scan): " + lib.name)); System.out.println(ZAnsi.yellow(" Library not found: " + lib.name));
} }
} }
} }
} }
System.out.println(ZAnsi.cyan(" buildClasspathFromManifest: " + paths.size() + " libraries in classpath"));
if (isForgeLike) {
System.out.println(ZAnsi.cyan(" Version ID: " + getVersionId() + " (MC: " + instance.getMinecraftVersion() + ")"));
System.out.println(ZAnsi.cyan(" Paths dir: " + instance.getPath().resolve("versions")));
}
if (includeVersionJar) { if (includeVersionJar) {
Path versionJar = findVersionJar(); Path versionJar = findVersionJar();
if (versionJar != null) { if (versionJar != null && isValidJar(versionJar)) {
if (isValidJar(versionJar)) {
paths.add(0, versionJar.toAbsolutePath().toString()); paths.add(0, versionJar.toAbsolutePath().toString());
System.out.println(ZAnsi.green(" Added version jar: " + versionJar.getFileName())); System.out.println(ZAnsi.green(" Added version jar: " + versionJar.getFileName()));
} else {
System.out.println(ZAnsi.yellow(" Corrupt version jar, deleting: " + versionJar.getFileName()));
Files.delete(versionJar);
}
} }
} }
@@ -727,19 +709,27 @@ public class LaunchCommandBuilder {
Path librariesDir = instance.getPath().resolve("libraries"); Path librariesDir = instance.getPath().resolve("libraries");
String mcVersion = instance.getMinecraftVersion(); String mcVersion = instance.getMinecraftVersion();
String loaderVer = instance.getLoaderVersion(); String loaderVer = instance.getLoaderVersion();
String fullVersion = mcVersion + "-" + loaderVer;
String[] candidates = { String[] candidates = {
"net/minecraftforge/forge/" + mcVersion + "-" + loaderVer + "/forge-" + mcVersion + "-" + loaderVer + "-client.jar", // Client jar (то, что ты использовал раньше)
"net/neoforged/neoforge/" + mcVersion + "-" + loaderVer + "/neoforge-" + mcVersion + "-" + loaderVer + "-client.jar" "net/minecraftforge/forge/" + fullVersion + "/forge-" + fullVersion + "-client.jar",
// Universal jar — самый важный для Forge 1.20.1+
"net/minecraftforge/forge/" + fullVersion + "/forge-" + fullVersion + "-universal.jar",
// NeoForge варианты
"net/neoforged/neoforge/" + fullVersion + "/neoforge-" + fullVersion + "-client.jar",
"net/neoforged/neoforge/" + fullVersion + "/neoforge-" + fullVersion + "-universal.jar"
}; };
for (String candidate : candidates) { for (String candidate : candidates) {
Path jarPath = librariesDir.resolve(candidate); Path jarPath = librariesDir.resolve(candidate);
if (Files.exists(jarPath) && isValidJar(jarPath)) { if (Files.exists(jarPath) && isValidJar(jarPath)) {
System.out.println(ZAnsi.green(" Found Forge jar: " + jarPath.getFileName()));
return jarPath; return jarPath;
} }
} }
System.out.println(ZAnsi.yellow(" No Forge client/universal jar found!"));
return null; return null;
} }