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
This commit is contained in:
SashegDev
2026-07-14 10:30:55 +00:00
parent dcb3412a58
commit d7ba06f24c
2 changed files with 140 additions and 89 deletions
@@ -44,92 +44,129 @@ public class LaunchCommandBuilder {
? options.getJavaPath() : "java"; ? options.getJavaPath() : "java";
command.add(javaPath); command.add(javaPath);
command.addAll(getJvmArguments(options));
Path nativesDir = instance.getPath().resolve("natives"); Path nativesDir = instance.getPath().resolve("natives");
if (!Files.exists(nativesDir)) { if (!Files.exists(nativesDir)) {
Files.createDirectories(nativesDir); Files.createDirectories(nativesDir);
} }
command.add("-Djava.library.path=" + nativesDir.toAbsolutePath());
String loaderType = instance.getLoaderType().toLowerCase(); String loaderType = instance.getLoaderType().toLowerCase();
boolean isModloader = "fabric".equals(loaderType) || "forge".equals(loaderType) || "neoforge".equals(loaderType);
VersionManifest manifest = resolveVersionManifest(); 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/<forgeId>/<forgeId>.jar
// so securejarhandler creates the "minecraft" module from it
ensureVersionJarForForge();
// Build classpath from manifest libraries. if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) {
String classpath; // Forge/NeoForge: Pass version.json JVM args and game args through
if (manifest != null) { // with placeholder substitution. Do NOT manually add -p, -cp,
classpath = buildClasspathFromManifest(manifest, false); // --add-modules, --add-opens, or -DignoreList. The version.json
} else { // defines all of these. This matches AstralRinth's approach.
classpath = ""; System.out.println(ZAnsi.cyan(" Forge/NeoForge: using version.json args with placeholder substitution"));
}
if (classpath == null || classpath.isEmpty()) {
classpath = buildVanillaClasspath();
}
// The forge client jar (forge-*-client.jar) is NOT listed in the // Build classpath from manifest libraries + client jar
// version JSON's libraries array, so it's NOT on the classpath. String classpath;
// FML discovers it by scanning the library directory, but if (manifest != null) {
// securejarhandler only processes classpath entries. classpath = buildClasspathFromManifest(manifest, false);
// We add it explicitly so securejarhandler creates the "minecraft" } else {
// module from its Automatic-Module-Name: minecraft. classpath = "";
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));
}
command.add("-cp"); // Add client jar to classpath (first position)
String cpFile = writeClasspathFile(classpath); Path clientJar = findVersionJar();
command.add(cpFile); 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. String cpFile = writeClasspathFile(classpath);
// Forge's -p is a HARDCODED list of 8 bootstrap JARs
// (bootstraplauncher, securejarhandler, asm-*, JarJarFileSystems). // Build variable map for placeholder substitution
// CRITICAL: Do NOT modify -DignoreList. The "forge-" pattern Map<String, String> vars = buildVariableMap(options);
// tells securejarhandler to SKIP forge-related jars (forge-*-client.jar, vars.put("classpath", cpFile);
// forge-auto-renaming-tool) and NOT create modules from them.
// Removing "forge-" causes ForgeAutoRenamingTool to export // Parse ALL version.json JVM args (merged with parent) with placeholder
// org.objectweb.asm.tree, splitting with the asm-tree module. // substitution. This includes -Djava.library.path, -cp, --add-modules,
if (manifest != null) { // --add-opens, etc. — everything the launcher needs.
Map<String, String> vars = buildVariableMap(options); List<String> allJvmArgs = manifest != null ? manifest.getAllJvmArguments() : new ArrayList<>();
vars.put("classpath", cpFile); if (!allJvmArgs.isEmpty()) {
for (String arg : manifest.getJvmArguments()) { for (String arg : allJvmArgs) {
String resolved = resolveVariable(arg, vars); String resolved = resolveVariable(arg, vars);
if (!resolved.isEmpty()) {
command.add(resolved); command.add(resolved);
} }
// Filter out classpath entries already on module path }
// to prevent any remaining split-package conflicts System.out.println(ZAnsi.green(" Using " + allJvmArgs.size() + " JVM args from version.json"));
String filteredCp = filterClasspathAgainstModulePath(classpath, manifest.getJvmArguments()); } else {
if (!filteredCp.equals(classpath)) { System.out.println(ZAnsi.yellow(" WARNING: No JVM args in version.json, using manual fallback"));
command.set(command.indexOf("-cp") + 1, writeClasspathFile(filteredCp)); command.addAll(getJvmArguments(options));
System.out.println(ZAnsi.green(" Filtered classpath against module path")); 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<String> 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);
} }
} }
System.out.println(ZAnsi.green(" Using " + allGameArgs.size() + " game args from version.json"));
command.add(getVanillaMainClass()); } else {
System.out.println(ZAnsi.yellow(" WARNING: No game args in version.json, using manual fallback"));
command.addAll(getVanillaGameArguments(options)); command.addAll(getVanillaGameArguments(options));
command.addAll(getModloaderLaunchArgs()); 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("-cp");
command.add(writeClasspathFile(buildVanillaClasspath())); command.add(writeClasspathFile(buildVanillaClasspath()));
String mainClass = null; String mainClass = null;
@@ -143,31 +180,27 @@ public class LaunchCommandBuilder {
} }
command.add(mainClass); command.add(mainClass);
command.addAll(getVanillaGameArguments(options)); command.addAll(getVanillaGameArguments(options));
} } else if (manifest != null) {
} else if (manifest != null) { String classpath = buildClasspathFromManifest(manifest, true);
String classpath = buildClasspathFromManifest(manifest, true); if (classpath.isEmpty()) {
System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath"));
// Fallback if classpath is empty command.add("-cp");
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions").resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) { command.add(writeClasspathFile(buildVanillaClasspath()));
System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath")); 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("-cp");
command.add(writeClasspathFile(buildVanillaClasspath())); command.add(writeClasspathFile(buildVanillaClasspath()));
command.add(getVanillaMainClass()); command.add(getVanillaMainClass());
command.addAll(getVanillaGameArguments(options)); 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; return command;
@@ -46,6 +46,24 @@ public class VersionManifest {
public List<String> getGameArguments() { return gameArguments; } public List<String> getGameArguments() { return gameArguments; }
public List<Library> getLibraries() { return libraries; } public List<Library> getLibraries() { return libraries; }
public List<String> getAllJvmArguments() {
List<String> all = new ArrayList<>();
if (parent != null) {
all.addAll(parent.getAllJvmArguments());
}
all.addAll(jvmArguments);
return all;
}
public List<String> getAllGameArguments() {
List<String> all = new ArrayList<>();
if (parent != null) {
all.addAll(parent.getAllGameArguments());
}
all.addAll(gameArguments);
return all;
}
public void resolveParent(Path versionsDir) { public void resolveParent(Path versionsDir) {
if (inheritsFrom == null || parent != null) return; if (inheritsFrom == null || parent != null) return;
Path parentDir = versionsDir.resolve(inheritsFrom); Path parentDir = versionsDir.resolve(inheritsFrom);