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 0d08e81..f386580 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 @@ -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,27 +31,11 @@ public class LaunchCommandBuilder { private final Instance instance; - private String buildDeduplicatedClasspath(String... classpaths) { - String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":"; - Set 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; } - public List build(LaunchOptions options) throws Exception { + public List build(LaunchOptions options) throws Exception { System.out.println(ZAnsi.cyan("Generating launch command for " + instance.getName() + "...")); List command = new ArrayList<>(); @@ -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//.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 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 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)) { - paths.add(0, versionJar.toAbsolutePath().toString()); - System.out.println(ZAnsi.green(" Added version jar: " + versionJar.getFileName())); + 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" };