diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java index 17d28e7..9d75539 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java @@ -370,6 +370,8 @@ public class LaunchMenu { String newLoaderVersion; if ("fabric".equalsIgnoreCase(currentLoader)) { newLoaderVersion = askFabricLoaderVersion(); + } else if ("neoforge".equalsIgnoreCase(currentLoader)) { + newLoaderVersion = askNeoForgeVersion(mcVersion); } else { newLoaderVersion = askForgeVersion(mcVersion); } @@ -384,6 +386,8 @@ public class LaunchMenu { try { if ("fabric".equalsIgnoreCase(currentLoader)) { success = lib.installFabric(mcVersion, newLoaderVersion); + } else if ("neoforge".equalsIgnoreCase(currentLoader)) { + success = lib.installNeoForge(mcVersion, newLoaderVersion); } else { success = lib.installForge(mcVersion, newLoaderVersion); } @@ -546,11 +550,23 @@ public class LaunchMenu { return; } - String loaderType = selectedLoader.contains("Fabric") ? "fabric" : "forge"; + String loaderType; + if (selectedLoader.contains("Fabric")) { + loaderType = "fabric"; + } else if (selectedLoader.contains("NeoForge")) { + loaderType = "neoforge"; + } else { + loaderType = "forge"; + } - String loaderVersion = loaderType.equals("fabric") - ? askFabricLoaderVersion() - : askForgeVersion(mcVersion); + String loaderVersion; + if (loaderType.equals("fabric")) { + loaderVersion = askFabricLoaderVersion(); + } else if (loaderType.equals("neoforge")) { + loaderVersion = askNeoForgeVersion(mcVersion); + } else { + loaderVersion = askForgeVersion(mcVersion); + } if (loaderVersion == null) return; @@ -568,9 +584,14 @@ public class LaunchMenu { MinecraftLib lib = new MinecraftLib(newInstance); - boolean success = loaderType.equals("fabric") - ? lib.installFabric(mcVersion, loaderVersion) - : lib.installForge(mcVersion, loaderVersion); + boolean success; + if (loaderType.equals("fabric")) { + success = lib.installFabric(mcVersion, loaderVersion); + } else if (loaderType.equals("neoforge")) { + success = lib.installNeoForge(mcVersion, loaderVersion); + } else { + success = lib.installForge(mcVersion, loaderVersion); + } if (success) { System.out.println(ZAnsi.brightGreen("\n[OK] Сборка '" + packName + "' успешно установлена!")); @@ -585,6 +606,7 @@ public class LaunchMenu { List options = new ArrayList<>(); if (isFabricSupported(mcVersion)) options.add("Fabric"); + if (isNeoForgeSupported(mcVersion)) options.add("NeoForge"); if (isForgeSupported(mcVersion)) options.add("Forge"); options.add("Vanilla"); options.add("Назад"); @@ -602,6 +624,12 @@ public class LaunchMenu { version.matches("^1\\.20.*") || version.matches("^1\\.21.*"); } + private boolean isNeoForgeSupported(String version) { + return version.matches("^1\\.20\\.[1-9].*") || + version.matches("^1\\.21.*") || + version.matches("^\\d{2}\\..*"); + } + private String askFabricLoaderVersion() throws Exception { System.out.println(ZAnsi.cyan("Получение списка версий Fabric Loader...")); List versions = ZHttpClient.getFabricLoaderVersions(); @@ -668,4 +696,75 @@ public class LaunchMenu { versions.sort((a, b) -> b.compareTo(a)); return versions; } + + private String askNeoForgeVersion(String mcVersion) throws Exception { + System.out.println(ZAnsi.cyan("Получение списка версий NeoForge для " + mcVersion + "...")); + + List allNeoForgeVersions = getAllNeoForgeVersions(); + + List compatibleVersions = allNeoForgeVersions.stream() + .filter(v -> isNeoForgeVersionCompatible(v, mcVersion)) + .collect(Collectors.toList()); + + if (compatibleVersions.isEmpty()) { + System.out.println(ZAnsi.yellow("Не найдено совместимых версий NeoForge для " + mcVersion)); + ConsoleUtils.pause(); + return null; + } + + List options = compatibleVersions.stream() + .limit(30) + .map(v -> "NeoForge " + v) + .collect(Collectors.toList()); + options.add("Назад"); + + ArrowMenu menu = new ArrowMenu("Выбор версии NeoForge для " + mcVersion, options); + int choice = menu.show(); + + if (choice == -1 || choice == options.size() - 1) return null; + + return compatibleVersions.get(choice); + } + + private boolean isNeoForgeVersionCompatible(String version, String mcVersion) { + if (mcVersion.equals("1.20.1")) { + return version.startsWith("47."); + } + String majorMinor = mcVersion.replace("1.", ""); + String[] parts = majorMinor.split("\\."); + int targetMajor = Integer.parseInt(parts[0]); + return version.startsWith(targetMajor + "."); + } + + private List getAllNeoForgeVersions() throws Exception { + List versions = new ArrayList<>(); + + String[] mavenUrls = { + "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml", + "https://maven.neoforged.net/releases/net/neoforged/forge/maven-metadata.xml" + }; + + for (String mavenUrl : mavenUrls) { + try { + String xml = ZHttpClient.downloadString(mavenUrl); + int index = 0; + while ((index = xml.indexOf("", index)) != -1) { + int start = index + 9; + int end = xml.indexOf("", start); + if (end == -1) break; + + String version = xml.substring(start, end).trim(); + if (!versions.contains(version)) { + versions.add(version); + } + index = end; + } + } catch (Exception e) { + // Skip if one maven doesn't have the artifact + } + } + + versions.sort((a, b) -> b.compareTo(a)); + return versions; + } } \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java index 86be644..7c37175 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java @@ -14,7 +14,7 @@ public class Instance { private final Path path; private String minecraftVersion; - private String loaderType; // vanilla, fabric, forge + private String loaderType; // vanilla, fabric, forge, neoforge private String loaderVersion; private String assetIndex; private boolean isServerPack; // флаг, что это сборка с сервера diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java index 0839625..9593357 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java @@ -2,6 +2,7 @@ package me.sashegdev.zernmc.launcher.minecraft; import me.sashegdev.zernmc.launcher.minecraft.installer.FabricInstaller; import me.sashegdev.zernmc.launcher.minecraft.installer.ForgeInstaller; +import me.sashegdev.zernmc.launcher.minecraft.installer.NeoForgeInstaller; import me.sashegdev.zernmc.launcher.minecraft.installer.VersionInstaller; import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder; import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions; @@ -41,6 +42,11 @@ public class MinecraftLib { return installer.install(minecraftVersion, forgeVersion); } + public boolean installNeoForge(String minecraftVersion, String neoforgeVersion) throws Exception { + NeoForgeInstaller installer = new NeoForgeInstaller(instance); + return installer.install(minecraftVersion, neoforgeVersion); + } + public boolean installFabric(String minecraftVersion, String loaderVersion) throws Exception { FabricInstaller installer = new FabricInstaller(instance); boolean success = installer.install(minecraftVersion, loaderVersion); @@ -76,8 +82,17 @@ public class MinecraftLib { return false; } } else if ("forge".equalsIgnoreCase(loaderType)) { - System.out.println(ZAnsi.yellow("Forge пока не поддерживается")); - return false; + boolean forgeInstalled = installForge(minecraftVersion, loaderVersion); + if (!forgeInstalled) { + System.out.println(ZAnsi.brightRed("Не удалось установить Forge")); + return false; + } + } else if ("neoforge".equalsIgnoreCase(loaderType)) { + boolean neoforgeInstalled = installNeoForge(minecraftVersion, loaderVersion); + if (!neoforgeInstalled) { + System.out.println(ZAnsi.brightRed("Не удалось установить NeoForge")); + return false; + } } // 3. В будущем здесь будет diff и скачивание модов @@ -129,7 +144,8 @@ public class MinecraftLib { try (var stream = Files.walk(versionsDir)) { stream.filter(Files::isDirectory) .filter(dir -> dir.getFileName().toString().contains("fabric-loader") || - dir.getFileName().toString().contains("forge")) + dir.getFileName().toString().contains("forge") || + dir.getFileName().toString().contains("neoforge")) .filter(dir -> !dir.getFileName().toString().contains(keepVersion)) .forEach(this::safeDeleteDirectory); } @@ -163,6 +179,8 @@ public class MinecraftLib { deleteAllExcept(libraries.resolve("net/fabricmc/fabric-loader"), currentLoaderVer); } else if ("forge".equals(loaderType)) { deleteAllExcept(libraries.resolve("net/minecraftforge/forge"), currentLoaderVer); + } else if ("neoforge".equals(loaderType)) { + deleteAllExcept(libraries.resolve("net/neoforged/neoforge"), currentLoaderVer); } // Также чистим versions/ от старых fabric/forge версий diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java index b4d88c3..2aae36d 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java @@ -157,6 +157,12 @@ public class PackDownloader { System.err.println(ZAnsi.brightRed("Не удалось установить Fabric")); return false; } + } else if ("neoforge".equalsIgnoreCase(manifest.getLoaderType())) { + boolean success = lib.installNeoForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion()); + if (!success) { + System.err.println(ZAnsi.brightRed("Не удалось установить NeoForge")); + return false; + } } else if ("forge".equalsIgnoreCase(manifest.getLoaderType())) { boolean success = lib.installForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion()); if (!success) { diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java new file mode 100644 index 0000000..5f345d0 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/NeoForgeInstaller.java @@ -0,0 +1,271 @@ +package me.sashegdev.zernmc.launcher.minecraft.installer; + +import me.sashegdev.zernmc.launcher.minecraft.Instance; +import me.sashegdev.zernmc.launcher.utils.ProgressBar; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; + +import java.io.*; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.HashMap; +import java.util.Map; + +public class NeoForgeInstaller { + + private final Instance instance; + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(java.time.Duration.ofSeconds(30)) + .build(); + + public NeoForgeInstaller(Instance instance) { + this.instance = instance; + } + + public boolean install(String mcVersion, String neoForgeVersion) throws Exception { + System.out.println(ZAnsi.cyan("Установка NeoForge " + neoForgeVersion + " для Minecraft " + mcVersion)); + + System.out.println(ZAnsi.cyan("Установка базовой версии Minecraft " + mcVersion + "...")); + VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath()); + String assetIndex = vanillaInstaller.install(mcVersion); + + if (assetIndex == null || assetIndex.isEmpty()) { + System.out.println(ZAnsi.brightRed("Не удалось установить базовую версию Minecraft")); + return false; + } + + instance.setAssetIndex(assetIndex); + createLauncherProfile(); + + String mavenGroup = getMavenGroup(mcVersion); + String mavenArtifact = getMavenArtifact(mcVersion); + + String installerUrl = "https://maven.neoforged.net/releases/" + + mavenGroup.replace('.', '/') + "/" + + mavenArtifact + "/" + + neoForgeVersion + + "/" + mavenArtifact + "-" + neoForgeVersion + "-installer.jar"; + + Path installerJar = instance.getPath().resolve("neoforge-installer.jar"); + + System.out.println(ZAnsi.cyan("Скачивание NeoForge Installer...")); + downloadFileWithProgress(installerUrl, installerJar); + + System.out.println(ZAnsi.cyan("Запуск NeoForge Installer...")); + System.out.println(ZAnsi.yellow("Это может занять несколько минут. Пожалуйста, подождите...\n")); + + boolean success = runNeoForgeInstaller(installerJar); + + if (success) { + try { + downloadMissingLibraries(mcVersion, neoForgeVersion, mavenGroup, mavenArtifact); + } catch (Exception e) { + System.out.println(ZAnsi.yellow("Предупреждение: не удалось докачать некоторые библиотеки: " + e.getMessage())); + } + + System.out.println(ZAnsi.brightGreen("\nNeoForge " + neoForgeVersion + " успешно установлен!")); + instance.setMinecraftVersion(mcVersion); + instance.setLoaderType("neoforge"); + instance.setLoaderVersion(neoForgeVersion); + + Files.deleteIfExists(installerJar); + return true; + } else { + System.out.println(ZAnsi.brightRed("\nОшибка при установке NeoForge!")); + return false; + } + } + + private String getMavenGroup(String mcVersion) { + if (mcVersion.equals("1.20.1")) { + return "net.neoforged"; + } + return "net.neoforged"; + } + + private String getMavenArtifact(String mcVersion) { + if (mcVersion.equals("1.20.1")) { + return "forge"; + } + return "neoforge"; + } + + private void createLauncherProfile() throws IOException { + Path profilePath = instance.getPath().resolve("launcher_profiles.json"); + if (Files.exists(profilePath)) return; + + String minimalProfile = """ + { + "profiles": {}, + "selectedProfile": "Default" + } + """; + Files.writeString(profilePath, minimalProfile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + System.out.println(ZAnsi.yellow("Создан launcher_profiles.json")); + } + + private void downloadFileWithProgress(String url, Path target) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream()); + + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode()); + } + + long contentLength = response.headers().firstValueAsLong("Content-Length").orElse(-1); + + try (InputStream in = response.body(); + FileOutputStream out = new FileOutputStream(target.toFile())) { + + byte[] buffer = new byte[8192]; + int bytesRead; + long totalRead = 0; + int lastPercent = -1; + + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + totalRead += bytesRead; + + if (contentLength > 0) { + int percent = (int) ((totalRead * 100) / contentLength); + if (percent != lastPercent) { + String downloaded = ProgressBar.formatBytes(totalRead); + String total = ProgressBar.formatBytes(contentLength); + ProgressBar.show("NeoForge Installer", percent, 100, "% (" + downloaded + "/" + total + ")"); + lastPercent = percent; + } + } else { + char[] spinner = {'|', '/', '-', '\\'}; + int idx = (int) (totalRead / 1024) % 4; + System.out.print("\rСкачивание NeoForge Installer: " + ProgressBar.formatBytes(totalRead) + " " + spinner[idx]); + } + } + } + + ProgressBar.finish("NeoForge Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")"); + } + + private boolean runNeoForgeInstaller(Path installerJar) throws IOException, InterruptedException { + int maxRetries = 3; + int attempt = 1; + + while (attempt <= maxRetries) { + System.out.println(ZAnsi.cyan("Попытка " + attempt + " из " + maxRetries)); + + ProcessBuilder pb = new ProcessBuilder( + "java", + "-jar", + installerJar.toAbsolutePath().toString(), + "--installClient" + ); + + pb.environment().put("JAVA_OPTS", "-Dhttp.connectionTimeout=60000 -Dhttp.socketTimeout=60000"); + pb.directory(instance.getPath().toFile()); + pb.redirectErrorStream(true); + + Process process = pb.start(); + + StringBuilder output = new StringBuilder(); + boolean hasErrors = false; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append("\n"); + + if (line.contains("Downloading") || line.contains("Extracting")) { + System.out.println(ZAnsi.blue(" -> " + line)); + } else if (line.contains("SUCCESS") || line.contains("successfully")) { + System.out.println(ZAnsi.brightGreen(" + " + line)); + } else if (line.contains("WARNING") || line.contains("warning")) { + System.out.println(ZAnsi.yellow(" ! " + line)); + } else if (line.contains("ERROR") || line.contains("error") || line.contains("failed") || line.contains("timed out")) { + System.out.println(ZAnsi.brightRed(" X " + line)); + if (line.contains("timed out") || line.contains("failed to download")) { + hasErrors = true; + } + } else if (!line.isBlank()) { + System.out.println(" " + line); + } + } + } + + int exitCode = process.waitFor(); + + if (exitCode == 0 && !hasErrors) { + return true; + } + + if (attempt < maxRetries) { + System.out.println(ZAnsi.yellow("Ошибка при установке. Повторная попытка через 5 секунд...")); + Thread.sleep(5000); + + Path librariesDir = instance.getPath().resolve("libraries"); + if (Files.exists(librariesDir)) { + try (var stream = Files.walk(librariesDir)) { + stream.filter(p -> p.toString().contains("asm") && p.toString().endsWith(".jar")) + .forEach(p -> { + try { Files.deleteIfExists(p); } + catch (IOException e) { /* ignore */ } + }); + } + } + } else { + System.out.println(ZAnsi.brightRed("NeoForge Installer завершился с кодом ошибки: " + exitCode)); + + if (output.toString().contains("timed out")) { + System.out.println(ZAnsi.yellow("\nВозможные решения:")); + System.out.println(ZAnsi.yellow("1. Проверьте интернет-соединение")); + System.out.println(ZAnsi.yellow("2. Запустите лаунчер от имени администратора")); + System.out.println(ZAnsi.yellow("3. Временно отключите антивирус/брандмауэр")); + System.out.println(ZAnsi.yellow("4. Попробуйте установить другую версию NeoForge")); + } + } + + attempt++; + } + + return false; + } + + private void downloadMissingLibraries(String mcVersion, String neoForgeVersion, String mavenGroup, String mavenArtifact) throws Exception { + System.out.println(ZAnsi.cyan("Проверка и докачка отсутствующих библиотек...")); + + Map alternativeUrls = new HashMap<>(); + alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar", + "https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar"); + alternativeUrls.put("org/ow2/asm/asm-commons/9.6/asm-commons-9.6.jar", + "https://repo1.maven.org/maven2/org/ow2/asm/asm-commons/9.6/asm-commons-9.6.jar"); + alternativeUrls.put("org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar", + "https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar"); + + Path librariesDir = instance.getPath().resolve("libraries"); + + for (Map.Entry entry : alternativeUrls.entrySet()) { + Path target = librariesDir.resolve(entry.getKey()); + if (!Files.exists(target)) { + Files.createDirectories(target.getParent()); + System.out.println(ZAnsi.yellow("Докачка: " + target.getFileName())); + + for (int attempt = 1; attempt <= 3; attempt++) { + try { + downloadFileWithProgress(entry.getValue(), target); + break; + } catch (Exception e) { + if (attempt == 3) throw e; + System.out.println(ZAnsi.yellow("Повторная попытка " + attempt + "/3...")); + Thread.sleep(2000); + } + } + } + } + } +} 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 9d1f58a..e91365e 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 @@ -44,6 +44,12 @@ public class LaunchCommandBuilder { 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)); } else { command.add("-cp"); command.add(buildClasspath()); @@ -312,7 +318,6 @@ public class LaunchCommandBuilder { args.add("--assetsDir"); args.add(instance.getPath().resolve("assets").toAbsolutePath().toString()); - // FIXED: Используем правильный assetIndex для Forge args.add("--assetIndex"); String assetIndex = instance.getAssetIndex(); if (assetIndex == null || assetIndex.isEmpty()) { @@ -344,6 +349,162 @@ public class LaunchCommandBuilder { 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 */ @@ -367,6 +528,12 @@ public class LaunchCommandBuilder { 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; } diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java index ffbea61..bf4e848 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java @@ -53,6 +53,7 @@ public class ZHttpClient { MOJANG_META("https://piston-meta.mojang.com", false), MOJANG_RESOURCES("https://resources.download.minecraft.net", false), FORGE_MAVEN("https://maven.minecraftforge.net", false), + NEOFORGE_MAVEN("https://maven.neoforged.net", false), GOOGLE("https://google.com", false), CLOUDFLARE("https://cloudflare.com", false); @@ -106,7 +107,8 @@ public class ZHttpClient { ServiceType.FABRIC_MAVEN, ServiceType.MOJANG_META, ServiceType.MOJANG_RESOURCES, - ServiceType.FORGE_MAVEN + ServiceType.FORGE_MAVEN, + ServiceType.NEOFORGE_MAVEN ); for (ServiceType service : servicesToCheck) { @@ -237,6 +239,7 @@ public class ZHttpClient { return ServiceType.MOJANG_META; if (url.contains("resources.download.minecraft.net")) return ServiceType.MOJANG_RESOURCES; if (url.contains("maven.minecraftforge.net")) return ServiceType.FORGE_MAVEN; + if (url.contains("maven.neoforged.net")) return ServiceType.NEOFORGE_MAVEN; if (url.contains("google.com")) return ServiceType.GOOGLE; if (url.contains("cloudflare.com")) return ServiceType.CLOUDFLARE; return null; diff --git a/server/main.py b/server/main.py index 715d1ee..8706ebe 100644 --- a/server/main.py +++ b/server/main.py @@ -520,7 +520,8 @@ async def list_packs(current_user: dict = Depends(get_current_user)): "updated_at": updated_at, "minecraft_version": meta.get("minecraft_version", "unknown"), "loader_type": meta.get("loader_type", "vanilla"), - "loader_version": meta.get("loader_version") + "loader_version": meta.get("loader_version"), + "asset_index": meta.get("asset_index") }) except Exception as e: logger.error(f"Failed to load pack meta for {pack_dir.name}: {e}") @@ -1079,6 +1080,58 @@ async def proxy_forge_maven(path: str, request: Request): raise HTTPException(502, f"Bad Gateway: {str(e)}") +@app.get("/proxy/neoforge/versions") +async def proxy_neoforge_versions(request: Request): + """Прокси для списка версий NeoForge""" + client_ip = request.client.host if request.client else "unknown" + logger.info(f"Proxy request: NeoForge versions from {client_ip}") + + url = "https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml" + + try: + response = await proxy_client.get(url) + response.raise_for_status() + + return Response( + content=response.content, + media_type="application/xml", + headers={"X-Proxied-By": "ZernMC"} + ) + + except httpx.HTTPError as e: + logger.error(f"Proxy error for NeoForge versions: {e}") + raise HTTPException(502, f"Bad Gateway: {str(e)}") + + +@app.get("/proxy/neoforge/maven/{path:path}") +async def proxy_neoforge_maven(path: str, request: Request): + """Прокси для NeoForge Maven файлов""" + client_ip = request.client.host if request.client else "unknown" + logger.info(f"Proxy request: NeoForge Maven {path} from {client_ip}") + + full_url = f"https://maven.neoforged.net/{path}" + + try: + response = await proxy_client.get(full_url) + response.raise_for_status() + + content_type = "application/octet-stream" + if path.endswith(".jar"): + content_type = "application/java-archive" + elif path.endswith(".pom"): + content_type = "application/xml" + + return Response( + content=response.content, + media_type=content_type, + headers={"X-Proxied-By": "ZernMC"} + ) + + except httpx.HTTPError as e: + logger.error(f"Proxy error for NeoForge Maven {path}: {e}") + raise HTTPException(502, f"Bad Gateway: {str(e)}") + + @app.get("/proxy/download") async def proxy_download(request: Request): """Универсальный прокси для скачивания файлов""" @@ -1096,7 +1149,8 @@ async def proxy_download(request: Request): "launchermeta.mojang.com", "resources.download.minecraft.net", "maven.minecraftforge.net", - "files.minecraftforge.net" + "files.minecraftforge.net", + "maven.neoforged.net" ] # Проверяем, что URL ведет на разрешенный домен @@ -1172,7 +1226,8 @@ async def proxy_status(): "piston-meta.mojang.com", "launchermeta.mojang.com", "resources.download.minecraft.net", - "maven.minecraftforge.net" + "maven.minecraftforge.net", + "maven.neoforged.net" ], "note": "Use this proxy if you have network issues connecting to Fabric/Mojang/Forge" } diff --git a/server/models.py b/server/models.py index 9a9367e..e983664 100644 --- a/server/models.py +++ b/server/models.py @@ -27,4 +27,5 @@ class PackMeta(BaseModel): minecraft_version: str loader_type: str - loader_version: Optional[str] = None \ No newline at end of file + loader_version: Optional[str] = None + asset_index: Optional[str] = None \ No newline at end of file diff --git a/server/pack_manager.py b/server/pack_manager.py index 4eae54c..584321c 100644 --- a/server/pack_manager.py +++ b/server/pack_manager.py @@ -109,6 +109,7 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta: minecraft_version = "1.20.4" loader_type = "vanilla" loader_version = None + asset_index = None pack_config_path = pack_path / "instance.json" if pack_config_path.exists(): @@ -119,6 +120,7 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta: minecraft_version = config.get("minecraftVersion", minecraft_version) loader_type = config.get("loaderType", loader_type) loader_version = config.get("loaderVersion") + asset_index = config.get("assetIndex") except Exception as e: logger.warning(f"Failed to load instance.json for {pack_name}: {e}") @@ -131,7 +133,8 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta: ignored_dirs=ignored_dirs, minecraft_version=minecraft_version, loader_type=loader_type, - loader_version=loader_version + loader_version=loader_version, + asset_index=asset_index ) # Save to disk (синхронно)