Попытка заставить работать Forge

This commit is contained in:
Sashegdev
2026-04-05 16:18:39 +00:00
parent b29222af68
commit e21fd922ab
3 changed files with 410 additions and 77 deletions
@@ -3,7 +3,7 @@ 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.IOException;
import java.io.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@@ -11,6 +11,8 @@ 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 ForgeInstaller {
@@ -30,56 +32,55 @@ public class ForgeInstaller {
System.out.println(ZAnsi.cyan("Установка базовой версии Minecraft " + mcVersion + "..."));
VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath());
String assetIndex = vanillaInstaller.install(mcVersion); // ← теперь возвращает String
String assetIndex = vanillaInstaller.install(mcVersion);
if (assetIndex == null || assetIndex.isEmpty()) {
System.out.println(ZAnsi.brightRed("Не удалось установить базовую версию Minecraft"));
return false;
}
// Сохраняем assetIndex (очень важно!)
instance.setAssetIndex(assetIndex);
// Шаг 2: Создаём launcher_profiles.json
createLauncherProfile();
// Шаг 3: Скачиваем и запускаем Forge Installer
// Шаг 3: Скачиваем Forge Installer с прогресс-баром
String installerUrl = "https://maven.minecraftforge.net/net/minecraftforge/forge/"
+ mcVersion + "-" + forgeVersion
+ "/forge-" + mcVersion + "-" + forgeVersion + "-installer.jar";
Path installerJar = instance.getPath().resolve("forge-installer.jar");
ProgressBar.show("Скачивание Forge Installer", 0, 100, "%");
downloadFile(installerUrl, installerJar);
ProgressBar.finish("Forge Installer скачан");
System.out.println(ZAnsi.cyan("Скачивание Forge Installer..."));
downloadFileWithProgress(installerUrl, installerJar);
// Шаг 4: Запускаем Forge Installer и показываем его вывод
System.out.println(ZAnsi.cyan("Запуск Forge Installer..."));
ProcessBuilder pb = new ProcessBuilder(
"java",
"-jar",
installerJar.toAbsolutePath().toString(),
"--installClient"
);
pb.directory(instance.getPath().toFile());
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
System.out.println(ZAnsi.yellow("Это может занять несколько минут. Пожалуйста, подождите...\n"));
boolean success = runForgeInstaller(installerJar);
Process process = pb.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
System.out.println(ZAnsi.brightRed("Forge Installer завершился с ошибкой (код " + exitCode + ")"));
// После успешной установки Forge, но перед сохранением метаданных
if (success) {
// Докачиваем пропущенные библиотеки
try {
downloadMissingLibraries(mcVersion, forgeVersion);
} catch (Exception e) {
System.out.println(ZAnsi.yellow("Предупреждение: не удалось докачать некоторые библиотеки: " + e.getMessage()));
}
System.out.println(ZAnsi.brightGreen("\nForge " + forgeVersion + " успешно установлен!"));
instance.setMinecraftVersion(mcVersion);
instance.setLoaderType("forge");
instance.setLoaderVersion(forgeVersion);
// Очищаем временный файл установщика
Files.deleteIfExists(installerJar);
return true;
} else {
System.out.println(ZAnsi.brightRed("\nОшибка при установке Forge!"));
return false;
}
System.out.println(ZAnsi.brightGreen("Forge " + forgeVersion + " успешно установлен!"));
instance.setMinecraftVersion(mcVersion);
instance.setLoaderType("forge");
instance.setLoaderVersion(forgeVersion);
return true;
}
private void createLauncherProfile() throws IOException {
@@ -96,14 +97,174 @@ public class ForgeInstaller {
System.out.println(ZAnsi.yellow("Создан launcher_profiles.json"));
}
private void downloadFile(String url, Path target) throws Exception {
private void downloadFileWithProgress(String url, Path target) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target));
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() != 200) {
throw new IOException("Не удалось скачать Forge installer (HTTP " + response.statusCode() + ")");
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("Forge Installer", percent, 100, "% (" + downloaded + "/" + total + ")");
lastPercent = percent;
}
} else {
// Если размер неизвестен, показываем анимацию
char[] spinner = {'|', '/', '-', '\\'};
int idx = (int) (totalRead / 1024) % 4;
System.out.print("\rСкачивание Forge Installer: " + ProgressBar.formatBytes(totalRead) + " " + spinner[idx]);
}
}
}
ProgressBar.finish("Forge Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")");
}
private boolean runForgeInstaller(Path installerJar) throws IOException, InterruptedException {
// Пробуем до 3 раз с разными опциями
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"
);
// Добавляем JVM аргументы для увеличения таймаутов
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");
// Форматируем вывод Forge Installer
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)) {
// Удаляем только частично скачанные библиотеки Forge
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("Forge 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. Попробуйте установить другую версию Forge"));
}
}
attempt++;
}
return false;
}
private void downloadMissingLibraries(String mcVersion, String forgeVersion) throws Exception {
System.out.println(ZAnsi.cyan("Проверка и докачка отсутствующих библиотек..."));
// Список проблемных библиотек и их альтернативные URL
Map<String, String> 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/9.6/asm-9.6.jar",
"https://mirrors.huaweicloud.com/repository/maven/org/ow2/asm/asm/9.6/asm-9.6.jar");
Path librariesDir = instance.getPath().resolve("libraries");
for (Map.Entry<String, String> 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);
}
}
}
}
}
}
@@ -9,9 +9,6 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* Генерирует полную команду запуска Minecraft (Vanilla / Fabric / Forge)
*/
public class LaunchCommandBuilder {
private final Instance instance;
@@ -34,37 +31,52 @@ public class LaunchCommandBuilder {
// 3. Natives
Path nativesDir = instance.getPath().resolve("natives");
if (!Files.exists(nativesDir)) {
Files.createDirectories(nativesDir);
}
command.add("-Djava.library.path=" + nativesDir.toAbsolutePath());
// 4. Classpath
String classpath = buildClasspath();
command.add("-cp");
command.add(classpath);
// 5. Главный класс
String mainClass = getMainClass();
command.add(mainClass);
// 6. Аргументы Minecraft
command.addAll(getMinecraftArguments(options));
String loaderType = instance.getLoaderType().toLowerCase();
if ("forge".equals(loaderType)) {
// Forge требует особого порядка аргументов
command.addAll(getForgeJvmArguments());
// Forge специфичный classpath
command.add("-cp");
command.add(buildForgeClasspath());
// Главный класс для Forge
command.add("cpw.mods.modlauncher.Launcher");
// Аргументы Forge
command.addAll(getForgeArguments(options));
} else {
// Стандартный classpath для vanilla/fabric
command.add("-cp");
command.add(buildClasspath());
// Главный класс
command.add(getMainClass());
// Стандартные аргументы Minecraft
command.addAll(getMinecraftArguments(options));
}
return command;
}
private String getJavaPath() {
// Пока берём системную java. Позже можно добавить выбор из ~/.zernmc/jre/
return "java";
}
private List<String> getJvmArguments(LaunchOptions options) {
List<String> jvmArgs = new ArrayList<>();
// Выделенная память
int ramMB = options.getMaxMemory() > 0 ? options.getMaxMemory() : 2048;
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");
@@ -72,12 +84,8 @@ public class LaunchCommandBuilder {
jvmArgs.add("-XX:MaxGCPauseMillis=50");
jvmArgs.add("-XX:G1HeapRegionSize=32M");
// Дополнительные JVM аргументы из настроек пользователя
if (options.getExtraJvmArgs() != null && !options.getExtraJvmArgs().isEmpty()) {
jvmArgs.addAll(options.getExtraJvmArgs());
}
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");
@@ -91,35 +99,63 @@ public class LaunchCommandBuilder {
jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED");
}
if (options.getExtraJvmArgs() != null && !options.getExtraJvmArgs().isEmpty()) {
jvmArgs.addAll(options.getExtraJvmArgs());
}
return jvmArgs;
}
private List<String> getForgeJvmArguments() {
List<String> jvmArgs = new ArrayList<>();
// Критические аргументы для Forge
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");
// Forge специфичные свойства
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 {
List<String> paths = new ArrayList<>();
//String loaderType = instance.getLoaderType().toLowerCase();
String versionId = getVersionId();
String versionId = getVersionId(); // ← используем getVersionId()
// Добавляем основной jar
Path versionJar = instance.getPath()
.resolve("versions")
.resolve(versionId)
.resolve(versionId + ".jar");
if (Files.exists(versionJar)) {
paths.add(versionJar.toAbsolutePath().toString());
} else {
Path altVersionJar = instance.getPath()
// Fallback на vanilla версию
String mcVersion = instance.getMinecraftVersion();
Path fallbackJar = instance.getPath()
.resolve("versions")
.resolve(instance.getMinecraftVersion())
.resolve(instance.getMinecraftVersion() + ".jar");
if (Files.exists(altVersionJar)) {
paths.add(altVersionJar.toAbsolutePath().toString());
} else {
System.err.println(ZAnsi.yellow("Warning: Vanilla Minecraft jar not found at: " + versionJar));
.resolve(mcVersion)
.resolve(mcVersion + ".jar");
if (Files.exists(fallbackJar)) {
paths.add(fallbackJar.toAbsolutePath().toString());
}
}
// Все библиотеки
Path librariesDir = instance.getPath().resolve("libraries");
if (Files.exists(librariesDir)) {
try (var stream = Files.walk(librariesDir)) {
@@ -128,6 +164,84 @@ public class LaunchCommandBuilder {
.forEach(paths::add);
}
}
String separator = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
return String.join(separator, paths);
}
private String buildForgeClasspath() throws Exception {
List<String> paths = new ArrayList<>();
String versionId = getVersionId(); // ← используем getVersionId()
String mcVersion = instance.getMinecraftVersion();
String forgeVersion = instance.getLoaderVersion();
// 1. Сначала добавляем все библиотеки из libraries
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);
}
}
// 2. Добавляем jar версии (используя versionId)
Path versionJar = instance.getPath()
.resolve("versions")
.resolve(versionId)
.resolve(versionId + ".jar");
if (Files.exists(versionJar)) {
paths.add(0, versionJar.toAbsolutePath().toString());
} else {
// Fallback на vanilla jar
Path vanillaJar = instance.getPath()
.resolve("versions")
.resolve(mcVersion)
.resolve(mcVersion + ".jar");
if (Files.exists(vanillaJar)) {
paths.add(0, vanillaJar.toAbsolutePath().toString());
}
}
// 3. Добавляем Forge universal jar
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());
}
// 4. Добавляем Forge client jar
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());
}
// 5. Добавляем fmlcore и другие Forge модули
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);
@@ -137,12 +251,10 @@ public class LaunchCommandBuilder {
String loaderType = instance.getLoaderType().toLowerCase();
if ("fabric".equals(loaderType)) {
// Fabric 0.14+ использует KnotClient
return "net.fabricmc.loader.impl.launch.knot.KnotClient";
}
else if ("forge".equals(loaderType)) {
// Forge 1.20.1 использует ClientModLoader
return "net.minecraftforge.client.loading.ClientModLoader";
return "cpw.mods.modlauncher.Launcher";
}
else {
return "net.minecraft.client.main.Main";
@@ -168,15 +280,14 @@ public class LaunchCommandBuilder {
args.add(options.getUsername() != null ? options.getUsername() : "Player");
args.add("--accessToken");
args.add("0"); // потом токен от блядкого сервера
args.add(options.getAccessToken() != null ? options.getAccessToken() : "0");
args.add("--uuid");
args.add("00000000-0000-0000-0000-000000000000"); // тоже потом от блядкого сервера
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()));
@@ -189,6 +300,56 @@ public class LaunchCommandBuilder {
return args;
}
private List<String> getForgeArguments(LaunchOptions options) {
List<String> args = new ArrayList<>();
// Forge требует специфические аргументы в правильном порядке
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");
// Добавляем стандартные аргументы Minecraft (Forge их тоже принимает)
args.add("--gameDir");
args.add(instance.getPath().toAbsolutePath().toString());
args.add("--assetsDir");
args.add(instance.getPath().resolve("assets").toAbsolutePath().toString());
args.add("--assetIndex");
args.add(instance.getAssetIndex());
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 String getVersionId() {
String loaderType = instance.getLoaderType().toLowerCase();
String mcVersion = instance.getMinecraftVersion();
@@ -198,14 +359,14 @@ public class LaunchCommandBuilder {
return mcVersion;
}
else if ("fabric".equals(loaderType)) {
// Fabric использует vanilla версию для jar файла
return mcVersion;
}
else if ("forge".equals(loaderType)) {
// Forge создаёт свою версию в папке versions
return mcVersion + "-forge-" + loaderVer;
}
return mcVersion;
}
}
@@ -52,6 +52,17 @@ public class ProgressBar {
System.out.flush();
}
public static void showAnimated(String label, long current, long total, String unit) {
if (total <= 0) {
// Анимация для неизвестного размера
char[] spinner = {'|', '/', '-', '\\'};
int idx = (int) (current / 1024) % 4;
System.out.print("\r" + label + " [" + spinner[idx] + "] " + formatBytes(current));
} else {
show(label, (int) ((current * 100) / total), 100, unit);
}
}
public static void finish(String message) {
System.out.println("\r" + ZAnsi.brightGreen(message + " завершено ✓"));
System.out.flush();