diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java index 5eb8b13..faffc0b 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java @@ -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//.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 vars = buildVariableMap(options); vars.put("classpath", cpFile); @@ -657,6 +647,47 @@ public class LaunchCommandBuilder { return null; } + /** + * Ensure the version jar exists at versions//.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// + * 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: