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;
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.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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 {
@@ -31,11 +32,27 @@ public class LaunchCommandBuilder {
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) {
this.instance = instance;
}
public List<String> build(LaunchOptions options) throws Exception {
public List<String> build(LaunchOptions options) throws Exception {
System.out.println(ZAnsi.cyan("Generating launch command for " + instance.getName() + "..."));
List<String> command = new ArrayList<>();
@@ -57,103 +74,79 @@ public class LaunchCommandBuilder {
VersionManifest manifest = resolveVersionManifest();
// For modloaders, use vanilla classpath but read main class from manifest when available
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)) {
// Build classpath from manifest libraries.
String classpath;
if (manifest != null) {
classpath = buildClasspathFromManifest(manifest, false);
} else {
classpath = "";
}
if (classpath == null || classpath.isEmpty()) {
classpath = buildVanillaClasspath();
}
System.out.println(ZAnsi.cyan(" Forge/NeoForge - deduplicated classpath"));
// The forge client jar (forge-*-client.jar) is NOT listed in the
// version JSON's libraries array, so it's NOT on the classpath.
// FML discovers it by scanning the library directory, but
// securejarhandler only processes classpath entries.
// We add it explicitly so securejarhandler creates the "minecraft"
// module from its Automatic-Module-Name: minecraft.
String forgeVersion = instance.getMinecraftVersion() + "-" + instance.getLoaderVersion();
Path forgeClientJar = instance.getPath().resolve("libraries")
.resolve("net/minecraftforge/forge")
.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()));
ensureVersionJarForForge();
String vanillaCp = buildVanillaClasspath();
Path forgeJar = findPatchedForgeJar();
String classpath;
if (forgeJar != null && Files.exists(forgeJar) && isValidJar(forgeJar)) {
classpath = buildDeduplicatedClasspath(forgeJar.toAbsolutePath().toString(), vanillaCp);
System.out.println(ZAnsi.green(" Added Forge jar: " + forgeJar.getFileName()));
} else {
System.out.println(ZAnsi.yellow(" Forge client jar not found at " + forgeClientJar));
classpath = vanillaCp;
}
command.add("-cp");
String cpFile = writeClasspathFile(classpath);
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) {
Map<String, String> vars = buildVariableMap(options);
vars.put("classpath", cpFile);
for (String arg : manifest.getJvmArguments()) {
String resolved = resolveVariable(arg, vars);
if (resolved.startsWith("-DignoreList=")) {
String value = resolved.substring("-DignoreList=".length());
// Remove forge- from the comma-separated list:
// - ,forge-, in the middle → just the comma
// - ,forge- at the end → remove
// - forge-, at the start → remove
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;
String value = resolved.substring("-DignoreList=".length())
.replaceAll("forge[^,]*", "")
.replaceAll(",+", ",")
.replaceAll("^,|,$", "");
resolved = "-DignoreList=" + value;
}
if (resolved.startsWith("-p ") || resolved.startsWith("--module-path")) {
System.out.println(ZAnsi.yellow(" Skipped module-path"));
continue;
}
command.add(resolved);
}
}
command.add("-DlibraryDirectory=" + instance.getPath().resolve("libraries").toAbsolutePath());
command.add(getVanillaMainClass());
command.addAll(getVanillaGameArguments(options));
command.addAll(getModloaderLaunchArgs());
command.addAll(getVanillaGameArguments(options));
} else {
// Fabric и остальные модлоадеры
System.out.println(ZAnsi.cyan(" Modloader detected (" + loaderType + "), using vanilla classpath"));
command.add("-cp");
command.add(writeClasspathFile(buildVanillaClasspath()));
String mainClass = null;
if (manifest != null) {
mainClass = manifest.getMainClass();
System.out.println(ZAnsi.cyan(" Main class from manifest: " + mainClass));
}
if (mainClass == null || mainClass.isEmpty()) {
mainClass = getVanillaMainClass();
System.out.println(ZAnsi.yellow(" Using fallback main class: " + mainClass));
}
command.add(mainClass);
command.addAll(getVanillaGameArguments(options));
}
} else if (manifest != null) {
}
else if (manifest != null) {
// Vanilla
String classpath = buildClasspathFromManifest(manifest, true);
// Fallback if classpath is empty
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions").resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) {
System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath"));
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions")
.resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) {
command.add("-cp");
command.add(writeClasspathFile(buildVanillaClasspath()));
command.add(getVanillaMainClass());
@@ -161,13 +154,12 @@ public class LaunchCommandBuilder {
} else {
command.add("-cp");
command.add(writeClasspathFile(classpath));
String mainClass = resolveMainClass(manifest);
command.add(mainClass);
command.add(resolveMainClass(manifest));
command.addAll(resolveGameArguments(manifest, options));
}
} else {
}
else {
// Полный fallback
command.add("-cp");
command.add(writeClasspathFile(buildVanillaClasspath()));
command.add(getVanillaMainClass());
@@ -519,24 +511,27 @@ public class LaunchCommandBuilder {
List<String> paths = new ArrayList<>();
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();
boolean isForgeLike = "forge".equals(loaderType) || "neoforge".equals(loaderType);
for (VersionManifest.Library lib : manifest.getAllLibraries()) {
// For Forge/NeoForge: include ALL libraries including net.minecraft:*
// in the classpath string. Forge version.json uses "-p ${classpath}" to
// set BOTH classpath and module path to the same string. The Minecraft
// client JAR must be on the module path so securejarhandler can load it
// as a proper module. Skipping it causes ClassNotFoundException.
String libName = lib.name != null ? lib.name.toLowerCase() : "";
// ИСКЛЮЧАЕМ forge/neoforge библиотеки, чтобы не было дубликатов
if (libName.contains("net.minecraftforge:forge") || libName.contains("net.neoforged:neoforge")) {
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;
if (libPath != null && Files.exists(libPath)) {
if (isValidJar(libPath)) {
paths.add(libPath.toAbsolutePath().toString());
} else {
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
Files.delete(libPath);
Files.deleteIfExists(libPath);
}
} else {
String mavenPath = mavenToPath(lib.name);
@@ -546,43 +541,30 @@ public class LaunchCommandBuilder {
paths.add(fallbackPath.toAbsolutePath().toString());
} else {
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
Files.delete(fallbackPath);
Files.deleteIfExists(fallbackPath);
}
} else {
// Last resort: scan libraries dir for a JAR matching this artifact
String artifactName = lib.name.split(":")[1];
Path found = scanForJar(librariesDir, artifactName);
if (found != null) {
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());
} else {
System.out.println(ZAnsi.yellow(" Corrupt library (scan), deleting: " + found.getFileName()));
Files.delete(found);
Files.deleteIfExists(found);
}
} 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) {
Path versionJar = findVersionJar();
if (versionJar != null) {
if (isValidJar(versionJar)) {
paths.add(0, versionJar.toAbsolutePath().toString());
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);
}
if (versionJar != null && isValidJar(versionJar)) {
paths.add(0, versionJar.toAbsolutePath().toString());
System.out.println(ZAnsi.green(" Added version jar: " + versionJar.getFileName()));
}
}
@@ -727,19 +709,27 @@ public class LaunchCommandBuilder {
Path librariesDir = instance.getPath().resolve("libraries");
String mcVersion = instance.getMinecraftVersion();
String loaderVer = instance.getLoaderVersion();
String fullVersion = mcVersion + "-" + loaderVer;
String[] candidates = {
"net/minecraftforge/forge/" + mcVersion + "-" + loaderVer + "/forge-" + mcVersion + "-" + loaderVer + "-client.jar",
"net/neoforged/neoforge/" + mcVersion + "-" + loaderVer + "/neoforge-" + mcVersion + "-" + loaderVer + "-client.jar"
// 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) {
Path jarPath = librariesDir.resolve(candidate);
if (Files.exists(jarPath) && isValidJar(jarPath)) {
System.out.println(ZAnsi.green(" Found Forge jar: " + jarPath.getFileName()));
return jarPath;
}
}
System.out.println(ZAnsi.yellow(" No Forge client/universal jar found!"));
return null;
}