From d7ba06f24c1835ae4cd43bf0ef896b9e2a831a78 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Tue, 14 Jul 2026 10:30:55 +0000 Subject: [PATCH] fix: rewrite Forge launch to pass version.json args directly (AstralRinth approach) Root cause: our launcher was fighting with version.json by manually constructing -p, -cp, --add-modules, --add-opens, -DignoreList, and -DlibraryDirectory. AstralRinth passes version.json JVM args through with placeholder substitution, then appends memory/GC/custom args. Changes: - VersionManifest: add getAllJvmArguments() and getAllGameArguments() that merge parent (vanilla) args with child (forge) args - LaunchCommandBuilder.build(): rewrite Forge/NeoForge branch to parse ALL version.json JVM args and game args with placeholder substitution - Removed: ensureVersionJarForForge(), findSrgClientJar(), filterClasspathAgainstModulePath(), manual -Djava.library.path for Forge - Classpath: built from version.json libraries + client jar (first position) - Memory/GC args appended AFTER version.json args (like AstralRinth) - Fallback to manual args if version.json has no JVM/game args --- .../launch/LaunchCommandBuilder.java | 211 ++++++++++-------- .../minecraft/launch/VersionManifest.java | 18 ++ 2 files changed, 140 insertions(+), 89 deletions(-) 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 2db755d..f156ba1 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 @@ -44,92 +44,129 @@ public class LaunchCommandBuilder { ? options.getJavaPath() : "java"; command.add(javaPath); - command.addAll(getJvmArguments(options)); - Path nativesDir = instance.getPath().resolve("natives"); if (!Files.exists(nativesDir)) { Files.createDirectories(nativesDir); } - command.add("-Djava.library.path=" + nativesDir.toAbsolutePath()); String loaderType = instance.getLoaderType().toLowerCase(); - boolean isModloader = "fabric".equals(loaderType) || "forge".equals(loaderType) || "neoforge".equals(loaderType); - VersionManifest manifest = resolveVersionManifest(); - - // For modloaders, use vanilla classpath but read main class from manifest when available - if (isModloader) { - // For Forge/NeoForge: use recursive scan of libraries/ for classpath, - // always use ModLauncher as main class (version.json might have - // BootstrapLauncher which breaks with Java module system) - if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) { - // Ensure vanilla jar is at versions//.jar - // so securejarhandler creates the "minecraft" module from it - ensureVersionJarForForge(); - // Build classpath from manifest libraries. - String classpath; - if (manifest != null) { - classpath = buildClasspathFromManifest(manifest, false); - } else { - classpath = ""; - } - if (classpath == null || classpath.isEmpty()) { - classpath = buildVanillaClasspath(); - } + if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) { + // Forge/NeoForge: Pass version.json JVM args and game args through + // with placeholder substitution. Do NOT manually add -p, -cp, + // --add-modules, --add-opens, or -DignoreList. The version.json + // defines all of these. This matches AstralRinth's approach. + System.out.println(ZAnsi.cyan(" Forge/NeoForge: using version.json args with placeholder substitution")); - // The forge client jar (forge-*-client.jar) is NOT listed in the - // version JSON's libraries array, so it's NOT on the classpath. - // FML discovers it by scanning the library directory, but - // securejarhandler only processes classpath entries. - // We add it explicitly so securejarhandler creates the "minecraft" - // module from its Automatic-Module-Name: minecraft. - String forgeVersion = instance.getMinecraftVersion() + "-" + instance.getLoaderVersion(); - Path forgeClientJar = instance.getPath().resolve("libraries") - .resolve("net/minecraftforge/forge") - .resolve(forgeVersion) - .resolve("forge-" + forgeVersion + "-client.jar"); - if (Files.exists(forgeClientJar) && isValidJar(forgeClientJar)) { - String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":"; - classpath = forgeClientJar.toAbsolutePath().toString() + sep + classpath; - System.out.println(ZAnsi.green(" Added forge client jar: " + forgeClientJar.getFileName())); - } else { - System.out.println(ZAnsi.yellow(" Forge client jar not found at " + forgeClientJar)); - } + // Build classpath from manifest libraries + client jar + String classpath; + if (manifest != null) { + classpath = buildClasspathFromManifest(manifest, false); + } else { + classpath = ""; + } - command.add("-cp"); - String cpFile = writeClasspathFile(classpath); - command.add(cpFile); + // Add client jar to classpath (first position) + Path clientJar = findVersionJar(); + if (clientJar != null && isValidJar(clientJar)) { + String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":"; + classpath = clientJar.toAbsolutePath().toString() + (classpath.isEmpty() ? "" : sep + classpath); + System.out.println(ZAnsi.green(" Added client jar: " + clientJar.getFileName())); + } else { + System.out.println(ZAnsi.yellow(" Client jar not found, falling back to vanilla classpath")); + classpath = buildVanillaClasspath(); + } - // Add JVM arguments from version.json manifest. - // Forge's -p is a HARDCODED list of 8 bootstrap JARs - // (bootstraplauncher, securejarhandler, asm-*, JarJarFileSystems). - // CRITICAL: Do NOT modify -DignoreList. The "forge-" pattern - // tells securejarhandler to SKIP forge-related jars (forge-*-client.jar, - // forge-auto-renaming-tool) and NOT create modules from them. - // Removing "forge-" causes ForgeAutoRenamingTool to export - // org.objectweb.asm.tree, splitting with the asm-tree module. - if (manifest != null) { - Map vars = buildVariableMap(options); - vars.put("classpath", cpFile); - for (String arg : manifest.getJvmArguments()) { - String resolved = resolveVariable(arg, vars); + String cpFile = writeClasspathFile(classpath); + + // Build variable map for placeholder substitution + Map vars = buildVariableMap(options); + vars.put("classpath", cpFile); + + // Parse ALL version.json JVM args (merged with parent) with placeholder + // substitution. This includes -Djava.library.path, -cp, --add-modules, + // --add-opens, etc. — everything the launcher needs. + List allJvmArgs = manifest != null ? manifest.getAllJvmArguments() : new ArrayList<>(); + if (!allJvmArgs.isEmpty()) { + for (String arg : allJvmArgs) { + String resolved = resolveVariable(arg, vars); + if (!resolved.isEmpty()) { command.add(resolved); } - // Filter out classpath entries already on module path - // to prevent any remaining split-package conflicts - String filteredCp = filterClasspathAgainstModulePath(classpath, manifest.getJvmArguments()); - if (!filteredCp.equals(classpath)) { - command.set(command.indexOf("-cp") + 1, writeClasspathFile(filteredCp)); - System.out.println(ZAnsi.green(" Filtered classpath against module path")); + } + System.out.println(ZAnsi.green(" Using " + allJvmArgs.size() + " JVM args from version.json")); + } else { + System.out.println(ZAnsi.yellow(" WARNING: No JVM args in version.json, using manual fallback")); + command.addAll(getJvmArguments(options)); + command.add("-Djava.library.path=" + nativesDir.toAbsolutePath()); + command.add("-cp"); + command.add(cpFile); + } + + // Append memory/GC args (always after version.json args, like AstralRinth) + int ramMB = options.getMaxMemory() > 0 ? options.getMaxMemory() : 4096; + command.add("-Xmx" + ramMB + "M"); + command.add("-Xms" + Math.max(512, ramMB / 2) + "M"); + command.add("-XX:+UseG1GC"); + command.add("-XX:+UnlockExperimentalVMOptions"); + command.add("-XX:G1NewSizePercent=20"); + command.add("-XX:G1ReservePercent=20"); + command.add("-XX:MaxGCPauseMillis=50"); + command.add("-XX:G1HeapRegionSize=32M"); + + // Append custom user JVM args + if (options.getExtraJvmArgs() != null && !options.getExtraJvmArgs().isEmpty()) { + command.addAll(options.getExtraJvmArgs()); + } + + // Main class from version.json + String mainClass = null; + if (manifest != null) { + mainClass = manifest.getMainClass(); + } + if (mainClass == null || mainClass.isEmpty()) { + mainClass = "cpw.mods.bootstraplauncher.BootstrapLauncher"; + System.out.println(ZAnsi.yellow(" Using fallback main class: " + mainClass)); + } else { + System.out.println(ZAnsi.green(" Main class from manifest: " + mainClass)); + } + command.add(mainClass); + + // Parse ALL version.json game args (merged with parent) with placeholder + // substitution. This includes --launchTarget, --fml.forgeVersion, + // --fml.mcVersion, --version, --gameDir, --assetsDir, etc. + List allGameArgs = manifest != null ? manifest.getAllGameArguments() : new ArrayList<>(); + if (!allGameArgs.isEmpty()) { + for (String arg : allGameArgs) { + String resolved = resolveVariable(arg, vars); + if (!resolved.isEmpty()) { + command.add(resolved); } } - - command.add(getVanillaMainClass()); + System.out.println(ZAnsi.green(" Using " + allGameArgs.size() + " game args from version.json")); + } else { + System.out.println(ZAnsi.yellow(" WARNING: No game args in version.json, using manual fallback")); command.addAll(getVanillaGameArguments(options)); command.addAll(getModloaderLaunchArgs()); - } else { - System.out.println(ZAnsi.cyan(" Modloader detected (" + loaderType + "), using vanilla classpath")); + } + + // Append width/height if custom resolution + if (options.getWidth() > 0) { + command.add("--width"); + command.add(String.valueOf(options.getWidth())); + } + if (options.getHeight() > 0) { + command.add("--height"); + command.add(String.valueOf(options.getHeight())); + } + } else { + // Fabric, vanilla, and other loaders: use manual JVM args + command.addAll(getJvmArguments(options)); + command.add("-Djava.library.path=" + nativesDir.toAbsolutePath()); + + if ("fabric".equals(loaderType)) { + System.out.println(ZAnsi.cyan(" Fabric: using vanilla classpath")); command.add("-cp"); command.add(writeClasspathFile(buildVanillaClasspath())); String mainClass = null; @@ -143,31 +180,27 @@ public class LaunchCommandBuilder { } command.add(mainClass); command.addAll(getVanillaGameArguments(options)); - } - } else if (manifest != null) { - 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())) { - System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath")); + } else if (manifest != null) { + String classpath = buildClasspathFromManifest(manifest, true); + if (classpath.isEmpty()) { + System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath")); + command.add("-cp"); + command.add(writeClasspathFile(buildVanillaClasspath())); + command.add(getVanillaMainClass()); + command.addAll(getVanillaGameArguments(options)); + } else { + command.add("-cp"); + command.add(writeClasspathFile(classpath)); + String mainClass = resolveMainClass(manifest); + command.add(mainClass); + command.addAll(resolveGameArguments(manifest, options)); + } + } else { command.add("-cp"); command.add(writeClasspathFile(buildVanillaClasspath())); command.add(getVanillaMainClass()); command.addAll(getVanillaGameArguments(options)); - } else { - command.add("-cp"); - command.add(writeClasspathFile(classpath)); - - String mainClass = resolveMainClass(manifest); - command.add(mainClass); - - command.addAll(resolveGameArguments(manifest, options)); } - } else { - command.add("-cp"); - command.add(writeClasspathFile(buildVanillaClasspath())); - command.add(getVanillaMainClass()); - command.addAll(getVanillaGameArguments(options)); } return command; diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java index 74acc4f..dd051e3 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java @@ -46,6 +46,24 @@ public class VersionManifest { public List getGameArguments() { return gameArguments; } public List getLibraries() { return libraries; } + public List getAllJvmArguments() { + List all = new ArrayList<>(); + if (parent != null) { + all.addAll(parent.getAllJvmArguments()); + } + all.addAll(jvmArguments); + return all; + } + + public List getAllGameArguments() { + List all = new ArrayList<>(); + if (parent != null) { + all.addAll(parent.getAllGameArguments()); + } + all.addAll(gameArguments); + return all; + } + public void resolveParent(Path versionsDir) { if (inheritsFrom == null || parent != null) return; Path parentDir = versionsDir.resolve(inheritsFrom);