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 18af34d..0d08e81 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,21 +1,22 @@ 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 { @@ -31,11 +32,27 @@ 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<>(); @@ -57,103 +74,79 @@ 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)) { - // Build classpath from manifest libraries. - String classpath; - if (manifest != null) { - classpath = buildClasspathFromManifest(manifest, false); - } else { - classpath = ""; - } - if (classpath == null || classpath.isEmpty()) { - classpath = buildVanillaClasspath(); - } + System.out.println(ZAnsi.cyan(" Forge/NeoForge - deduplicated classpath")); - // 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())); + ensureVersionJarForForge(); + + String vanillaCp = buildVanillaClasspath(); + Path forgeJar = findPatchedForgeJar(); + + 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())); } else { - System.out.println(ZAnsi.yellow(" Forge client jar not found at " + forgeClientJar)); + classpath = vanillaCp; } 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). - // Intercept -DignoreList to remove "forge-" pattern, which - // blocks the forge client jar (forge-*-client.jar) from being - // processed by securejarhandler into the "minecraft" 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()); - // Remove forge- from the comma-separated list: - // - ,forge-, in the middle → just the comma - // - ,forge- at the end → remove - // - forge-, at the start → remove - String modified = value - .replace(",forge-,", ",") - .replace(",forge-", "") - .replace("forge-,", ""); - if (modified.equals("forge-")) modified = ""; - if (!modified.equals(value)) { - System.out.println(ZAnsi.green(" Removed 'forge-' from ignoreList")); - } - resolved = "-DignoreList=" + modified; + 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); } } command.add("-DlibraryDirectory=" + instance.getPath().resolve("libraries").toAbsolutePath()); command.add(getVanillaMainClass()); - command.addAll(getVanillaGameArguments(options)); command.addAll(getModloaderLaunchArgs()); + command.addAll(getVanillaGameArguments(options)); } 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) { + } + else if (manifest != null) { + // Vanilla 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")); + if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions") + .resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) { + command.add("-cp"); command.add(writeClasspathFile(buildVanillaClasspath())); command.add(getVanillaMainClass()); @@ -161,13 +154,12 @@ public class LaunchCommandBuilder { } else { command.add("-cp"); command.add(writeClasspathFile(classpath)); - - String mainClass = resolveMainClass(manifest); - command.add(mainClass); - + command.add(resolveMainClass(manifest)); command.addAll(resolveGameArguments(manifest, options)); } - } else { + } + else { + // Полный fallback command.add("-cp"); command.add(writeClasspathFile(buildVanillaClasspath())); command.add(getVanillaMainClass()); @@ -519,24 +511,27 @@ 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 (including inherited)")); + System.out.println(ZAnsi.cyan(" buildClasspathFromManifest: " + manifest.getAllLibraries().size() + " libraries in manifest")); String loaderType = instance.getLoaderType().toLowerCase(); boolean isForgeLike = "forge".equals(loaderType) || "neoforge".equals(loaderType); for (VersionManifest.Library lib : manifest.getAllLibraries()) { - // 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. + 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; + } + 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.delete(libPath); + Files.deleteIfExists(libPath); } } else { String mavenPath = mavenToPath(lib.name); @@ -546,43 +541,30 @@ public class LaunchCommandBuilder { paths.add(fallbackPath.toAbsolutePath().toString()); } else { System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name)); - Files.delete(fallbackPath); + Files.deleteIfExists(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 + " → " + librariesDir.relativize(found))); + System.out.println(ZAnsi.green(" Found by scan: " + lib.name)); paths.add(found.toAbsolutePath().toString()); } else { - System.out.println(ZAnsi.yellow(" Corrupt library (scan), deleting: " + found.getFileName())); - Files.delete(found); + Files.deleteIfExists(found); } } else { - System.out.println(ZAnsi.yellow(" Library not found (even after scan): " + lib.name)); + System.out.println(ZAnsi.yellow(" Library not found: " + 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) { - 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); - } + if (versionJar != null && isValidJar(versionJar)) { + paths.add(0, versionJar.toAbsolutePath().toString()); + System.out.println(ZAnsi.green(" Added version jar: " + versionJar.getFileName())); } } @@ -727,19 +709,27 @@ public class LaunchCommandBuilder { Path librariesDir = instance.getPath().resolve("libraries"); String mcVersion = instance.getMinecraftVersion(); String loaderVer = instance.getLoaderVersion(); + String fullVersion = mcVersion + "-" + loaderVer; String[] candidates = { - "net/minecraftforge/forge/" + mcVersion + "-" + loaderVer + "/forge-" + mcVersion + "-" + loaderVer + "-client.jar", - "net/neoforged/neoforge/" + mcVersion + "-" + loaderVer + "/neoforge-" + mcVersion + "-" + loaderVer + "-client.jar" + // 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" }; for (String candidate : candidates) { Path jarPath = librariesDir.resolve(candidate); if (Files.exists(jarPath) && isValidJar(jarPath)) { + System.out.println(ZAnsi.green(" Found Forge jar: " + jarPath.getFileName())); return jarPath; } } + System.out.println(ZAnsi.yellow(" No Forge client/universal jar found!")); return null; }