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 5e2bb1d..8fdd0d3 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,44 +63,50 @@ 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)) { - // BootstrapLauncher creates the module layer ModLauncher needs - // for ServiceLoader discovery. - // Build classpath from manifest (shorter, ordered). - // Only JARs explicitly listed in version.json go on the classpath. - // Do NOT append JARs from recursive scan — tool JARs like - // ForgeAutoRenamingTool that contain shaded ASM packages cause - // split-package conflicts with the Java module system when - // --add-modules=ALL-MODULE-PATH resolves them as automatic modules. + // Build TWO classpaths for Forge/NeoForge: + // 1. modulePath: libraries only (for -p module path) — version jar excluded + // to prevent split-package conflict with Forge's "minecraft" module + // 2. classpath: libraries + version jar (for -cp classpath) — version jar + // included so PreparableReloadListener and other MC classes are loadable + String modulePath; String classpath; if (manifest != null) { - classpath = buildClasspathFromManifest(manifest); + modulePath = buildClasspathFromManifest(manifest, false); + classpath = buildClasspathFromManifest(manifest, true); } else { + modulePath = ""; classpath = ""; } - if (classpath == null || classpath.isEmpty()) { - classpath = 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 (modulePath == null || modulePath.isEmpty()) { + modulePath = buildVanillaClasspath(); } + if (classpath == null || classpath.isEmpty()) { + classpath = modulePath; + } + command.add("-cp"); String cpFile = writeClasspathFile(classpath); + String mpFile = writeClasspathFile(modulePath); command.add(cpFile); - // Add JVM arguments from version.json manifest - // Includes -DignoreList, -p (module path), --add-modules, --add-opens, - // and -DlibraryDirectory — all required for BootstrapLauncher to - // 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). + // Add JVM arguments from version.json manifest. + // Intercept -p / --module-path to use mpFile (no version jar) + // and -cp / --classpath to skip (we already added our own -cp). if (manifest != null) { Map vars = buildVariableMap(options); vars.put("classpath", cpFile); - for (String arg : manifest.getJvmArguments()) { - command.add(resolveVariable(arg, vars)); + List jvmArgs = manifest.getJvmArguments(); + 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)); } } else if (manifest != null) { - String classpath = buildClasspathFromManifest(manifest); + 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())) { @@ -417,7 +423,7 @@ public class LaunchCommandBuilder { List args = new ArrayList<>(); args.add("--version"); - args.add(instance.getName()); + args.add(getVersionId()); args.add("--gameDir"); args.add(instance.getPath().toAbsolutePath().toString()); args.add("--assetsDir"); @@ -491,7 +497,7 @@ public class LaunchCommandBuilder { return result; } - private String buildClasspathFromManifest(VersionManifest manifest) throws Exception { + private String buildClasspathFromManifest(VersionManifest manifest, boolean includeVersionJar) throws Exception { List paths = new ArrayList<>(); Path librariesDir = instance.getPath().resolve("libraries"); @@ -549,14 +555,16 @@ public class LaunchCommandBuilder { System.out.println(ZAnsi.cyan(" Paths dir: " + instance.getPath().resolve("versions"))); } - 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 (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); + } } }