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 e784e9d..5eb8b13 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,14 +63,16 @@ 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 manifest's -p is a HARDCODED list of 8 bootstrap JARs - // (bootstraplauncher, securejarhandler, asm-*, JarJarFileSystems) - // using ${library_directory}. Version jar is NOT on -p. - // Put version jar on -cp only → PreparableReloadListener loadable, - // no _1._20._1 vs minecraft split-package conflict. + // 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. String classpath; if (manifest != null) { - classpath = buildClasspathFromManifest(manifest, true); + classpath = buildClasspathFromManifest(manifest, false); } else { classpath = ""; } @@ -78,6 +80,21 @@ 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); @@ -640,6 +657,35 @@ public class LaunchCommandBuilder { return null; } + /** + * Find the patched Forge client jar in libraries/. + * The Forge installer creates it via binarypatcher processor: + * libraries/net/minecraftforge/forge//forge--client.jar + * This jar contains all vanilla MC classes + Forge patches. + * Using it instead of vanilla 1.20.1.jar avoids the _1._20._1 vs minecraft + * split-package conflict, because the filename produces a different automatic + * module name. + */ + private Path findPatchedForgeJar() { + Path librariesDir = instance.getPath().resolve("libraries"); + String mcVersion = instance.getMinecraftVersion(); + String loaderVer = instance.getLoaderVersion(); + + String[] candidates = { + "net/minecraftforge/forge/" + mcVersion + "-" + loaderVer + "/forge-" + mcVersion + "-" + loaderVer + "-client.jar", + "net/neoforged/neoforge/" + mcVersion + "-" + loaderVer + "/neoforge-" + mcVersion + "-" + loaderVer + "-client.jar" + }; + + for (String candidate : candidates) { + Path jarPath = librariesDir.resolve(candidate); + if (Files.exists(jarPath) && isValidJar(jarPath)) { + return jarPath; + } + } + + return null; + } + private String mavenToPath(String mavenName) { String[] parts = mavenName.split(":"); if (parts.length < 3) return mavenName;