From f2d3de82f76bed61f35d08c801a0d94d51a37e2d Mon Sep 17 00:00:00 2001 From: SashegDev Date: Mon, 4 May 2026 22:58:49 +0000 Subject: [PATCH] refactor(launch): dynamic version JSON parsing for Forge/NeoForge compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace hardcoded Forge/NeoForge args with version.json parsing - Add VersionManifest.java — parses mainClass, arguments, libraries from JSON - Implement rule matching for OS-specific library/argument filtering - Build classpath dynamically from manifest libraries with fallback resolution - Resolve game args with variable substitution (${version_name}, ${game_directory}, etc.) - Auto-discover version.json path with multiple candidate formats - Support all Forge versions (1.12.2 through 1.21+) and NeoForge out of the box --- .../launch/LaunchCommandBuilder.java | 758 ++++++++---------- .../minecraft/launch/VersionManifest.java | 165 ++++ 2 files changed, 487 insertions(+), 436 deletions(-) create mode 100644 launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java index e91365e..0be55be 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java @@ -3,11 +3,14 @@ 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.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class LaunchCommandBuilder { @@ -22,111 +25,266 @@ public class LaunchCommandBuilder { List command = new ArrayList<>(); - // 1. Путь к Java - String javaPath = getJavaPath(); + String javaPath = "java"; command.add(javaPath); - // 2. JVM аргументы command.addAll(getJvmArguments(options)); - // 3. Natives 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(); - - if ("forge".equals(loaderType)) { - command.addAll(getForgeJvmArguments()); + VersionManifest manifest = resolveVersionManifest(); + if (manifest != null) { command.add("-cp"); - command.add(buildForgeClasspath()); - command.add("cpw.mods.modlauncher.Launcher"); - command.addAll(getForgeArguments(options)); - } else if ("neoforge".equals(loaderType)) { - command.addAll(getNeoForgeJvmArguments()); - command.add("-cp"); - command.add(buildNeoForgeClasspath()); - command.add(getNeoForgeMainClass()); - command.addAll(getNeoForgeArguments(options)); + command.add(buildClasspathFromManifest(manifest)); + + String mainClass = resolveMainClass(manifest); + command.add(mainClass); + + command.addAll(resolveGameArguments(manifest, options)); } else { command.add("-cp"); - command.add(buildClasspath()); - command.add(getMainClass()); - command.addAll(getMinecraftArguments(options)); + command.add(buildVanillaClasspath()); + command.add(getVanillaMainClass()); + command.addAll(getVanillaGameArguments(options)); } return command; } - private String getJavaPath() { - return "java"; + private VersionManifest resolveVersionManifest() { + try { + Path versionJson = findVersionJson(); + if (versionJson != null && Files.exists(versionJson)) { + String content = Files.readString(versionJson); + JSONObject json = new JSONObject(content); + System.out.println(ZAnsi.green("Найден version.json: " + versionJson.getFileName())); + return new VersionManifest(json); + } + } catch (Exception e) { + System.out.println(ZAnsi.yellow("Не удалось загрузить version.json: " + e.getMessage())); + } + return null; } - private List getJvmArguments(LaunchOptions options) { - List jvmArgs = new ArrayList<>(); + private Path findVersionJson() { + Path versionsDir = instance.getPath().resolve("versions"); + String loaderType = instance.getLoaderType().toLowerCase(); + String mcVersion = instance.getMinecraftVersion(); + String loaderVersion = instance.getLoaderVersion(); - int ramMB = options.getMaxMemory() > 0 ? options.getMaxMemory() : 4096; - jvmArgs.add("-Xmx" + ramMB + "M"); - jvmArgs.add("-Xms" + Math.max(512, ramMB / 2) + "M"); + if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) { + String[] candidates = { + getVersionId(), + mcVersion + "-" + loaderType + "-" + loaderVersion, + loaderType + "-" + loaderVersion, + mcVersion + "-" + loaderVersion, + mcVersion + }; + for (String candidate : candidates) { + Path jsonPath = versionsDir.resolve(candidate).resolve(candidate + ".json"); + if (Files.exists(jsonPath)) { + return jsonPath; + } + } - jvmArgs.add("-XX:+UseG1GC"); - jvmArgs.add("-XX:+UnlockExperimentalVMOptions"); - jvmArgs.add("-XX:G1NewSizePercent=20"); - jvmArgs.add("-XX:G1ReservePercent=20"); - jvmArgs.add("-XX:MaxGCPauseMillis=50"); - jvmArgs.add("-XX:G1HeapRegionSize=32M"); + try { + if (Files.exists(versionsDir)) { + try (var stream = Files.list(versionsDir)) { + return stream + .filter(Files::isDirectory) + .filter(dir -> dir.getFileName().toString().contains("forge") || + dir.getFileName().toString().contains("neoforge")) + .filter(dir -> dir.getFileName().toString().contains(mcVersion)) + .findFirst() + .map(dir -> dir.resolve(dir.getFileName().toString() + ".json")) + .filter(Files::exists) + .orElse(null); + } + } + } catch (Exception ignored) {} + } + + Path fallback = versionsDir.resolve(mcVersion).resolve(mcVersion + ".json"); + if (Files.exists(fallback)) { + return fallback; + } + + return null; + } + + private String getVersionId() { + String loaderType = instance.getLoaderType().toLowerCase(); + String mcVersion = instance.getMinecraftVersion(); + String loaderVer = instance.getLoaderVersion(); + + if ("vanilla".equals(loaderType)) { + return mcVersion; + } + else if ("fabric".equals(loaderType)) { + String fabricId = instance.getFabricVersionId(); + if (fabricId != null && !fabricId.isEmpty()) { + return fabricId; + } + return "fabric-loader-" + loaderVer + "-" + mcVersion; + } + else if ("forge".equals(loaderType)) { + return mcVersion + "-forge-" + loaderVer; + } + else if ("neoforge".equals(loaderType)) { + if (mcVersion.equals("1.20.1")) { + return mcVersion + "-neoforge-" + loaderVer; + } + return "neoforge-" + loaderVer; + } + + return mcVersion; + } + + private String resolveMainClass(VersionManifest manifest) { + return manifest.getMainClass(); + } + + private String getVanillaMainClass() { + String loaderType = instance.getLoaderType().toLowerCase(); + if ("fabric".equals(loaderType)) { + return "net.fabricmc.loader.impl.launch.knot.KnotClient"; + } + return "net.minecraft.client.main.Main"; + } + + private List resolveGameArguments(VersionManifest manifest, LaunchOptions options) { + List args = new ArrayList<>(); + Map vars = buildVariableMap(options); + + for (String raw : manifest.getGameArguments()) { + args.add(resolveVariable(raw, vars)); + } + + if (options.getWidth() > 0) { + args.add("--width"); + args.add(String.valueOf(options.getWidth())); + } + if (options.getHeight() > 0) { + args.add("--height"); + args.add(String.valueOf(options.getHeight())); + } + + return args; + } + + private List getVanillaGameArguments(LaunchOptions options) { + List args = new ArrayList<>(); + + args.add("--version"); + args.add(instance.getName()); + args.add("--gameDir"); + args.add(instance.getPath().toAbsolutePath().toString()); + args.add("--assetsDir"); + args.add(instance.getPath().resolve("assets").toAbsolutePath().toString()); + args.add("--assetIndex"); + String assetIndex = instance.getAssetIndex(); + if (assetIndex == null || assetIndex.isEmpty()) { + assetIndex = instance.getMinecraftVersion(); + System.out.println(ZAnsi.yellow("Asset index не найден, использую версию: " + assetIndex)); + } else { + System.out.println(ZAnsi.green("Использую asset index: " + assetIndex)); + } + args.add(assetIndex); + args.add("--username"); + args.add(options.getUsername() != null ? options.getUsername() : "Player"); + args.add("--accessToken"); + args.add(options.getAccessToken() != null ? options.getAccessToken() : "0"); + args.add("--uuid"); + args.add(options.getUuid() != null ? options.getUuid() : "00000000-0000-0000-0000-000000000000"); + args.add("--userType"); + args.add("legacy"); + + return args; + } + + private Map buildVariableMap(LaunchOptions options) { + Map vars = new HashMap<>(); + + Path gameDir = instance.getPath().toAbsolutePath(); + Path assetsDir = gameDir.resolve("assets"); + Path nativesDir = gameDir.resolve("natives"); + Path librariesDir = gameDir.resolve("libraries"); + + vars.put("version_name", instance.getName()); + vars.put("game_directory", gameDir.toString()); + vars.put("assets_root", assetsDir.toString()); + vars.put("assets_index_name", instance.getAssetIndex() != null ? instance.getAssetIndex() : instance.getMinecraftVersion()); + vars.put("auth_player_name", options.getUsername() != null ? options.getUsername() : "Player"); + vars.put("auth_access_token", options.getAccessToken() != null ? options.getAccessToken() : "0"); + vars.put("auth_uuid", options.getUuid() != null ? options.getUuid() : "00000000-0000-0000-0000-000000000000"); + vars.put("auth_xuid", "0"); + vars.put("user_type", "legacy"); + vars.put("version_type", "release"); + vars.put("natives_directory", nativesDir.toString()); + vars.put("library_directory", librariesDir.toString()); + vars.put("launcher_name", "ZernMC"); + vars.put("launcher_version", "1.0"); + vars.put("classpath_separator", System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":"); + vars.put("resolution_width", String.valueOf(options.getWidth() > 0 ? options.getWidth() : 1920)); + vars.put("resolution_height", String.valueOf(options.getHeight() > 0 ? options.getHeight() : 1080)); + vars.put("game_directory", gameDir.toString()); String loaderType = instance.getLoaderType().toLowerCase(); - - if ("fabric".equals(loaderType)) { - jvmArgs.add("--add-modules=ALL-MODULE-PATH"); - jvmArgs.add("--add-opens=java.base/java.io=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.util=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.lang=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.net=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"); - jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED"); + if ("forge".equals(loaderType)) { + vars.put("forge_version", instance.getLoaderVersion() != null ? instance.getLoaderVersion() : ""); + } else if ("neoforge".equals(loaderType)) { + vars.put("neoforge_version", instance.getLoaderVersion() != null ? instance.getLoaderVersion() : ""); + vars.put("fml.neoForgeVersion", instance.getLoaderVersion() != null ? instance.getLoaderVersion() : ""); + vars.put("fml.neoForgeGroup", "net.neoforged"); } - if (options.getExtraJvmArgs() != null && !options.getExtraJvmArgs().isEmpty()) { - jvmArgs.addAll(options.getExtraJvmArgs()); + return vars; + } + + private String resolveVariable(String raw, Map vars) { + if (!raw.contains("${")) return raw; + String result = raw; + for (Map.Entry entry : vars.entrySet()) { + result = result.replace("${" + entry.getKey() + "}", entry.getValue()); } - - return jvmArgs; + return result; } - private List getForgeJvmArguments() { - List jvmArgs = new ArrayList<>(); - - jvmArgs.add("--add-modules=ALL-MODULE-PATH"); - jvmArgs.add("--add-opens=java.base/java.util.jar=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.io=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.net=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.util=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"); - jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED"); - - jvmArgs.add("-Dforge.logging.console.level=debug"); - jvmArgs.add("-Dforge.logging.mojang.level=info"); - jvmArgs.add("-DignoreList=bootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,JarJarFileSystems,client-extra,fmlcore,javafmllanguage,lowcodelanguage,mclanguage,forge-"); - jvmArgs.add("-DmergeModules=jna-5.10.0.jar,jna-platform-5.10.0.jar"); - - return jvmArgs; - } - - private String buildClasspath() throws Exception { + private String buildClasspathFromManifest(VersionManifest manifest) throws Exception { List paths = new ArrayList<>(); + Path librariesDir = instance.getPath().resolve("libraries"); + for (VersionManifest.Library lib : manifest.getLibraries()) { + Path libPath = librariesDir.resolve(lib.artifactPath); + if (Files.exists(libPath)) { + paths.add(libPath.toAbsolutePath().toString()); + } else { + String mavenPath = mavenToPath(lib.name); + Path fallbackPath = librariesDir.resolve(mavenPath); + if (Files.exists(fallbackPath)) { + paths.add(fallbackPath.toAbsolutePath().toString()); + } else { + System.out.println(ZAnsi.yellow(" Библиотека не найдена: " + lib.name)); + } + } + } + + Path versionJar = findVersionJar(); + if (versionJar != null) { + paths.add(0, versionJar.toAbsolutePath().toString()); + } + + String separator = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":"; + return String.join(separator, paths); + } + + private String buildVanillaClasspath() throws Exception { + List paths = new ArrayList<>(); String versionId = getVersionId(); - Path versionJar = instance.getPath() .resolve("versions") .resolve(versionId) @@ -158,383 +316,111 @@ public class LaunchCommandBuilder { return String.join(separator, paths); } - private String buildForgeClasspath() throws Exception { - List paths = new ArrayList<>(); - + private Path findVersionJar() { String versionId = getVersionId(); - String mcVersion = instance.getMinecraftVersion(); - String forgeVersion = instance.getLoaderVersion(); + Path versionsDir = instance.getPath().resolve("versions"); - Path librariesDir = instance.getPath().resolve("libraries"); - if (Files.exists(librariesDir)) { - try (var stream = Files.walk(librariesDir)) { - stream.filter(p -> p.toString().endsWith(".jar")) - .map(p -> p.toAbsolutePath().toString()) - .forEach(paths::add); + Path[] candidates = { + versionsDir.resolve(versionId).resolve(versionId + ".jar"), + versionsDir.resolve(instance.getMinecraftVersion()).resolve(instance.getMinecraftVersion() + ".jar") + }; + + for (Path candidate : candidates) { + if (Files.exists(candidate)) { + return candidate; } } - Path versionJar = instance.getPath() - .resolve("versions") - .resolve(versionId) - .resolve(versionId + ".jar"); - if (Files.exists(versionJar)) { - paths.add(0, versionJar.toAbsolutePath().toString()); - } else { - Path vanillaJar = instance.getPath() - .resolve("versions") - .resolve(mcVersion) - .resolve(mcVersion + ".jar"); - if (Files.exists(vanillaJar)) { - paths.add(0, vanillaJar.toAbsolutePath().toString()); + try { + if (Files.exists(versionsDir)) { + try (var stream = Files.list(versionsDir)) { + return stream + .filter(Files::isDirectory) + .filter(dir -> dir.getFileName().toString().contains("forge") || + dir.getFileName().toString().contains("neoforge")) + .filter(dir -> dir.getFileName().toString().contains(instance.getMinecraftVersion())) + .findFirst() + .map(dir -> dir.resolve(dir.getFileName().toString() + ".jar")) + .filter(Files::exists) + .orElse(null); + } } - } + } catch (Exception ignored) {} - Path forgeUniversal = instance.getPath() - .resolve("libraries") - .resolve("net") - .resolve("minecraftforge") - .resolve("forge") - .resolve(mcVersion + "-" + forgeVersion) - .resolve("forge-" + mcVersion + "-" + forgeVersion + "-universal.jar"); - if (Files.exists(forgeUniversal)) { - paths.add(forgeUniversal.toAbsolutePath().toString()); - } - - Path forgeClient = instance.getPath() - .resolve("libraries") - .resolve("net") - .resolve("minecraftforge") - .resolve("forge") - .resolve(mcVersion + "-" + forgeVersion) - .resolve("forge-" + mcVersion + "-" + forgeVersion + "-client.jar"); - if (Files.exists(forgeClient)) { - paths.add(forgeClient.toAbsolutePath().toString()); - } - - String[] forgeModules = {"fmlcore", "javafmllanguage", "lowcodelanguage", "mclanguage"}; - for (String module : forgeModules) { - Path modulePath = instance.getPath() - .resolve("libraries") - .resolve("net") - .resolve("minecraftforge") - .resolve(module) - .resolve(mcVersion + "-" + forgeVersion) - .resolve(module + "-" + mcVersion + "-" + forgeVersion + ".jar"); - if (Files.exists(modulePath)) { - paths.add(modulePath.toAbsolutePath().toString()); - } - } - - String separator = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":"; - return String.join(separator, paths); + return null; } - private String getMainClass() { + private String mavenToPath(String mavenName) { + String[] parts = mavenName.split(":"); + if (parts.length < 3) return mavenName; + + String group = parts[0].replace('.', '/'); + String artifact = parts[1]; + String version = parts[2]; + + if (parts.length == 4) { + String classifier = parts[3]; + return group + "/" + artifact + "/" + version + "/" + artifact + "-" + version + "-" + classifier + ".jar"; + } + + return group + "/" + artifact + "/" + version + "/" + artifact + "-" + version + ".jar"; + } + + private List getJvmArguments(LaunchOptions options) { + List jvmArgs = new ArrayList<>(); + + int ramMB = options.getMaxMemory() > 0 ? options.getMaxMemory() : 4096; + jvmArgs.add("-Xmx" + ramMB + "M"); + jvmArgs.add("-Xms" + Math.max(512, ramMB / 2) + "M"); + + jvmArgs.add("-XX:+UseG1GC"); + jvmArgs.add("-XX:+UnlockExperimentalVMOptions"); + jvmArgs.add("-XX:G1NewSizePercent=20"); + jvmArgs.add("-XX:G1ReservePercent=20"); + jvmArgs.add("-XX:MaxGCPauseMillis=50"); + jvmArgs.add("-XX:G1HeapRegionSize=32M"); + String loaderType = instance.getLoaderType().toLowerCase(); if ("fabric".equals(loaderType)) { - return "net.fabricmc.loader.impl.launch.knot.KnotClient"; - } - else if ("forge".equals(loaderType)) { - return "cpw.mods.modlauncher.Launcher"; - } - else { - return "net.minecraft.client.main.Main"; - } - } - - /** - * ИСПРАВЛЕНО: используем instance.getAssetIndex() вместо minecraftVersion - */ - private List getMinecraftArguments(LaunchOptions options) { - List args = new ArrayList<>(); - - args.add("--version"); - args.add(instance.getName()); - - args.add("--gameDir"); - args.add(instance.getPath().toAbsolutePath().toString()); - - args.add("--assetsDir"); - args.add(instance.getPath().resolve("assets").toAbsolutePath().toString()); - - // FIXED: Используем правильный assetIndex - args.add("--assetIndex"); - String assetIndex = instance.getAssetIndex(); - if (assetIndex == null || assetIndex.isEmpty()) { - assetIndex = instance.getMinecraftVersion(); - System.out.println(ZAnsi.yellow("Asset index не найден, использую версию: " + assetIndex)); - } else { - System.out.println(ZAnsi.green("Использую asset index: " + assetIndex)); - } - args.add(assetIndex); - - args.add("--username"); - args.add(options.getUsername() != null ? options.getUsername() : "Player"); - - args.add("--accessToken"); - args.add(options.getAccessToken() != null ? options.getAccessToken() : "0"); - - args.add("--uuid"); - args.add(options.getUuid() != null ? options.getUuid() : "00000000-0000-0000-0000-000000000000"); - - args.add("--userType"); - args.add("legacy"); - - if (options.getWidth() > 0) { - args.add("--width"); - args.add(String.valueOf(options.getWidth())); - } - if (options.getHeight() > 0) { - args.add("--height"); - args.add(String.valueOf(options.getHeight())); + jvmArgs.add("--add-modules=ALL-MODULE-PATH"); + jvmArgs.add("--add-opens=java.base/java.io=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.util=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.lang=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.net=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"); + jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED"); + } else if ("forge".equals(loaderType)) { + jvmArgs.add("--add-modules=ALL-MODULE-PATH"); + jvmArgs.add("--add-opens=java.base/java.util.jar=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.io=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.net=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.util=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"); + jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED"); + } else if ("neoforge".equals(loaderType)) { + jvmArgs.add("--add-modules=ALL-MODULE-PATH"); + jvmArgs.add("--add-opens=java.base/java.util.jar=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.io=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.net=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/java.util=ALL-UNNAMED"); + jvmArgs.add("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"); + jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED"); } - return args; - } + if (options.getExtraJvmArgs() != null && !options.getExtraJvmArgs().isEmpty()) { + jvmArgs.addAll(options.getExtraJvmArgs()); + } - /** - * ИСПРАВЛЕНО: для Forge тоже используем правильный assetIndex - */ - private List getForgeArguments(LaunchOptions options) { - List args = new ArrayList<>(); - - args.add("--launchTarget"); - args.add("forgeclient"); - - args.add("--fml.forgeVersion"); - args.add(instance.getLoaderVersion()); - - args.add("--fml.mcVersion"); - args.add(instance.getMinecraftVersion()); - - args.add("--fml.forgeGroup"); - args.add("net.minecraftforge"); - - args.add("--gameDir"); - args.add(instance.getPath().toAbsolutePath().toString()); - - args.add("--assetsDir"); - args.add(instance.getPath().resolve("assets").toAbsolutePath().toString()); - - args.add("--assetIndex"); - String assetIndex = instance.getAssetIndex(); - if (assetIndex == null || assetIndex.isEmpty()) { - assetIndex = instance.getMinecraftVersion(); - } - args.add(assetIndex); - - args.add("--username"); - args.add(options.getUsername() != null ? options.getUsername() : "Player"); - - args.add("--accessToken"); - args.add(options.getAccessToken() != null ? options.getAccessToken() : "0"); - - args.add("--uuid"); - args.add(options.getUuid() != null ? options.getUuid() : "00000000-0000-0000-0000-000000000000"); - - args.add("--userType"); - args.add("legacy"); - - if (options.getWidth() > 0) { - args.add("--width"); - args.add(String.valueOf(options.getWidth())); - } - if (options.getHeight() > 0) { - args.add("--height"); - args.add(String.valueOf(options.getHeight())); - } - - return args; - } - - private List getNeoForgeJvmArguments() { - List jvmArgs = new ArrayList<>(); - - jvmArgs.add("--add-modules=ALL-MODULE-PATH"); - jvmArgs.add("--add-opens=java.base/java.util.jar=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.io=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.net=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/java.util=ALL-UNNAMED"); - jvmArgs.add("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"); - jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED"); - - jvmArgs.add("-Dneoforge.logging.console.level=debug"); - jvmArgs.add("-Dneoforge.logging.mojang.level=info"); - - String mcVersion = instance.getMinecraftVersion(); - if (!mcVersion.equals("1.20.1")) { - jvmArgs.add("-DignoreList=bootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,JarJarFileSystems,client-extra,neoforge-"); - jvmArgs.add("-DmergeModules=jna-5.10.0.jar,jna-platform-5.10.0.jar"); - jvmArgs.add("-DlibraryDirectory=libraries"); - } - return jvmArgs; } - - private String buildNeoForgeClasspath() throws Exception { - List paths = new ArrayList<>(); - - String versionId = getVersionId(); - String mcVersion = instance.getMinecraftVersion(); - String neoForgeVersion = instance.getLoaderVersion(); - - Path librariesDir = instance.getPath().resolve("libraries"); - if (Files.exists(librariesDir)) { - try (var stream = Files.walk(librariesDir)) { - stream.filter(p -> p.toString().endsWith(".jar")) - .map(p -> p.toAbsolutePath().toString()) - .forEach(paths::add); - } - } - - Path versionJar = instance.getPath() - .resolve("versions") - .resolve(versionId) - .resolve(versionId + ".jar"); - if (Files.exists(versionJar)) { - paths.add(0, versionJar.toAbsolutePath().toString()); - } else { - Path vanillaJar = instance.getPath() - .resolve("versions") - .resolve(mcVersion) - .resolve(mcVersion + ".jar"); - if (Files.exists(vanillaJar)) { - paths.add(0, vanillaJar.toAbsolutePath().toString()); - } - } - - String mavenArtifact = mcVersion.equals("1.20.1") ? "forge" : "neoforge"; - - Path neoForgeUniversal = instance.getPath() - .resolve("libraries") - .resolve("net") - .resolve("neoforged") - .resolve(mavenArtifact) - .resolve(neoForgeVersion) - .resolve(mavenArtifact + "-" + neoForgeVersion + "-universal.jar"); - if (Files.exists(neoForgeUniversal)) { - paths.add(neoForgeUniversal.toAbsolutePath().toString()); - } - - Path neoForgeClient = instance.getPath() - .resolve("libraries") - .resolve("net") - .resolve("neoforged") - .resolve(mavenArtifact) - .resolve(neoForgeVersion) - .resolve(mavenArtifact + "-" + neoForgeVersion + "-client.jar"); - if (Files.exists(neoForgeClient)) { - paths.add(neoForgeClient.toAbsolutePath().toString()); - } - - String separator = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":"; - return String.join(separator, paths); - } - - private String getNeoForgeMainClass() { - String mcVersion = instance.getMinecraftVersion(); - if (mcVersion.equals("1.20.1")) { - return "cpw.mods.modlauncher.Launcher"; - } - return "io.neoforged.neoforgespi.CoreMod"; - } - - private List getNeoForgeArguments(LaunchOptions options) { - List args = new ArrayList<>(); - String mcVersion = instance.getMinecraftVersion(); - - if (mcVersion.equals("1.20.1")) { - args.add("--launchTarget"); - args.add("forgeclient"); - args.add("--fml.forgeVersion"); - args.add(instance.getLoaderVersion()); - args.add("--fml.mcVersion"); - args.add(mcVersion); - args.add("--fml.forgeGroup"); - args.add("net.neoforged"); - } else { - args.add("--launchTarget"); - args.add("neoforgeclient"); - args.add("--fml.neoForgeVersion"); - args.add(instance.getLoaderVersion()); - args.add("--fml.mcVersion"); - args.add(mcVersion); - args.add("--fml.neoForgeGroup"); - args.add("net.neoforged"); - } - - args.add("--gameDir"); - args.add(instance.getPath().toAbsolutePath().toString()); - - args.add("--assetsDir"); - args.add(instance.getPath().resolve("assets").toAbsolutePath().toString()); - - args.add("--assetIndex"); - String assetIndex = instance.getAssetIndex(); - if (assetIndex == null || assetIndex.isEmpty()) { - assetIndex = mcVersion; - } - args.add(assetIndex); - - args.add("--username"); - args.add(options.getUsername() != null ? options.getUsername() : "Player"); - - args.add("--accessToken"); - args.add(options.getAccessToken() != null ? options.getAccessToken() : "0"); - - args.add("--uuid"); - args.add(options.getUuid() != null ? options.getUuid() : "00000000-0000-0000-0000-000000000000"); - - args.add("--userType"); - args.add("legacy"); - - if (options.getWidth() > 0) { - args.add("--width"); - args.add(String.valueOf(options.getWidth())); - } - if (options.getHeight() > 0) { - args.add("--height"); - args.add(String.valueOf(options.getHeight())); - } - - return args; - } - - /** - * ИСПРАВЛЕНО: для Fabric используем сохраненный fabricVersionId - */ - private String getVersionId() { - String loaderType = instance.getLoaderType().toLowerCase(); - String mcVersion = instance.getMinecraftVersion(); - String loaderVer = instance.getLoaderVersion(); - - if ("vanilla".equals(loaderType)) { - return mcVersion; - } - else if ("fabric".equals(loaderType)) { - // Используем сохраненный fabricVersionId если есть - String fabricId = instance.getFabricVersionId(); - if (fabricId != null && !fabricId.isEmpty()) { - return fabricId; - } - // fallback - return "fabric-loader-" + loaderVer + "-" + mcVersion; - } - else if ("forge".equals(loaderType)) { - return mcVersion + "-forge-" + loaderVer; - } - else if ("neoforge".equals(loaderType)) { - if (mcVersion.equals("1.20.1")) { - return mcVersion + "-neoforge-" + loaderVer; - } - return "neoforge-" + loaderVer; - } - - return mcVersion; - } -} \ No newline at end of file +} diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java new file mode 100644 index 0000000..2fdea9c --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java @@ -0,0 +1,165 @@ +package me.sashegdev.zernmc.launcher.minecraft.launch; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class VersionManifest { + + private final String id; + private final String mainClass; + private final String assetIndexId; + private final List jvmArguments; + private final List gameArguments; + private final List libraries; + + public VersionManifest(JSONObject json) { + this.id = json.getString("id"); + this.mainClass = json.getString("mainClass"); + + if (json.has("assetIndex")) { + JSONObject ai = json.getJSONObject("assetIndex"); + this.assetIndexId = ai.has("id") ? ai.getString("id") : "unknown"; + } else { + this.assetIndexId = "unknown"; + } + + this.jvmArguments = parseArguments(json, "jvm"); + this.gameArguments = parseArguments(json, "game"); + this.libraries = parseLibraries(json); + } + + public String getId() { return id; } + public String getMainClass() { return mainClass; } + public String getAssetIndexId() { return assetIndexId; } + public List getJvmArguments() { return jvmArguments; } + public List getGameArguments() { return gameArguments; } + public List getLibraries() { return libraries; } + + private List parseArguments(JSONObject json, String type) { + List args = new ArrayList<>(); + if (!json.has("arguments")) return args; + + JSONObject arguments = json.getJSONObject("arguments"); + if (!arguments.has(type)) return args; + + JSONArray arr = arguments.getJSONArray(type); + for (int i = 0; i < arr.length(); i++) { + Object item = arr.get(i); + if (item instanceof String) { + args.add((String) item); + } else if (item instanceof JSONObject) { + JSONObject ruleObj = (JSONObject) item; + if (ruleMatches(ruleObj)) { + Object value = ruleObj.get("value"); + if (value instanceof String) { + args.add((String) value); + } else if (value instanceof JSONArray) { + JSONArray valArr = (JSONArray) value; + for (int j = 0; j < valArr.length(); j++) { + args.add(valArr.getString(j)); + } + } + } + } + } + return args; + } + + private boolean ruleMatches(JSONObject ruleObj) { + JSONArray rules = ruleObj.getJSONArray("rules"); + boolean result = false; + for (int i = 0; i < rules.length(); i++) { + JSONObject rule = rules.getJSONObject(i); + String action = rule.getString("action"); + boolean matches = true; + + if (rule.has("os")) { + JSONObject os = rule.getJSONObject("os"); + String osName = System.getProperty("os.name").toLowerCase(); + if (os.has("name")) { + String reqName = os.getString("name").toLowerCase(); + if (reqName.equals("windows") && !osName.contains("win")) matches = false; + else if (reqName.equals("linux") && !osName.contains("linux") && !osName.contains("nix")) matches = false; + else if (reqName.equals("osx") && !osName.contains("mac")) matches = false; + } + if (os.has("arch")) { + String reqArch = os.getString("arch"); + String osArch = System.getProperty("os.arch"); + if (!reqArch.equals(osArch)) matches = false; + } + } + + if (rule.has("features")) { + JSONObject features = rule.getJSONObject("features"); + for (String key : features.keySet()) { + if (key.startsWith("is_demo_user") || key.startsWith("has_custom_resolution")) continue; + matches = false; + } + } + + if ("allow".equals(action) && matches) { + result = true; + } else if ("disallow".equals(action) && matches) { + return false; + } + } + return result; + } + + private List parseLibraries(JSONObject json) { + List libs = new ArrayList<>(); + if (!json.has("libraries")) return libs; + + JSONArray arr = json.getJSONArray("libraries"); + for (int i = 0; i < arr.length(); i++) { + JSONObject libJson = arr.getJSONObject(i); + if (libJson.has("downloads") && libJson.getJSONObject("downloads").has("artifact")) { + String name = libJson.getString("name"); + String artifactPath = libJson.getJSONObject("downloads").getJSONObject("artifact").getString("path"); + Library lib = new Library(name, artifactPath); + + if (libJson.has("natives")) { + JSONObject natives = libJson.getJSONObject("natives"); + for (String key : natives.keySet()) { + String osKey = key.toLowerCase(); + lib.natives.put(osKey, natives.getString(key)); + } + } + + if (libJson.has("rules")) { + JSONObject dummyObj = new JSONObject(); + dummyObj.put("rules", libJson.getJSONArray("rules")); + dummyObj.put("value", ""); + if (ruleMatches(dummyObj)) { + libs.add(lib); + } + } else { + libs.add(lib); + } + } + } + return libs; + } + + public static class Library { + public final String name; + public final String artifactPath; + public final Map natives = new HashMap<>(); + + public Library(String name, String artifactPath) { + this.name = name; + this.artifactPath = artifactPath; + } + + public String getSimpleName() { + return name.substring(name.indexOf(':') + 1); + } + } +}