refactor: deduplicate classpath, improve forge jar detection, clean up comments
This commit is contained in:
+89
-99
@@ -1,21 +1,22 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft.launch;
|
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.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.zip.ZipException;
|
|
||||||
import java.util.zip.ZipFile;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
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 {
|
public class LaunchCommandBuilder {
|
||||||
|
|
||||||
@@ -31,11 +32,27 @@ public class LaunchCommandBuilder {
|
|||||||
|
|
||||||
private final Instance instance;
|
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) {
|
public LaunchCommandBuilder(Instance instance) {
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> build(LaunchOptions options) throws Exception {
|
public List<String> build(LaunchOptions options) throws Exception {
|
||||||
System.out.println(ZAnsi.cyan("Generating launch command for " + instance.getName() + "..."));
|
System.out.println(ZAnsi.cyan("Generating launch command for " + instance.getName() + "..."));
|
||||||
|
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
@@ -57,103 +74,79 @@ public class LaunchCommandBuilder {
|
|||||||
|
|
||||||
VersionManifest manifest = resolveVersionManifest();
|
VersionManifest manifest = resolveVersionManifest();
|
||||||
|
|
||||||
// For modloaders, use vanilla classpath but read main class from manifest when available
|
|
||||||
if (isModloader) {
|
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)) {
|
if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) {
|
||||||
// Build classpath from manifest libraries.
|
System.out.println(ZAnsi.cyan(" Forge/NeoForge - deduplicated classpath"));
|
||||||
String classpath;
|
|
||||||
if (manifest != null) {
|
|
||||||
classpath = buildClasspathFromManifest(manifest, false);
|
|
||||||
} else {
|
|
||||||
classpath = "";
|
|
||||||
}
|
|
||||||
if (classpath == null || classpath.isEmpty()) {
|
|
||||||
classpath = buildVanillaClasspath();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The forge client jar (forge-*-client.jar) is NOT listed in the
|
ensureVersionJarForForge();
|
||||||
// version JSON's libraries array, so it's NOT on the classpath.
|
|
||||||
// FML discovers it by scanning the library directory, but
|
String vanillaCp = buildVanillaClasspath();
|
||||||
// securejarhandler only processes classpath entries.
|
Path forgeJar = findPatchedForgeJar();
|
||||||
// We add it explicitly so securejarhandler creates the "minecraft"
|
|
||||||
// module from its Automatic-Module-Name: minecraft.
|
String classpath;
|
||||||
String forgeVersion = instance.getMinecraftVersion() + "-" + instance.getLoaderVersion();
|
if (forgeJar != null && Files.exists(forgeJar) && isValidJar(forgeJar)) {
|
||||||
Path forgeClientJar = instance.getPath().resolve("libraries")
|
classpath = buildDeduplicatedClasspath(forgeJar.toAbsolutePath().toString(), vanillaCp);
|
||||||
.resolve("net/minecraftforge/forge")
|
System.out.println(ZAnsi.green(" Added Forge jar: " + forgeJar.getFileName()));
|
||||||
.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 {
|
} else {
|
||||||
System.out.println(ZAnsi.yellow(" Forge client jar not found at " + forgeClientJar));
|
classpath = vanillaCp;
|
||||||
}
|
}
|
||||||
|
|
||||||
command.add("-cp");
|
command.add("-cp");
|
||||||
String cpFile = writeClasspathFile(classpath);
|
String cpFile = writeClasspathFile(classpath);
|
||||||
command.add(cpFile);
|
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) {
|
if (manifest != null) {
|
||||||
Map<String, String> vars = buildVariableMap(options);
|
Map<String, String> vars = buildVariableMap(options);
|
||||||
vars.put("classpath", cpFile);
|
vars.put("classpath", cpFile);
|
||||||
|
|
||||||
for (String arg : manifest.getJvmArguments()) {
|
for (String arg : manifest.getJvmArguments()) {
|
||||||
String resolved = resolveVariable(arg, vars);
|
String resolved = resolveVariable(arg, vars);
|
||||||
|
|
||||||
if (resolved.startsWith("-DignoreList=")) {
|
if (resolved.startsWith("-DignoreList=")) {
|
||||||
String value = resolved.substring("-DignoreList=".length());
|
String value = resolved.substring("-DignoreList=".length())
|
||||||
// Remove forge- from the comma-separated list:
|
.replaceAll("forge[^,]*", "")
|
||||||
// - ,forge-, in the middle → just the comma
|
.replaceAll(",+", ",")
|
||||||
// - ,forge- at the end → remove
|
.replaceAll("^,|,$", "");
|
||||||
// - forge-, at the start → remove
|
resolved = "-DignoreList=" + value;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resolved.startsWith("-p ") || resolved.startsWith("--module-path")) {
|
||||||
|
System.out.println(ZAnsi.yellow(" Skipped module-path"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
command.add(resolved);
|
command.add(resolved);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
command.add("-DlibraryDirectory=" + instance.getPath().resolve("libraries").toAbsolutePath());
|
command.add("-DlibraryDirectory=" + instance.getPath().resolve("libraries").toAbsolutePath());
|
||||||
command.add(getVanillaMainClass());
|
command.add(getVanillaMainClass());
|
||||||
command.addAll(getVanillaGameArguments(options));
|
|
||||||
command.addAll(getModloaderLaunchArgs());
|
command.addAll(getModloaderLaunchArgs());
|
||||||
|
command.addAll(getVanillaGameArguments(options));
|
||||||
} else {
|
} else {
|
||||||
|
// Fabric и остальные модлоадеры
|
||||||
System.out.println(ZAnsi.cyan(" Modloader detected (" + loaderType + "), using vanilla classpath"));
|
System.out.println(ZAnsi.cyan(" Modloader detected (" + loaderType + "), using vanilla classpath"));
|
||||||
command.add("-cp");
|
command.add("-cp");
|
||||||
command.add(writeClasspathFile(buildVanillaClasspath()));
|
command.add(writeClasspathFile(buildVanillaClasspath()));
|
||||||
|
|
||||||
String mainClass = null;
|
String mainClass = null;
|
||||||
if (manifest != null) {
|
if (manifest != null) {
|
||||||
mainClass = manifest.getMainClass();
|
mainClass = manifest.getMainClass();
|
||||||
System.out.println(ZAnsi.cyan(" Main class from manifest: " + mainClass));
|
|
||||||
}
|
}
|
||||||
if (mainClass == null || mainClass.isEmpty()) {
|
if (mainClass == null || mainClass.isEmpty()) {
|
||||||
mainClass = getVanillaMainClass();
|
mainClass = getVanillaMainClass();
|
||||||
System.out.println(ZAnsi.yellow(" Using fallback main class: " + mainClass));
|
|
||||||
}
|
}
|
||||||
command.add(mainClass);
|
command.add(mainClass);
|
||||||
command.addAll(getVanillaGameArguments(options));
|
command.addAll(getVanillaGameArguments(options));
|
||||||
}
|
}
|
||||||
} else if (manifest != null) {
|
}
|
||||||
|
else if (manifest != null) {
|
||||||
|
// Vanilla
|
||||||
String classpath = buildClasspathFromManifest(manifest, true);
|
String classpath = buildClasspathFromManifest(manifest, true);
|
||||||
|
|
||||||
// Fallback if classpath is empty
|
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions")
|
||||||
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions").resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) {
|
.resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) {
|
||||||
System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath"));
|
|
||||||
command.add("-cp");
|
command.add("-cp");
|
||||||
command.add(writeClasspathFile(buildVanillaClasspath()));
|
command.add(writeClasspathFile(buildVanillaClasspath()));
|
||||||
command.add(getVanillaMainClass());
|
command.add(getVanillaMainClass());
|
||||||
@@ -161,13 +154,12 @@ public class LaunchCommandBuilder {
|
|||||||
} else {
|
} else {
|
||||||
command.add("-cp");
|
command.add("-cp");
|
||||||
command.add(writeClasspathFile(classpath));
|
command.add(writeClasspathFile(classpath));
|
||||||
|
command.add(resolveMainClass(manifest));
|
||||||
String mainClass = resolveMainClass(manifest);
|
|
||||||
command.add(mainClass);
|
|
||||||
|
|
||||||
command.addAll(resolveGameArguments(manifest, options));
|
command.addAll(resolveGameArguments(manifest, options));
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
|
// Полный fallback
|
||||||
command.add("-cp");
|
command.add("-cp");
|
||||||
command.add(writeClasspathFile(buildVanillaClasspath()));
|
command.add(writeClasspathFile(buildVanillaClasspath()));
|
||||||
command.add(getVanillaMainClass());
|
command.add(getVanillaMainClass());
|
||||||
@@ -519,24 +511,27 @@ public class LaunchCommandBuilder {
|
|||||||
List<String> paths = new ArrayList<>();
|
List<String> paths = new ArrayList<>();
|
||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
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();
|
String loaderType = instance.getLoaderType().toLowerCase();
|
||||||
boolean isForgeLike = "forge".equals(loaderType) || "neoforge".equals(loaderType);
|
boolean isForgeLike = "forge".equals(loaderType) || "neoforge".equals(loaderType);
|
||||||
|
|
||||||
for (VersionManifest.Library lib : manifest.getAllLibraries()) {
|
for (VersionManifest.Library lib : manifest.getAllLibraries()) {
|
||||||
// For Forge/NeoForge: include ALL libraries including net.minecraft:*
|
String libName = lib.name != null ? lib.name.toLowerCase() : "";
|
||||||
// in the classpath string. Forge version.json uses "-p ${classpath}" to
|
|
||||||
// set BOTH classpath and module path to the same string. The Minecraft
|
// ИСКЛЮЧАЕМ forge/neoforge библиотеки, чтобы не было дубликатов
|
||||||
// client JAR must be on the module path so securejarhandler can load it
|
if (libName.contains("net.minecraftforge:forge") || libName.contains("net.neoforged:neoforge")) {
|
||||||
// as a proper module. Skipping it causes ClassNotFoundException.
|
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;
|
Path libPath = lib.artifactPath != null ? librariesDir.resolve(lib.artifactPath) : null;
|
||||||
if (libPath != null && Files.exists(libPath)) {
|
if (libPath != null && Files.exists(libPath)) {
|
||||||
if (isValidJar(libPath)) {
|
if (isValidJar(libPath)) {
|
||||||
paths.add(libPath.toAbsolutePath().toString());
|
paths.add(libPath.toAbsolutePath().toString());
|
||||||
} else {
|
} else {
|
||||||
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
|
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
|
||||||
Files.delete(libPath);
|
Files.deleteIfExists(libPath);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String mavenPath = mavenToPath(lib.name);
|
String mavenPath = mavenToPath(lib.name);
|
||||||
@@ -546,43 +541,30 @@ public class LaunchCommandBuilder {
|
|||||||
paths.add(fallbackPath.toAbsolutePath().toString());
|
paths.add(fallbackPath.toAbsolutePath().toString());
|
||||||
} else {
|
} else {
|
||||||
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
|
System.out.println(ZAnsi.yellow(" Corrupt library, deleting: " + lib.name));
|
||||||
Files.delete(fallbackPath);
|
Files.deleteIfExists(fallbackPath);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Last resort: scan libraries dir for a JAR matching this artifact
|
|
||||||
String artifactName = lib.name.split(":")[1];
|
String artifactName = lib.name.split(":")[1];
|
||||||
Path found = scanForJar(librariesDir, artifactName);
|
Path found = scanForJar(librariesDir, artifactName);
|
||||||
if (found != null) {
|
if (found != null) {
|
||||||
if (isValidJar(found)) {
|
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());
|
paths.add(found.toAbsolutePath().toString());
|
||||||
} else {
|
} else {
|
||||||
System.out.println(ZAnsi.yellow(" Corrupt library (scan), deleting: " + found.getFileName()));
|
Files.deleteIfExists(found);
|
||||||
Files.delete(found);
|
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
if (includeVersionJar) {
|
||||||
Path versionJar = findVersionJar();
|
Path versionJar = findVersionJar();
|
||||||
if (versionJar != null) {
|
if (versionJar != null && isValidJar(versionJar)) {
|
||||||
if (isValidJar(versionJar)) {
|
paths.add(0, versionJar.toAbsolutePath().toString());
|
||||||
paths.add(0, versionJar.toAbsolutePath().toString());
|
System.out.println(ZAnsi.green(" Added version jar: " + versionJar.getFileName()));
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,19 +709,27 @@ public class LaunchCommandBuilder {
|
|||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
Path librariesDir = instance.getPath().resolve("libraries");
|
||||||
String mcVersion = instance.getMinecraftVersion();
|
String mcVersion = instance.getMinecraftVersion();
|
||||||
String loaderVer = instance.getLoaderVersion();
|
String loaderVer = instance.getLoaderVersion();
|
||||||
|
String fullVersion = mcVersion + "-" + loaderVer;
|
||||||
|
|
||||||
String[] candidates = {
|
String[] candidates = {
|
||||||
"net/minecraftforge/forge/" + mcVersion + "-" + loaderVer + "/forge-" + mcVersion + "-" + loaderVer + "-client.jar",
|
// Client jar (то, что ты использовал раньше)
|
||||||
"net/neoforged/neoforge/" + mcVersion + "-" + loaderVer + "/neoforge-" + mcVersion + "-" + loaderVer + "-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) {
|
for (String candidate : candidates) {
|
||||||
Path jarPath = librariesDir.resolve(candidate);
|
Path jarPath = librariesDir.resolve(candidate);
|
||||||
if (Files.exists(jarPath) && isValidJar(jarPath)) {
|
if (Files.exists(jarPath) && isValidJar(jarPath)) {
|
||||||
|
System.out.println(ZAnsi.green(" Found Forge jar: " + jarPath.getFileName()));
|
||||||
return jarPath;
|
return jarPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
System.out.println(ZAnsi.yellow(" No Forge client/universal jar found!"));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user