fix: Forge split-package ResolutionException - remove ignoreList manipulation and call ensureVersionJarForForge
This commit is contained in:
+91
-79
@@ -1,22 +1,21 @@
|
||||
package me.sashegdev.zernmc.launcher.minecraft.launch;
|
||||
|
||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.zip.ZipException;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipException;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
||||
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
||||
|
||||
public class LaunchCommandBuilder {
|
||||
|
||||
@@ -32,22 +31,6 @@ public class LaunchCommandBuilder {
|
||||
|
||||
private final Instance instance;
|
||||
|
||||
private String buildDeduplicatedClasspath(String... classpaths) {
|
||||
String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
|
||||
Set<String> unique = new HashSet<>();
|
||||
|
||||
for (String cp : classpaths) {
|
||||
if (cp == null || cp.isEmpty()) continue;
|
||||
for (String entry : cp.split(sep)) {
|
||||
if (!entry.isEmpty()) {
|
||||
unique.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return String.join(sep, unique);
|
||||
}
|
||||
|
||||
public LaunchCommandBuilder(Instance instance) {
|
||||
this.instance = instance;
|
||||
}
|
||||
@@ -74,79 +57,100 @@ public class LaunchCommandBuilder {
|
||||
|
||||
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)) {
|
||||
System.out.println(ZAnsi.cyan(" Forge/NeoForge - deduplicated classpath"));
|
||||
|
||||
// Ensure vanilla jar is at versions/<forgeId>/<forgeId>.jar
|
||||
// so securejarhandler creates the "minecraft" module from it
|
||||
ensureVersionJarForForge();
|
||||
|
||||
String vanillaCp = buildVanillaClasspath();
|
||||
Path forgeJar = findPatchedForgeJar();
|
||||
|
||||
// Build classpath from manifest libraries.
|
||||
String classpath;
|
||||
if (forgeJar != null && Files.exists(forgeJar) && isValidJar(forgeJar)) {
|
||||
classpath = buildDeduplicatedClasspath(forgeJar.toAbsolutePath().toString(), vanillaCp);
|
||||
System.out.println(ZAnsi.green(" Added Forge jar: " + forgeJar.getFileName()));
|
||||
if (manifest != null) {
|
||||
classpath = buildClasspathFromManifest(manifest, false);
|
||||
} else {
|
||||
classpath = vanillaCp;
|
||||
classpath = "";
|
||||
}
|
||||
if (classpath == null || classpath.isEmpty()) {
|
||||
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) {
|
||||
Map<String, String> vars = buildVariableMap(options);
|
||||
vars.put("classpath", cpFile);
|
||||
|
||||
for (String arg : manifest.getJvmArguments()) {
|
||||
String resolved = resolveVariable(arg, vars);
|
||||
|
||||
if (resolved.startsWith("-DignoreList=")) {
|
||||
String value = resolved.substring("-DignoreList=".length())
|
||||
.replaceAll("forge[^,]*", "")
|
||||
.replaceAll(",+", ",")
|
||||
.replaceAll("^,|,$", "");
|
||||
resolved = "-DignoreList=" + value;
|
||||
}
|
||||
|
||||
if (resolved.startsWith("-p ") || resolved.startsWith("--module-path")) {
|
||||
System.out.println(ZAnsi.yellow(" Skipped module-path"));
|
||||
continue;
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
command.add("-DlibraryDirectory=" + instance.getPath().resolve("libraries").toAbsolutePath());
|
||||
command.add(getVanillaMainClass());
|
||||
command.addAll(getModloaderLaunchArgs());
|
||||
command.addAll(getVanillaGameArguments(options));
|
||||
command.addAll(getModloaderLaunchArgs());
|
||||
} else {
|
||||
// Fabric и остальные модлоадеры
|
||||
System.out.println(ZAnsi.cyan(" Modloader detected (" + loaderType + "), using vanilla classpath"));
|
||||
command.add("-cp");
|
||||
command.add(writeClasspathFile(buildVanillaClasspath()));
|
||||
|
||||
String mainClass = null;
|
||||
if (manifest != null) {
|
||||
mainClass = manifest.getMainClass();
|
||||
System.out.println(ZAnsi.cyan(" Main class from manifest: " + mainClass));
|
||||
}
|
||||
if (mainClass == null || mainClass.isEmpty()) {
|
||||
mainClass = getVanillaMainClass();
|
||||
System.out.println(ZAnsi.yellow(" Using fallback main class: " + mainClass));
|
||||
}
|
||||
command.add(mainClass);
|
||||
command.addAll(getVanillaGameArguments(options));
|
||||
}
|
||||
}
|
||||
else if (manifest != null) {
|
||||
// Vanilla
|
||||
} else if (manifest != null) {
|
||||
String classpath = buildClasspathFromManifest(manifest, true);
|
||||
|
||||
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions")
|
||||
.resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) {
|
||||
|
||||
// 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"));
|
||||
command.add("-cp");
|
||||
command.add(writeClasspathFile(buildVanillaClasspath()));
|
||||
command.add(getVanillaMainClass());
|
||||
@@ -154,12 +158,13 @@ public class LaunchCommandBuilder {
|
||||
} else {
|
||||
command.add("-cp");
|
||||
command.add(writeClasspathFile(classpath));
|
||||
command.add(resolveMainClass(manifest));
|
||||
|
||||
String mainClass = resolveMainClass(manifest);
|
||||
command.add(mainClass);
|
||||
|
||||
command.addAll(resolveGameArguments(manifest, options));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Полный fallback
|
||||
} else {
|
||||
command.add("-cp");
|
||||
command.add(writeClasspathFile(buildVanillaClasspath()));
|
||||
command.add(getVanillaMainClass());
|
||||
@@ -511,27 +516,24 @@ public class LaunchCommandBuilder {
|
||||
List<String> paths = new ArrayList<>();
|
||||
Path librariesDir = instance.getPath().resolve("libraries");
|
||||
|
||||
System.out.println(ZAnsi.cyan(" buildClasspathFromManifest: " + manifest.getAllLibraries().size() + " libraries in manifest"));
|
||||
System.out.println(ZAnsi.cyan(" buildClasspathFromManifest: " + manifest.getAllLibraries().size() + " libraries in manifest (including inherited)"));
|
||||
|
||||
String loaderType = instance.getLoaderType().toLowerCase();
|
||||
boolean isForgeLike = "forge".equals(loaderType) || "neoforge".equals(loaderType);
|
||||
|
||||
for (VersionManifest.Library lib : manifest.getAllLibraries()) {
|
||||
String libName = lib.name != null ? lib.name.toLowerCase() : "";
|
||||
|
||||
// ИСКЛЮЧАЕМ forge/neoforge библиотеки, чтобы не было дубликатов
|
||||
if (libName.contains("net.minecraftforge:forge") || libName.contains("net.neoforged:neoforge")) {
|
||||
System.out.println(ZAnsi.yellow(" Skipped forge/neoforge library from manifest to prevent duplicate"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// For Forge/NeoForge: include ALL libraries including net.minecraft:*
|
||||
// in the classpath string. Forge version.json uses "-p ${classpath}" to
|
||||
// set BOTH classpath and module path to the same string. The Minecraft
|
||||
// client JAR must be on the module path so securejarhandler can load it
|
||||
// as a proper module. Skipping it causes ClassNotFoundException.
|
||||
Path libPath = lib.artifactPath != null ? librariesDir.resolve(lib.artifactPath) : null;
|
||||
if (libPath != null && Files.exists(libPath)) {
|
||||
if (isValidJar(libPath)) {
|
||||
paths.add(libPath.toAbsolutePath().toString());
|
||||
} else {
|
||||
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
|
||||
Files.deleteIfExists(libPath);
|
||||
Files.delete(libPath);
|
||||
}
|
||||
} else {
|
||||
String mavenPath = mavenToPath(lib.name);
|
||||
@@ -541,30 +543,43 @@ public class LaunchCommandBuilder {
|
||||
paths.add(fallbackPath.toAbsolutePath().toString());
|
||||
} else {
|
||||
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
|
||||
Files.deleteIfExists(fallbackPath);
|
||||
Files.delete(fallbackPath);
|
||||
}
|
||||
} else {
|
||||
// Last resort: scan libraries dir for a JAR matching this artifact
|
||||
String artifactName = lib.name.split(":")[1];
|
||||
Path found = scanForJar(librariesDir, artifactName);
|
||||
if (found != null) {
|
||||
if (isValidJar(found)) {
|
||||
System.out.println(ZAnsi.green(" Found by scan: " + lib.name));
|
||||
System.out.println(ZAnsi.green(" Found by scan: " + lib.name + " → " + librariesDir.relativize(found)));
|
||||
paths.add(found.toAbsolutePath().toString());
|
||||
} else {
|
||||
Files.deleteIfExists(found);
|
||||
System.out.println(ZAnsi.yellow(" Corrupt library (scan), deleting: " + found.getFileName()));
|
||||
Files.delete(found);
|
||||
}
|
||||
} else {
|
||||
System.out.println(ZAnsi.yellow(" Library not found: " + lib.name));
|
||||
System.out.println(ZAnsi.yellow(" Library not found (even after scan): " + lib.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(ZAnsi.cyan(" buildClasspathFromManifest: " + paths.size() + " libraries in classpath"));
|
||||
if (isForgeLike) {
|
||||
System.out.println(ZAnsi.cyan(" Version ID: " + getVersionId() + " (MC: " + instance.getMinecraftVersion() + ")"));
|
||||
System.out.println(ZAnsi.cyan(" Paths dir: " + instance.getPath().resolve("versions")));
|
||||
}
|
||||
|
||||
if (includeVersionJar) {
|
||||
Path versionJar = findVersionJar();
|
||||
if (versionJar != null && isValidJar(versionJar)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,11 +727,8 @@ public class LaunchCommandBuilder {
|
||||
String fullVersion = mcVersion + "-" + loaderVer;
|
||||
|
||||
String[] candidates = {
|
||||
// Client jar (то, что ты использовал раньше)
|
||||
"net/minecraftforge/forge/" + fullVersion + "/forge-" + fullVersion + "-client.jar",
|
||||
// Universal jar — самый важный для Forge 1.20.1+
|
||||
"net/minecraftforge/forge/" + fullVersion + "/forge-" + fullVersion + "-universal.jar",
|
||||
// NeoForge варианты
|
||||
"net/neoforged/neoforge/" + fullVersion + "/neoforge-" + fullVersion + "-client.jar",
|
||||
"net/neoforged/neoforge/" + fullVersion + "/neoforge-" + fullVersion + "-universal.jar"
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user