fix: split classpath from module path to prevent Forge split-package error

- Build two separate classpaths: modulePath (libraries only) and classpath (libraries + version jar)
- Intercept manifest -p to use modulePath (no version jar) preventing _1._20._1 vs minecraft module conflict
- Intercept manifest -cp to skip (our explicit -cp already added)
- Fix --version to use getVersionId() instead of instance.getName()
This commit is contained in:
SashegDev
2026-07-13 14:24:25 +00:00
parent 546652f44c
commit 1efd283234
@@ -63,44 +63,50 @@ public class LaunchCommandBuilder {
// always use ModLauncher as main class (version.json might have // always use ModLauncher as main class (version.json might have
// BootstrapLauncher which breaks with Java module system) // BootstrapLauncher which breaks with Java module system)
if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) { if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) {
// BootstrapLauncher creates the module layer ModLauncher needs // Build TWO classpaths for Forge/NeoForge:
// for ServiceLoader discovery. // 1. modulePath: libraries only (for -p module path) — version jar excluded
// Build classpath from manifest (shorter, ordered). // to prevent split-package conflict with Forge's "minecraft" module
// Only JARs explicitly listed in version.json go on the classpath. // 2. classpath: libraries + version jar (for -cp classpath) — version jar
// Do NOT append JARs from recursive scan — tool JARs like // included so PreparableReloadListener and other MC classes are loadable
// ForgeAutoRenamingTool that contain shaded ASM packages cause String modulePath;
// split-package conflicts with the Java module system when
// --add-modules=ALL-MODULE-PATH resolves them as automatic modules.
String classpath; String classpath;
if (manifest != null) { if (manifest != null) {
classpath = buildClasspathFromManifest(manifest); modulePath = buildClasspathFromManifest(manifest, false);
classpath = buildClasspathFromManifest(manifest, true);
} else { } else {
modulePath = "";
classpath = ""; classpath = "";
} }
if (classpath == null || classpath.isEmpty()) { if (modulePath == null || modulePath.isEmpty()) {
classpath = buildVanillaClasspath(); modulePath = buildVanillaClasspath();
} else {
// Safety net: remove JARs from classpath that are already
// on the module path (from manifest JVM args) to prevent
// Java module system split-package conflicts.
classpath = filterClasspathAgainstModulePath(classpath, manifest.getJvmArguments());
} }
if (classpath == null || classpath.isEmpty()) {
classpath = modulePath;
}
command.add("-cp"); command.add("-cp");
String cpFile = writeClasspathFile(classpath); String cpFile = writeClasspathFile(classpath);
String mpFile = writeClasspathFile(modulePath);
command.add(cpFile); command.add(cpFile);
// Add JVM arguments from version.json manifest // Add JVM arguments from version.json manifest.
// Includes -DignoreList, -p (module path), --add-modules, --add-opens, // Intercept -p / --module-path to use mpFile (no version jar)
// and -DlibraryDirectory — all required for BootstrapLauncher to // and -cp / --classpath to skip (we already added our own -cp).
// resolve cpw.mods.securejarhandler as a module.
// Forge manifest JVM args also contain "-cp ${classpath}" — we resolve
// it here so the manifest's -cp takes precedence (avoids duplicate -cp
// issues and ensures the manifest controls the final classpath).
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()) { List<String> jvmArgs = manifest.getJvmArguments();
command.add(resolveVariable(arg, vars)); for (int i = 0; i < jvmArgs.size(); i++) {
String raw = jvmArgs.get(i);
if (("-p".equals(raw) || "--module-path".equals(raw)) && i + 1 < jvmArgs.size()) {
command.add(raw);
i++;
command.add(mpFile);
} else if (("-cp".equals(raw) || "--classpath".equals(raw)) && i + 1 < jvmArgs.size()) {
i++;
} else {
command.add(resolveVariable(raw, vars));
}
} }
} }
@@ -125,7 +131,7 @@ public class LaunchCommandBuilder {
command.addAll(getVanillaGameArguments(options)); command.addAll(getVanillaGameArguments(options));
} }
} else if (manifest != null) { } else if (manifest != null) {
String classpath = buildClasspathFromManifest(manifest); String classpath = buildClasspathFromManifest(manifest, true);
// Fallback if classpath is empty // Fallback if classpath is empty
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions").resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) { if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions").resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) {
@@ -417,7 +423,7 @@ public class LaunchCommandBuilder {
List<String> args = new ArrayList<>(); List<String> args = new ArrayList<>();
args.add("--version"); args.add("--version");
args.add(instance.getName()); args.add(getVersionId());
args.add("--gameDir"); args.add("--gameDir");
args.add(instance.getPath().toAbsolutePath().toString()); args.add(instance.getPath().toAbsolutePath().toString());
args.add("--assetsDir"); args.add("--assetsDir");
@@ -491,7 +497,7 @@ public class LaunchCommandBuilder {
return result; return result;
} }
private String buildClasspathFromManifest(VersionManifest manifest) throws Exception { private String buildClasspathFromManifest(VersionManifest manifest, boolean includeVersionJar) throws Exception {
List<String> paths = new ArrayList<>(); List<String> paths = new ArrayList<>();
Path librariesDir = instance.getPath().resolve("libraries"); Path librariesDir = instance.getPath().resolve("libraries");
@@ -549,14 +555,16 @@ public class LaunchCommandBuilder {
System.out.println(ZAnsi.cyan(" Paths dir: " + instance.getPath().resolve("versions"))); System.out.println(ZAnsi.cyan(" Paths dir: " + instance.getPath().resolve("versions")));
} }
Path versionJar = findVersionJar(); if (includeVersionJar) {
if (versionJar != null) { Path versionJar = findVersionJar();
if (isValidJar(versionJar)) { if (versionJar != null) {
paths.add(0, versionJar.toAbsolutePath().toString()); if (isValidJar(versionJar)) {
System.out.println(ZAnsi.green(" Added version jar: " + versionJar.getFileName())); paths.add(0, versionJar.toAbsolutePath().toString());
} else { System.out.println(ZAnsi.green(" Added version jar: " + versionJar.getFileName()));
System.out.println(ZAnsi.yellow(" Corrupt version jar, deleting: " + versionJar.getFileName())); } else {
Files.delete(versionJar); System.out.println(ZAnsi.yellow(" Corrupt version jar, deleting: " + versionJar.getFileName()));
Files.delete(versionJar);
}
} }
} }