fix: copy vanilla jar to versions/<versionId>/ for securejarhandler minecraft module

securejarhandler creates the minecraft module from versions/<versionId>/<versionId>.jar.
The Forge installer only creates the JSON there, not the jar. Without the jar,
securejarhandler cannot create the minecraft module, causing ClassNotFoundException
for PreparableReloadListener (a vanilla MC class expected in the minecraft module).

Fix: copy the vanilla version jar (e.g. versions/1.20.1/1.20.1.jar) to the
Forge version directory (e.g. versions/1.20.1-forge-47.4.20/1.20.1-forge-47.4.20.jar)
before launch. The jar is NOT put on -cp to prevent automatic module _1._20._1
from being created, which would conflict with the minecraft module (split-package).

Also removed patched forge client jar from -cp since securejarhandler excludes it
via -DignoreList (forge-* pattern).
This commit is contained in:
SashegDev
2026-07-13 15:53:04 +00:00
parent 96e68dab89
commit d4b6ea081e
@@ -63,13 +63,19 @@ public class LaunchCommandBuilder {
// always use ModLauncher as main class (version.json might have
// BootstrapLauncher which breaks with Java module system)
if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) {
// Forge: vanilla version jar on -cp creates automatic module _1._20._1
// which conflicts with Forge's "minecraft" module → split-package error.
// Fix: use patched Forge client jar instead of vanilla version jar.
// The patched jar (forge-1.20.1-47.4.20-client.jar) is created by the
// Forge installer in libraries/ and contains all MC classes including
// PreparableReloadListener. It is NOT listed in version.json manifest,
// so we find and add it separately.
// Ensure securejarhandler can create the "minecraft" module:
// It looks for versions/<versionId>/<versionId>.jar.
// The Forge installer only creates the JSON there, not the jar.
// We copy the vanilla version jar (e.g. versions/1.20.1/1.20.1.jar)
// to that location so securejarhandler finds it and creates the
// "minecraft" module (identified by Automatic-Module-Name: minecraft
// in the jar's MANIFEST.MF).
ensureVersionJarForForge();
// Build classpath from manifest libraries only (no version jar on -cp).
// Putting the vanilla jar on -cp would create automatic module _1._20._1
// (from filename 1.20.1.jar) which conflicts with the "minecraft" module
// created by securejarhandler → split-package ResolutionException.
String classpath;
if (manifest != null) {
classpath = buildClasspathFromManifest(manifest, false);
@@ -80,21 +86,6 @@ public class LaunchCommandBuilder {
classpath = buildVanillaClasspath();
}
// Find and add the patched Forge client jar
Path patchedJar = findPatchedForgeJar();
if (patchedJar != null) {
String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
classpath = patchedJar.toAbsolutePath().toString() + sep + classpath;
System.out.println(ZAnsi.green(" Added patched Forge jar: " + patchedJar.getFileName()));
} else {
System.out.println(ZAnsi.yellow(" No patched Forge client jar found, falling back to vanilla version jar"));
Path versionJar = findVersionJar();
if (versionJar != null && isValidJar(versionJar)) {
String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
classpath = versionJar.toAbsolutePath().toString() + sep + classpath;
}
}
command.add("-cp");
String cpFile = writeClasspathFile(classpath);
command.add(cpFile);
@@ -102,8 +93,7 @@ public class LaunchCommandBuilder {
// Add JVM arguments from version.json manifest as-is.
// Forge's -p is a HARDCODED list of 8 bootstrap JARs (bootstraplauncher,
// securejarhandler, asm-*, JarJarFileSystems) — NOT ${classpath}.
// Do NOT intercept it. Version jar stays on -cp only (not -p),
// preventing _1._20._1 vs minecraft split-package conflicts.
// Do NOT intercept it.
if (manifest != null) {
Map<String, String> vars = buildVariableMap(options);
vars.put("classpath", cpFile);
@@ -657,6 +647,47 @@ public class LaunchCommandBuilder {
return null;
}
/**
* Ensure the version jar exists at versions/<versionId>/<versionId>.jar
* so that securejarhandler can create the "minecraft" module from it.
*
* The Forge installer only creates the JSON at that path, not the jar.
* We copy the vanilla version jar (e.g. versions/1.20.1/1.20.1.jar) there.
*
* securejarhandler reads Automatic-Module-Name: minecraft from the jar's
* MANIFEST.MF and creates a named "minecraft" module. This is the module
* that Forge's code expects classes like PreparableReloadListener to be in.
*
* Critical: the jar is NOT put on -cp. Only the copy at versions/<versionId>/
* is used by securejarhandler. The original 1.20.1.jar must stay off the
* classpath to prevent automatic module _1._20._1 from being created,
* which would conflict with the "minecraft" module (split-package error).
*/
private void ensureVersionJarForForge() throws IOException {
String versionId = getVersionId();
String mcVersion = instance.getMinecraftVersion();
Path versionsDir = instance.getPath().resolve("versions");
Path targetDir = versionsDir.resolve(versionId);
Path targetJar = targetDir.resolve(versionId + ".jar");
if (Files.exists(targetJar) && isValidJar(targetJar)) {
System.out.println(ZAnsi.green(" Version jar already exists: " + targetJar));
return;
}
// Source: the vanilla version jar
Path sourceJar = versionsDir.resolve(mcVersion).resolve(mcVersion + ".jar");
if (!Files.exists(sourceJar) || !isValidJar(sourceJar)) {
System.out.println(ZAnsi.yellow(" Vanilla version jar not found at " + sourceJar));
return;
}
Files.createDirectories(targetDir);
Files.copy(sourceJar, targetJar);
System.out.println(ZAnsi.green(" Copied version jar for securejarhandler: " + sourceJar.getFileName() + "" + targetJar));
}
/**
* Find the patched Forge client jar in libraries/.
* The Forge installer creates it via binarypatcher processor: