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:
+93
-60
@@ -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/<forgeId>/<forgeId>.jar
|
||||
// so securejarhandler creates the "minecraft" module from it
|
||||
ensureVersionJarForForge();
|
||||
// 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"));
|
||||
|
||||
// Build classpath from manifest libraries.
|
||||
// Build classpath from manifest libraries + client jar
|
||||
String classpath;
|
||||
if (manifest != null) {
|
||||
classpath = buildClasspathFromManifest(manifest, false);
|
||||
} else {
|
||||
classpath = "";
|
||||
}
|
||||
if (classpath == null || classpath.isEmpty()) {
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
command.add("-cp");
|
||||
String cpFile = writeClasspathFile(classpath);
|
||||
command.add(cpFile);
|
||||
|
||||
// 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) {
|
||||
// Build variable map for placeholder substitution
|
||||
Map<String, String> vars = buildVariableMap(options);
|
||||
vars.put("classpath", cpFile);
|
||||
for (String arg : manifest.getJvmArguments()) {
|
||||
|
||||
// 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<String> 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);
|
||||
}
|
||||
|
||||
command.add(getVanillaMainClass());
|
||||
// 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"));
|
||||
} else {
|
||||
System.out.println(ZAnsi.yellow(" WARNING: No game args in version.json, using manual fallback"));
|
||||
command.addAll(getVanillaGameArguments(options));
|
||||
command.addAll(getModloaderLaunchArgs());
|
||||
}
|
||||
|
||||
// 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 {
|
||||
System.out.println(ZAnsi.cyan(" Modloader detected (" + loaderType + "), using vanilla classpath"));
|
||||
// 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,12 +180,9 @@ 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())) {
|
||||
if (classpath.isEmpty()) {
|
||||
System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath"));
|
||||
command.add("-cp");
|
||||
command.add(writeClasspathFile(buildVanillaClasspath()));
|
||||
@@ -157,10 +191,8 @@ public class LaunchCommandBuilder {
|
||||
} else {
|
||||
command.add("-cp");
|
||||
command.add(writeClasspathFile(classpath));
|
||||
|
||||
String mainClass = resolveMainClass(manifest);
|
||||
command.add(mainClass);
|
||||
|
||||
command.addAll(resolveGameArguments(manifest, options));
|
||||
}
|
||||
} else {
|
||||
@@ -169,6 +201,7 @@ public class LaunchCommandBuilder {
|
||||
command.add(getVanillaMainClass());
|
||||
command.addAll(getVanillaGameArguments(options));
|
||||
}
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
+18
@@ -46,6 +46,24 @@ public class VersionManifest {
|
||||
public List<String> getGameArguments() { return gameArguments; }
|
||||
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) {
|
||||
if (inheritsFrom == null || parent != null) return;
|
||||
Path parentDir = versionsDir.resolve(inheritsFrom);
|
||||
|
||||
Reference in New Issue
Block a user