чиним cli + ui | Cli 99% готовность, UI примерно 70%

This commit is contained in:
SashegDev
2026-05-24 18:38:16 +00:00
parent 7014c4a455
commit 166dbf8935
31 changed files with 2132 additions and 2583 deletions
@@ -51,34 +51,34 @@ public class Bootstrap {
isCliMode = argList.contains("--cli");
isJfxMode = !isCliMode;
log("Режим: " + (isCliMode ? "CLI" : "JFX"));
log("Mode: " + (isCliMode ? "CLI" : "JFX"));
String currentVersion = readCurrentVersion();
String serverVersion = getServerVersion();
log("Локальная версия: " + currentVersion);
log("Версия на сервере: " + serverVersion);
log("Local version: " + currentVersion);
log("Server version: " + serverVersion);
loadMirrors();
log("Основной сервер: " + BASE_URL);
log("Mirrors доступны: " + (MIRRORS.size() + 1));
log("Primary server: " + BASE_URL);
log("Mirrors available: " + (MIRRORS.size() + 1));
if (isNewer(serverVersion, currentVersion)) {
log("Доступно обновление!");
log("Update available!");
downloadUpdate(serverVersion);
} else {
log("Версия актуальна");
log("Version is up to date");
}
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log("Получен сигнал завершения...");
log("Shutdown signal received...");
}));
launchMain(args);
}
private static void launchMain(String[] args) throws Exception {
log("Загрузка лаунчера: " + getLauncherJar());
log("Loading launcher: " + getLauncherJar());
if (isCliMode) {
launchInProcess(args);
@@ -105,9 +105,15 @@ public class Bootstrap {
private static void launchInNewProcess(String[] args) throws Exception {
String os = System.getProperty("os.name").toLowerCase();
String javaExe = os.contains("windows") ? "java.exe" : "java";
Path javaBin = findJava(false);
// On Windows, use javaw.exe to hide console in JFX mode
if (os.contains("windows")) {
Path javawPath = javaBin.resolveSibling("javaw.exe");
if (Files.exists(javawPath)) {
javaBin = javawPath;
}
}
Path javafxPath = baseDir.resolve("lib").resolve("javafx");
List<String> cmd = new ArrayList<>();
@@ -132,12 +138,12 @@ public class Bootstrap {
pb.directory(baseDir.toFile());
pb.inheritIO();
log("Запуск процесса: " + String.join(" ", cmd));
log("Starting process: " + String.join(" ", cmd));
Process p = pb.start();
int code = p.waitFor();
log("JFX процесс завершился с кодом: " + code);
log("JFX process exited with code: " + code);
System.exit(code);
}
@@ -162,7 +168,7 @@ public class Bootstrap {
}
if (!Files.exists(javaBin)) {
throw new RuntimeException("Java не найдена");
throw new RuntimeException("Java not found");
}
return javaBin;
}
@@ -186,7 +192,7 @@ public class Bootstrap {
if (v != null && !v.isBlank()) return v;
}
} catch (Exception e) {
log("Ошибка чтения манифеста: " + e.getMessage());
log("Error reading manifest: " + e.getMessage());
}
}
return "0.0.0";
@@ -219,7 +225,7 @@ public class Bootstrap {
}
}
} catch (Exception e) {
log("Ошибка получения версии: " + e.getMessage());
log("Error fetching version: " + e.getMessage());
}
return "unknown";
}
@@ -240,21 +246,18 @@ public class Bootstrap {
}
private static void downloadUpdate(String newVersion) throws Exception {
log("Проверка обновлений...");
log("Checking for updates...");
// Получаем мета с сервера
Map<String, FileMeta> serverFiles = fetchServerMeta(newVersion);
if (serverFiles.isEmpty()) {
log("Не удалось получить мета с сервера");
log("Failed to get server meta");
return;
}
// Сканируем локальные файлы
Map<String, String> localFiles = scanLocalFiles();
log("Локальных файлов: " + localFiles.size());
log("Файлов на сервере: " + serverFiles.size());
log("Local files: " + localFiles.size());
log("Server files: " + serverFiles.size());
// Сравниваем и скачиваем
int downloaded = 0;
int skipped = 0;
@@ -271,17 +274,17 @@ public class Bootstrap {
}
if (localHash != null) {
log("Обновление: " + filePath);
log("Updating: " + filePath);
} else {
log("Скачивание: " + filePath);
log("Downloading: " + filePath);
}
downloadFile(newVersion, filePath, serverMeta.size);
downloaded++;
}
log("Обновлено файлов: " + downloaded + ", пропущено: " + skipped);
log("Обновлено до v" + newVersion);
log("Updated files: " + downloaded + ", skipped: " + skipped);
log("Updated to v" + newVersion);
}
private static Map<String, FileMeta> fetchServerMeta(String version) {
@@ -311,7 +314,7 @@ public class Bootstrap {
}
}
} catch (Exception e) {
log("Ошибка получения мета: " + e.getMessage());
log("Error fetching meta: " + e.getMessage());
}
return files;
}
@@ -349,7 +352,6 @@ public class Bootstrap {
}
private static void downloadFile(String version, String filePath, long expectedSize) throws Exception {
// Пробуем все сервера
List<String> servers = new ArrayList<>();
if (isServerReachable(BASE_URL)) servers.add(BASE_URL);
servers.addAll(MIRRORS);
@@ -365,7 +367,6 @@ public class Bootstrap {
}
}
// Последний резерв - основной сервер
downloadFileFromServer(BASE_URL + "/launcher/file/" + version + "/" + filePath, expectedSize, filePath);
}
@@ -398,7 +399,6 @@ public class Bootstrap {
out.write(buf, 0, len);
downloaded += len;
// Обновляем прогресс на каждом KB
if (downloaded - lastUpdate > 1024 || downloaded == expectedSize) {
long elapsed = System.currentTimeMillis() - startTime;
double speed = downloaded / 1024.0 / 1024.0 / (elapsed / 1000.0 + 0.001);
@@ -417,10 +417,9 @@ public class Bootstrap {
}
}
// Финальная строка
long elapsed = System.currentTimeMillis() - startTime;
double speed = downloaded / 1024.0 / 1024.0 / (elapsed / 1000.0 + 0.001);
System.out.println(String.format("\r[%s] %s - %.1f MB (%.1f MB/s) - Готово!",
System.out.println(String.format("\r[%s] %s - %.1f MB (%.1f MB/s) - Done!",
getProgressBar(downloaded, expectedSize),
fileName,
downloaded / 1024.0 / 1024.0,
@@ -474,7 +473,7 @@ public class Bootstrap {
}
}
} catch (Exception e) {
log("Mirrors недоступны: " + e.getMessage());
log("Mirrors unavailable: " + e.getMessage());
}
}
@@ -489,4 +488,4 @@ public class Bootstrap {
return false;
}
}
}
}
@@ -7,6 +7,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -25,26 +26,23 @@ public class Bootstrap {
log("=== ZernMC Launcher ===");
// Определяем режим запуска
List<String> argList = Arrays.asList(args);
boolean cliMode = argList.contains("--cli");
boolean jfxMode = !cliMode; // по умолчанию JFX
boolean jfxMode = !cliMode;
// Проверка и обновление лаунчера
String currentVersion = readCurrentVersion();
String serverVersion = getServerVersion();
log("Локальная версия: " + currentVersion);
log("Версия на сервере: " + serverVersion);
log("Local version: " + currentVersion);
log("Server version: " + serverVersion);
if (isNewer(serverVersion, currentVersion)) {
log("Доступно обновление!");
log("Update available!");
downloadUpdate(serverVersion);
} else {
log("Версия актуальна");
log("Version is up to date");
}
// Запуск в выбранном режиме
if (jfxMode) {
launchJFX();
} else {
@@ -118,10 +116,10 @@ public class Bootstrap {
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
total += len;
System.out.print("\rСкачано: " + (total/1024/1024) + " MB");
System.out.print("\rDownloaded: " + (total/1024/1024) + " MB");
}
}
log("Скачано");
log("Downloaded");
Path backup = jarFile.resolveSibling(JAR_NAME + ".old");
@@ -130,9 +128,9 @@ public class Bootstrap {
if (Files.exists(backup)) Files.delete(backup);
Files.writeString(baseDir.resolve(VERSION_FILE), newVersion);
log("Обновлено до v" + newVersion);
log("Updated to v" + newVersion);
} else {
throw new IOException("Сервер вернул код: " + conn.getResponseCode());
throw new IOException("Server returned code: " + conn.getResponseCode());
}
}
@@ -140,7 +138,7 @@ public class Bootstrap {
Path javaBin = findJava();
Path jarPath = baseDir.resolve(JAR_NAME);
log("Запуск JFX режима...");
log("Starting JFX mode...");
log("Java: " + javaBin);
log("JAR: " + jarPath);
@@ -176,7 +174,7 @@ public class Bootstrap {
int code = p.waitFor();
try { outputThread.interrupt(); } catch (Exception ignored) {}
log("Завершено с кодом: " + code);
log("Exited with code: " + code);
System.exit(code);
}
@@ -184,7 +182,7 @@ public class Bootstrap {
Path javaBin = findJava();
Path jarPath = baseDir.resolve(JAR_NAME);
log("Запуск CLI режима...");
log("Starting CLI mode...");
log("Java: " + javaBin);
log("JAR: " + jarPath);
@@ -220,7 +218,7 @@ public class Bootstrap {
int code = p.waitFor();
try { outputThread.interrupt(); } catch (Exception ignored) {}
log("Завершено с кодом: " + code);
log("Exited with code: " + code);
System.exit(code);
}
@@ -228,15 +226,12 @@ public class Bootstrap {
String os = System.getProperty("os.name").toLowerCase();
String javaExe = os.contains("windows") ? "java.exe" : "java";
// Сначала ищем jre21/bin/java рядом с лаунчером
Path javaBin = baseDir.resolve("jre21").resolve("bin").resolve(javaExe);
// Если нет, пробуем системную Java
if (!Files.exists(javaBin)) {
javaBin = Paths.get(System.getProperty("java.home"), "bin", javaExe);
}
// Если и это не найдено - ищем java в PATH
if (!Files.exists(javaBin)) {
try {
Process p = new ProcessBuilder("which", javaExe).start();
@@ -252,9 +247,9 @@ public class Bootstrap {
}
if (!Files.exists(javaBin)) {
throw new RuntimeException("Java не найдена. Убедитесь, что jre21 присутствует в папке с лаунчером или Java установлена в системе");
throw new RuntimeException("Java not found. Make sure jre21 is present in the launcher folder or Java is installed on the system");
}
return javaBin;
}
}
}
@@ -30,7 +30,7 @@ public class Main {
ZAnsi.install();
System.out.print("\033[H\033[2J");
System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION));
System.out.println(ZAnsi.brightGreen("Welcome to ZernMC Launcher " + CURRENT_VERSION));
List<String> argList = List.of(args);
boolean jfxMode = argList.contains("--jfx");
@@ -41,22 +41,22 @@ public class Main {
return;
}
System.out.print("\033[H\033[2J");
System.out.println(ZAnsi.brightGreen("Welcome to ZernMC Launcher " + CURRENT_VERSION));
startCLI();
}
private static void launchJFX() {
System.out.println(ZAnsi.cyan("Запуск JFX интерфейса..."));
try {
// Устанавливаем параметры для JavaFX (важно для Windows)
System.setProperty("javafx.runtime.version", "21");
JFXLauncher.main(new String[]{});
} catch (Exception e) {
System.err.println(ZAnsi.brightRed("Ошибка запуска JFX: " + e.getMessage()));
// Проверяем, связано ли это с отсутствием JavaFX
System.err.println(ZAnsi.brightRed("Error starting JFX: " + e.getMessage()));
if (e.getMessage() != null && e.getMessage().contains("QuantumRenderer")) {
System.err.println(ZAnsi.yellow("JavaFX недоступен. Возможно, отсутствуют нативные библиотеки."));
System.err.println(ZAnsi.yellow("Попробуйте использовать CLI режим: --cli"));
System.err.println(ZAnsi.yellow("JavaFX is not available. Native libraries may be missing."));
System.err.println(ZAnsi.yellow("Try CLI mode: --cli"));
}
e.printStackTrace();
System.exit(1);
@@ -66,36 +66,34 @@ public class Main {
private static void startCLI() throws IOException {
ZHttpClient.checkAllServicesOnStartup(true);
System.out.println(ZAnsi.cyan("Проверка авторизации..."));
System.out.println(ZAnsi.cyan("Checking authorization..."));
var sessionResponse = api.checkSession();
if (!sessionResponse.isSuccess()) {
LoginMenu loginMenu = new LoginMenu();
boolean loggedIn = loginMenu.show();
if (!loggedIn) {
System.out.println(ZAnsi.yellow("До свидания!"));
System.out.println(ZAnsi.yellow("Goodbye!"));
ZAnsi.uninstall();
System.exit(0);
}
} else {
var sessionInfo = sessionResponse.getData();
System.out.println(ZAnsi.brightGreen("Добро пожаловать обратно, " + sessionInfo.getUsername() + "!"));
System.out.println(ZAnsi.brightGreen("Welcome back, " + sessionInfo.getUsername() + "!"));
}
System.out.println(ZAnsi.cyan("Запуск CLI режима..."));
System.out.println(ZAnsi.cyan("Starting CLI mode..."));
// === ГЛАВНЫЙ ЦИКЛ ===
try {
mainLoop();
} catch (Exception e) {
System.err.println(ZAnsi.brightRed("Критическая ошибка: " + e.getMessage()));
System.err.println(ZAnsi.brightRed("Critical error: " + e.getMessage()));
e.printStackTrace();
} finally {
ZAnsi.uninstall();
}
}
// ====================== ГЛАВНЫЙ ЦИКЛ ======================
private static void mainLoop() throws Exception {
if (Config.isZernMCBuild()) {
zernMCFlow();
@@ -104,24 +102,21 @@ public class Main {
}
}
// ====================== ZERNMC FLOW ======================
private static void zernMCFlow() throws Exception {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.header("=== ZernMC Private Launcher ==="));
// 1. Проверка подключения к серверу
System.out.println(ZAnsi.cyan("Проверка подключения к ZernMC серверу..."));
System.out.println(ZAnsi.cyan("Checking connection to ZernMC server..."));
try {
String response = ZHttpClient.get("/health");
System.out.println(ZAnsi.brightGreen("Сервер доступен"));
System.out.println(ZAnsi.brightGreen("Server is available"));
} catch (Exception e) {
System.out.println(ZAnsi.brightRed("Не удалось подключиться к ZernMC серверу"));
System.out.println(ZAnsi.white("Ошибка: " + e.getMessage()));
System.out.println(ZAnsi.brightRed("Could not connect to ZernMC server"));
System.out.println(ZAnsi.white("Error: " + e.getMessage()));
ConsoleUtils.pause();
System.exit(1);
}
// 2. Авторизация
boolean sessionRestored = AuthManager.loadSavedSession();
if (!sessionRestored) {
LoginMenu loginMenu = new LoginMenu();
@@ -130,42 +125,40 @@ public class Main {
System.exit(0);
}
} else {
System.out.println(ZAnsi.brightGreen("Добро пожаловать обратно, " + AuthManager.getUsername() + "!"));
System.out.println(ZAnsi.brightGreen("Welcome back, " + AuthManager.getUsername() + "!"));
}
// 3. Запуск меню (LaunchMenu сам определит режим и вызовет нужный flow)
LaunchMenu launchMenu = new LaunchMenu();
launchMenu.show(); // ← Здесь будет вызван showZernMCOnly() внутри
launchMenu.show();
}
// ====================== GLOBAL FLOW ======================
private static void globalFlow() throws Exception {
while (true) {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.header("=== ZernMC Launcher ==="));
List<String> options = List.of(
"Запустить игру",
"Проверка обновлений",
"Настройки",
"Проверка подключения к серверам",
"Выход"
"Launch Game",
"Check Updates",
"Settings",
"Server Connection Check",
"Exit"
);
ArrowMenu menu = new ArrowMenu("Главное меню", options);
ArrowMenu menu = new ArrowMenu("Main Menu", options);
int choice = menu.show();
if (choice == -1 || choice == 4) {
System.out.println(ZAnsi.yellow("До свидания!"));
System.out.println(ZAnsi.yellow("Goodbye!"));
break;
}
switch (choice) {
case 0 -> new LaunchMenu().show(); // обычный LaunchMenu
case 0 -> new LaunchMenu().show();
case 1 -> new UpdateMenu().show();
case 2 -> new SettingsMenu().show();
case 3 -> new ServerCheckMenu().show();
}
}
}
}
}
@@ -33,8 +33,6 @@ public class LauncherAPI {
return launchService;
}
// ====================== Удобные методы ======================
public boolean isLoggedIn() {
return authService.isLoggedIn();
}
@@ -55,6 +53,14 @@ public class LauncherAPI {
return authService.logout();
}
public ApiResponse<Boolean> activatePass(String passCode) {
return authService.activatePass(passCode);
}
public ApiResponse<AuthService.LoginResult> register(String username, String password) {
return authService.register(username, password);
}
public ApiResponse<List<InstanceService.InstanceInfo>> getAllInstances() {
return instanceService.getAllInstances();
}
@@ -83,7 +89,7 @@ public class LauncherAPI {
} catch (Exception e) {
System.out.println("[API] MC versions fetch failed: " + e.getMessage());
}
return ApiResponse.error("Не удалось загрузить версии Minecraft");
return ApiResponse.error("Failed to load Minecraft versions");
}
public ApiResponse<List<String>> getLoaderVersions(String mcVersion, String loader) {
@@ -130,7 +136,7 @@ public class LauncherAPI {
return ApiResponse.success(versions);
} catch (Exception e) {
System.out.println("[API] Loader versions fetch failed: " + e.getMessage());
return ApiResponse.error("Не удалось загрузить версии лоадера");
return ApiResponse.error("Failed to load loader versions");
}
}
@@ -147,7 +153,7 @@ public class LauncherAPI {
try {
String token = authService.getCurrentToken();
if (token == null) {
return ApiResponse.error("Не авторизован");
return ApiResponse.error("Not logged in");
}
String response = ZHttpClient.get("/packs");
@@ -167,7 +173,7 @@ public class LauncherAPI {
return ApiResponse.success(packs);
} catch (Exception e) {
System.out.println("[API] Packs fetch failed: " + e.getMessage());
return ApiResponse.error("Ошибка загрузки сборок: " + e.getMessage());
return ApiResponse.error("Failed to load packs: " + e.getMessage());
}
}
}
@@ -8,6 +8,27 @@ import java.io.IOException;
public class AuthService {
public ApiResponse<LoginResult> register(String username, String password) {
try {
String response = post("/auth/register",
"{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}");
// If registration succeeds, auto-login
AuthManager.AuthResult result = AuthManager.login(username, password);
if (result.success) {
LoginResult loginResult = new LoginResult(AuthManager.getUsername(), AuthManager.getAccessToken());
return ApiResponse.success(loginResult);
}
return ApiResponse.error(result.error != null ? result.error : "Registration failed");
} catch (Exception e) {
String msg = e.getMessage();
if (msg != null && msg.contains("HTTP 409")) {
return ApiResponse.error("Username already taken");
}
return ApiResponse.error("Registration error: " + msg);
}
}
public ApiResponse<LoginResult> login(String username, String password) {
try {
AuthManager.AuthResult result = AuthManager.login(username, password);
@@ -15,9 +36,9 @@ public class AuthService {
LoginResult loginResult = new LoginResult(AuthManager.getUsername(), AuthManager.getAccessToken());
return ApiResponse.success(loginResult);
}
return ApiResponse.error(result.error != null ? result.error : "Неверный логин или пароль");
return ApiResponse.error(result.error != null ? result.error : "Invalid login or password");
} catch (Exception e) {
return ApiResponse.error("Ошибка авторизации: " + e.getMessage());
return ApiResponse.error("Auth error: " + e.getMessage());
}
}
@@ -26,7 +47,7 @@ public class AuthService {
AuthManager.logout();
return ApiResponse.success(true);
} catch (Exception e) {
return ApiResponse.error("Ошибка при выходе: " + e.getMessage());
return ApiResponse.error("Logout error: " + e.getMessage());
}
}
@@ -43,9 +64,9 @@ public class AuthService {
);
return ApiResponse.success(info);
}
return ApiResponse.error("Сессия не найдена");
return ApiResponse.error("Session not found");
} catch (Exception e) {
return ApiResponse.error("Ошибка проверки сессии: " + e.getMessage());
return ApiResponse.error("Session check error: " + e.getMessage());
}
}
@@ -55,7 +76,7 @@ public class AuthService {
"{\"code\":\"" + passCode + "\"}");
return ApiResponse.success(true);
} catch (Exception e) {
return ApiResponse.error("Ошибка активации проходки: " + e.getMessage());
return ApiResponse.error("Pass activation error: " + e.getMessage());
}
}
@@ -18,7 +18,7 @@ public class InstanceService {
.collect(Collectors.toList());
return ApiResponse.success(infoList);
} catch (IOException e) {
return ApiResponse.error("Ошибка получения списка сборок: " + e.getMessage());
return ApiResponse.error("Error getting instances list: " + e.getMessage());
}
}
@@ -26,11 +26,11 @@ public class InstanceService {
try {
Instance instance = InstanceManager.getInstance(name);
if (instance == null) {
return ApiResponse.error("Сборка не найдена: " + name);
return ApiResponse.error("Pack not found: " + name);
}
return ApiResponse.success(toInstanceInfo(instance));
} catch (Exception e) {
return ApiResponse.error("Ошибка получения сборки: " + e.getMessage());
return ApiResponse.error("Error getting pack: " + e.getMessage());
}
}
@@ -38,12 +38,12 @@ public class InstanceService {
try {
boolean created = InstanceManager.createInstanceFolder(name);
if (!created) {
return ApiResponse.error("Сборка с таким именем уже существует: " + name);
return ApiResponse.error("A pack with this name already exists: " + name);
}
Instance instance = InstanceManager.getInstance(name);
return ApiResponse.success(toInstanceInfo(instance));
} catch (IOException e) {
return ApiResponse.error("Ошибка создания сборки: " + e.getMessage());
return ApiResponse.error("Error creating pack: " + e.getMessage());
}
}
@@ -51,11 +51,11 @@ public class InstanceService {
try {
boolean deleted = InstanceManager.deleteInstance(name);
if (!deleted) {
return ApiResponse.error("Не удалось удалить сборку: " + name);
return ApiResponse.error("Failed to delete pack: " + name);
}
return ApiResponse.success(true);
} catch (Exception e) {
return ApiResponse.error("Ошибка удаления сборки: " + e.getMessage());
return ApiResponse.error("Error deleting pack: " + e.getMessage());
}
}
@@ -64,21 +64,24 @@ public class InstanceService {
Instance instance = InstanceManager.getInstance(name);
return ApiResponse.success(instance != null);
} catch (Exception e) {
return ApiResponse.error("Ошибка проверки сборки: " + e.getMessage());
return ApiResponse.error("Error checking pack: " + e.getMessage());
}
}
private InstanceInfo toInstanceInfo(Instance instance) {
// Определяем категорию: ZernMC или локальная
String name = instance.getName().toLowerCase();
String category = name.contains("zernmc") ? "zernmc" : "local";
String category = instance.isServerPack() ? "zernmc" : "local";
return new InstanceInfo(
instance.getName(),
instance.getPath().toString(),
instance.getMinecraftVersion(),
instance.getLoaderType(),
category
category,
instance.isServerPack(),
instance.getServerVersion(),
instance.getLoaderVersion(),
instance.getServerPackName()
);
}
@@ -87,14 +90,23 @@ public class InstanceService {
private String path;
private String version;
private String loaderType;
private String category; // "zernmc" или "local"
private String category;
private boolean isServerPack;
private int serverVersion;
private String loaderVersion;
private String serverPackName;
public InstanceInfo(String name, String path, String version, String loaderType, String category) {
public InstanceInfo(String name, String path, String version, String loaderType, String category,
boolean isServerPack, int serverVersion, String loaderVersion, String serverPackName) {
this.name = name;
this.path = path;
this.version = version;
this.loaderType = loaderType;
this.category = category;
this.isServerPack = isServerPack;
this.serverVersion = serverVersion;
this.loaderVersion = loaderVersion;
this.serverPackName = serverPackName;
}
public String getName() { return name; }
@@ -102,5 +114,9 @@ public class InstanceService {
public String getVersion() { return version; }
public String getLoaderType() { return loaderType; }
public String getCategory() { return category; }
public boolean isServerPack() { return isServerPack; }
public int getServerVersion() { return serverVersion; }
public String getLoaderVersion() { return loaderVersion; }
public String getServerPackName() { return serverPackName; }
}
}
@@ -7,6 +7,7 @@ import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder;
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
import me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher;
import me.sashegdev.zernmc.launcher.utils.Config;
import java.io.BufferedReader;
import java.io.IOException;
@@ -35,11 +36,11 @@ public class LaunchService {
try {
Instance instance = InstanceManager.getInstance(instanceName);
if (instance == null) {
return ApiResponse.error("Сборка не найдена: " + instanceName);
return ApiResponse.error("Pack not found: " + instanceName);
}
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
LaunchOptions options = new LaunchOptions();
LaunchOptions options = createOptions();
List<String> command = builder.build(options);
@@ -50,7 +51,7 @@ public class LaunchService {
);
return ApiResponse.success(info);
} catch (Exception e) {
return ApiResponse.error("Ошибка подготовки запуска: " + e.getMessage());
return ApiResponse.error("Error preparing launch: " + e.getMessage());
}
}
@@ -58,12 +59,11 @@ public class LaunchService {
try {
Instance instance = InstanceManager.getInstance(instanceName);
if (instance == null) {
return ApiResponse.error("Сборка не найдена: " + instanceName);
return ApiResponse.error("Pack not found: " + instanceName);
}
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
LaunchOptions options = new LaunchOptions();
LaunchOptions options = createOptions();
options.setUsername(AuthManager.getUsername());
options.setAccessToken(AuthManager.getAccessToken());
options.setUuid(AuthManager.getUuid());
@@ -100,7 +100,7 @@ public class LaunchService {
} catch (Exception ignored) {}
}
} catch (Exception e) {
JFXLauncher.appendGameLog("[Ошибка чтения логов: " + e.getMessage() + "]");
JFXLauncher.appendGameLog("[Error reading logs: " + e.getMessage() + "]");
} finally {
try { logFileOut.close(); } catch (Exception ignored) {}
}
@@ -110,13 +110,13 @@ public class LaunchService {
process.onExit().thenRun(() -> {
runningProcesses.remove(pid);
JFXLauncher.appendGameLog("[Minecraft завершился с кодом: " + process.exitValue() + "]");
JFXLauncher.appendGameLog("[Minecraft exited with code: " + process.exitValue() + "]");
});
ProcessInfo info = new ProcessInfo(instanceName, pid, "RUNNING");
return ApiResponse.success(info);
} catch (Exception e) {
return ApiResponse.error("Ошибка запуска: " + e.getMessage());
return ApiResponse.error("Launch error: " + e.getMessage());
}
}
@@ -133,7 +133,7 @@ public class LaunchService {
try {
Instance instance = InstanceManager.getInstance(instanceName);
if (instance == null) {
return ApiResponse.error("Сборка не найдена: " + instanceName);
return ApiResponse.error("Pack not found: " + instanceName);
}
Path versionJson = instance.getPath().resolve("version.json");
@@ -141,7 +141,7 @@ public class LaunchService {
return ApiResponse.success(hasVersionJson);
} catch (Exception e) {
return ApiResponse.error("Ошибка проверки готовности: " + e.getMessage());
return ApiResponse.error("Readiness check error: " + e.getMessage());
}
}
@@ -149,7 +149,7 @@ public class LaunchService {
try {
Instance instance = InstanceManager.getInstance(instanceName);
if (instance == null) {
return ApiResponse.error("Сборка не найдена: " + instanceName);
return ApiResponse.error("Pack not found: " + instanceName);
}
InstanceInfo info = new InstanceInfo(
@@ -161,10 +161,28 @@ public class LaunchService {
);
return ApiResponse.success(info);
} catch (Exception e) {
return ApiResponse.error("Ошибка получения информации: " + e.getMessage());
return ApiResponse.error("Info retrieval error: " + e.getMessage());
}
}
private static LaunchOptions createOptions() {
LaunchOptions options = new LaunchOptions();
options.setMaxMemory(Config.getMaxMemory());
options.setWidth(Config.getWindowWidth());
options.setHeight(Config.getWindowHeight());
options.setJavaPath(Config.getJavaPath());
String args = Config.getExtraJvmArgs();
if (args != null && !args.isEmpty()) {
List<String> extraArgs = new ArrayList<>();
for (String arg : args.split("\\s+")) {
arg = arg.trim();
if (!arg.isEmpty()) extraArgs.add(arg);
}
options.setExtraJvmArgs(extraArgs);
}
return options;
}
public static class LaunchInfo {
private String instanceName;
private List<String> command;
@@ -26,14 +26,12 @@ public class AuthManager {
private static volatile AuthSession session = null;
private static volatile UserInfo userInfo = null;
// === Роли ===
public static final int ROLE_USER = 0;
public static final int ROLE_PASS_HOLDER = 1;
public static final int ROLE_MODERATOR = 2;
public static final int ROLE_ELDER = 3;
public static final int ROLE_CREATOR = 4;
// === Права доступа ===
public static final String PERM_VIEW_PACKS = "view_packs";
public static final String PERM_DOWNLOAD_PACK = "download_pack";
@@ -56,7 +54,6 @@ public class AuthManager {
}
}
// ====================== АВТОРИЗАЦИЯ ======================
public static AuthResult login(String username, String password) {
return authRequest("/auth/login", username, password);
}
@@ -77,13 +74,13 @@ public class AuthManager {
userInfo = fetchUserInfo();
return AuthResult.ok();
} else if (resp.statusCode() == 422) {
return AuthResult.fail("Ошибка валидации: " + extractError(resp.body()));
return AuthResult.fail("Validation error: " + extractError(resp.body()));
} else {
return AuthResult.fail(extractError(resp.body()));
}
} catch (Exception e) {
e.printStackTrace();
return AuthResult.fail("Ошибка соединения: " + e.getMessage());
return AuthResult.fail("Connection error: " + e.getMessage());
}
}
@@ -149,16 +146,14 @@ public class AuthManager {
Files.createDirectories(AUTH_FILE.getParent());
Files.writeString(AUTH_FILE, GSON.toJson(session));
} catch (IOException e) {
System.err.println(ZAnsi.yellow("Не удалось сохранить сессию: " + e.getMessage()));
System.err.println(ZAnsi.yellow("Failed to save session: " + e.getMessage()));
}
}
// ==================== ПОЛУЧЕНИЕ ИНФОРМАЦИИ О ПОЛЬЗОВАТЕЛЕ ====================
private static UserInfo fetchUserInfo() {
if (!isLoggedIn() || session.accessToken == null) return null;
try {
// Используем существующий метод ZHttpClient.get() + вручную добавляем токен
java.net.HttpURLConnection conn = null;
try {
URL url = new URL(ZHttpClient.getBaseUrl() + "/admin/me");
@@ -185,12 +180,11 @@ public class AuthManager {
if (conn != null) conn.disconnect();
}
} catch (Exception e) {
System.err.println("Не удалось получить UserInfo: " + e.getMessage());
System.err.println("Failed to get UserInfo: " + e.getMessage());
return null;
}
}
// ==================== ПРОВЕРКИ ПРАВ ====================
public static boolean hasPass() {
if (userInfo != null) return userInfo.has_pass;
return getRole() >= ROLE_PASS_HOLDER;
@@ -200,14 +194,14 @@ public class AuthManager {
if (userInfo != null && userInfo.permissions != null) {
return userInfo.permissions.contains(PERM_VIEW_PACKS);
}
return hasPass(); // fallback для старых аккаунтов
return hasPass();
}
public static boolean canDownloadPacks() {
if (userInfo != null && userInfo.permissions != null) {
return userInfo.permissions.contains(PERM_DOWNLOAD_PACK);
}
return hasPass(); // fallback
return hasPass();
}
public static int getRole() {
@@ -221,7 +215,6 @@ public class AuthManager {
return "USER";
}
// ====================== POST ======================
private static SimpleHttpResponse post(String endpoint, String jsonBody) throws Exception {
String fullUrl = ZHttpClient.getBaseUrl() + endpoint;
HttpURLConnection conn = null;
@@ -291,24 +284,23 @@ public class AuthManager {
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
return json.has("has_active") && json.get("has_active").getAsBoolean();
} catch (Exception e) {
System.err.println(ZAnsi.red("Не удалось проверить проходки: ") + e.getMessage());
System.err.println(ZAnsi.red("Failed to check pass: ") + e.getMessage());
return false;
}
}
public static String getPassStatus() {
if (!isLoggedIn()) return "Не авторизован";
if (!isLoggedIn()) return "Not logged in";
try {
String response = ZHttpClient.get("/auth/pass/my");
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
boolean hasActive = json.has("has_active") && json.get("has_active").getAsBoolean();
return hasActive ? "Есть активная проходка" : "Проходка отсутствует";
return hasActive ? "Active pass" : "No pass";
} catch (Exception e) {
return "Ошибка проверки";
return "Check error";
}
}
// ====================== ВНУТРЕННИЕ КЛАССЫ ======================
public static class AuthSession {
@SerializedName("access_token") public String accessToken;
@SerializedName("refresh_token") public String refreshToken;
@@ -351,7 +343,6 @@ public class AuthManager {
}
}
// ====================== ВСПОМОГАТЕЛЬНЫЙ КЛАСС ======================
class SimpleHttpResponse {
final int statusCode;
final String body;
@@ -363,4 +354,4 @@ class SimpleHttpResponse {
int statusCode() { return statusCode; }
String body() { return body; }
}
}
@@ -33,12 +33,11 @@ public class LaunchMenu {
}
}
// ====================== ZERNMC BUILD ======================
private void showZernMCOnly() throws Exception {
while (true) {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.header("=== ZernMC Private Launcher ==="));
System.out.println(ZAnsi.cyan("Доступны только серверные сборки"));
System.out.println(ZAnsi.cyan("Server packs only"));
if (!awaitActivePass()) {
return;
@@ -48,13 +47,13 @@ public class LaunchMenu {
List<ServerPack> availablePacks = tempDownloader.getAvailablePacks();
if (availablePacks.isEmpty()) {
System.out.println(ZAnsi.yellow("На данный момент нет доступных сборок на сервере."));
System.out.println(ZAnsi.yellow("No packs available on the server."));
ConsoleUtils.pause();
return;
}
List<String> options = availablePacks.stream()
.map(p -> String.format("%s [%s + %s v%d] %d файлов",
.map(p -> String.format("%s [%s + %s v%d] - %d files",
p.getName(),
p.getMinecraftVersion(),
p.getLoaderType(),
@@ -62,9 +61,9 @@ public class LaunchMenu {
p.getFilesCount()))
.collect(Collectors.toList());
options.add("Назад в главное меню");
options.add("Back to main menu");
ArrowMenu menu = new ArrowMenu("Выберите сборку", options);
ArrowMenu menu = new ArrowMenu("Select a pack", options);
int choice = menu.show();
if (choice == -1 || choice == options.size() - 1) return;
@@ -76,25 +75,25 @@ public class LaunchMenu {
private boolean awaitActivePass() throws Exception {
if (AuthManager.hasActivePass()) {
System.out.println(ZAnsi.brightGreen("Активная проходка подтверждена"));
System.out.println(ZAnsi.brightGreen("Active pass confirmed"));
return true;
}
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.brightRed("У вас нет активной проходки!"));
System.out.println(ZAnsi.white("Для доступа к сборкам ZernMC требуется активная проходка."));
System.out.println(ZAnsi.brightRed("You don't have an active pass!"));
System.out.println(ZAnsi.white("Access to ZernMC packs requires an active pass."));
System.out.println();
openActivationWebsite();
System.out.println(ZAnsi.cyan("Ожидаем активацию проходки... (проверка каждые 10 секунд)"));
System.out.println(ZAnsi.white("Нажмите Enter для отмены"));
System.out.println(ZAnsi.cyan("Waiting for pass activation... (checking every 10 seconds)"));
System.out.println(ZAnsi.white("Press Enter to cancel"));
for (int i = 0; i < 60; i++) {
try {
if (System.in.available() > 0) {
Input.readLine();
System.out.println(ZAnsi.yellow("\nОжидание отменено."));
System.out.println(ZAnsi.yellow("\nWaiting cancelled."));
return false;
}
} catch (Exception ignored) {}
@@ -102,7 +101,7 @@ public class LaunchMenu {
Thread.sleep(10000);
if (AuthManager.hasActivePass()) {
System.out.println(ZAnsi.brightGreen("\n✓ Проходка успешно активирована!"));
System.out.println(ZAnsi.brightGreen("\n✓ Pass activated successfully!"));
return true;
}
@@ -110,43 +109,42 @@ public class LaunchMenu {
if ((i + 1) % 6 == 0) System.out.println();
}
System.out.println(ZAnsi.brightRed("\n\nВремя ожидания истекло."));
System.out.println(ZAnsi.brightRed("\n\nWaiting time expired."));
return false;
}
private void openActivationWebsite() {
//String url = "https://launcher.ru.zernmc.ru/activate-pass";
String url = ZHttpClient.getBaseUrl() + "/activate-pass";
try {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
Desktop.getDesktop().browse(new URI(url));
System.out.println(ZAnsi.cyan("Браузер открыт: " + url));
System.out.println(ZAnsi.cyan("Browser opened: " + url));
} else {
System.out.println(ZAnsi.yellow("Не удалось открыть браузер автоматически."));
System.out.println(ZAnsi.white("Откройте вручную: " + url));
System.out.println(ZAnsi.yellow("Could not open browser automatically."));
System.out.println(ZAnsi.white("Open manually: " + url));
}
} catch (Exception e) {
System.out.println(ZAnsi.brightRed("Ошибка открытия браузера: " + e.getMessage()));
System.out.println(ZAnsi.white("Ссылка: " + url));
System.out.println(ZAnsi.brightRed("Error opening browser: " + e.getMessage()));
System.out.println(ZAnsi.white("Link: " + url));
}
}
private void installAndRunServerPack(ServerPack selected) throws Exception {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.header("Установка сборки: " + selected.getName()));
System.out.println(ZAnsi.header("Installing pack: " + selected.getName()));
System.out.println(ZAnsi.white(" Minecraft: ") + selected.getMinecraftVersion());
System.out.println(ZAnsi.white(" Лоадер: ") + selected.getLoaderType() +
System.out.println(ZAnsi.white(" Loader: ") + selected.getLoaderType() +
(selected.getLoaderVersion() != null ? " " + selected.getLoaderVersion() : ""));
System.out.println(ZAnsi.white(" Версия: v") + selected.getVersion());
System.out.println(ZAnsi.white(" Файлов: ") + selected.getFilesCount());
System.out.println(ZAnsi.white(" Version: v") + selected.getVersion());
System.out.println(ZAnsi.white(" Files: ") + selected.getFilesCount());
String localName = askPackName();
if (localName == null) return;
if (InstanceManager.getInstance(localName) != null) {
System.out.println(ZAnsi.brightRed("Сборка с таким именем уже существует!"));
System.out.println(ZAnsi.brightRed("A pack with this name already exists!"));
ConsoleUtils.pause();
return;
}
@@ -158,18 +156,17 @@ public class LaunchMenu {
boolean success = packDownloader.installOrUpdatePack(selected.getName(), selected);
if (!success) {
System.out.println(ZAnsi.brightRed("\n[FAIL] Не удалось установить сборку."));
System.out.println(ZAnsi.brightRed("\n[FAIL] Could not install the pack."));
ConsoleUtils.pause();
return;
}
System.out.println(ZAnsi.brightGreen("\n[OK] Сборка '" + localName + "' успешно установлена!"));
System.out.println(ZAnsi.brightGreen("\n[OK] Pack '" + localName + "' installed successfully!"));
ConsoleUtils.pause();
launchExistingInstance(newInstance);
}
// ====================== GLOBAL BUILD ======================
private void showGlobal() throws Exception {
while (true) {
ConsoleUtils.clearScreen();
@@ -179,10 +176,10 @@ public class LaunchMenu {
.map(Instance::toString)
.collect(Collectors.toList());
options.add("Установить новую сборку");
options.add("Назад в главное меню");
options.add("Install new pack");
options.add("Back to main menu");
ArrowMenu menu = new ArrowMenu("Управление сборками", options);
ArrowMenu menu = new ArrowMenu("Manage packs", options);
int choice = menu.show();
if (choice == -1 || choice == options.size() - 1) break;
@@ -201,13 +198,13 @@ public class LaunchMenu {
ConsoleUtils.clearScreen();
List<String> options = List.of(
"Установить сборку с сервера ZernMC",
"Установить Vanilla Minecraft",
"Создать сборку вручную (Fabric/Forge)",
"Назад"
"Install pack from ZernMC server",
"Install Vanilla Minecraft",
"Create custom pack (Fabric/Forge)",
"Back"
);
ArrowMenu menu = new ArrowMenu("Установка новой сборки", options);
ArrowMenu menu = new ArrowMenu("Install new pack", options);
int choice = menu.show();
if (choice == -1 || choice == 3) return;
@@ -223,28 +220,28 @@ public class LaunchMenu {
if (!awaitActivePass()) return;
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.cyan("Получение списка доступных сборок..."));
System.out.println(ZAnsi.cyan("Fetching available packs..."));
PackDownloader tempDownloader = new PackDownloader(null);
List<ServerPack> availablePacks = tempDownloader.getAvailablePacks();
if (availablePacks.isEmpty()) {
System.out.println(ZAnsi.yellow("Нет доступных сборок на сервере."));
System.out.println(ZAnsi.yellow("No packs available on the server."));
ConsoleUtils.pause();
return;
}
List<String> options = availablePacks.stream()
.map(p -> String.format("%s [%s + %s v%d] %d файлов",
.map(p -> String.format("%s [%s + %s v%d] - %d files",
p.getName(),
p.getMinecraftVersion(),
p.getLoaderType(),
p.getVersion(),
p.getFilesCount()))
.collect(Collectors.toList());
options.add("Назад");
options.add("Back");
ArrowMenu menu = new ArrowMenu("Выберите сборку для установки", options);
ArrowMenu menu = new ArrowMenu("Select a pack to install", options);
int choice = menu.show();
if (choice == -1 || choice == options.size() - 1) return;
@@ -252,14 +249,14 @@ public class LaunchMenu {
ServerPack selected = availablePacks.get(choice);
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.header("Установка сборки: " + selected.getName()));
System.out.println(ZAnsi.header("Installing pack: " + selected.getName()));
System.out.print(ZAnsi.white("\nВведите название локальной сборки (Enter = имя пака): "));
System.out.print(ZAnsi.white("\nEnter local pack name (Enter = pack name): "));
String localName = Input.readLine().trim();
if (localName.isEmpty()) localName = selected.getName();
if (InstanceManager.getInstance(localName) != null) {
System.out.println(ZAnsi.brightRed("Сборка с таким именем уже существует!"));
System.out.println(ZAnsi.brightRed("A pack with this name already exists!"));
ConsoleUtils.pause();
return;
}
@@ -271,37 +268,36 @@ public class LaunchMenu {
boolean success = packDownloader.installOrUpdatePack(selected.getName(), selected);
if (success) {
System.out.println(ZAnsi.brightGreen("\n[OK] Сборка '" + localName + "' успешно установлена!"));
System.out.println(ZAnsi.brightGreen("\n[OK] Pack '" + localName + "' installed successfully!"));
} else {
System.out.println(ZAnsi.brightRed("\n[FAIL] Не удалось установить сборку."));
System.out.println(ZAnsi.brightRed("\n[FAIL] Could not install the pack."));
}
ConsoleUtils.pause();
}
// ====================== manageInstance полностью восстановлен ======================
private void manageInstance(Instance instance) throws Exception {
while (true) {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.header("Управление сборкой: " + instance.getName()));
System.out.println(ZAnsi.white("Версия: " + instance.getMinecraftVersion()));
System.out.println(ZAnsi.white("Лоадер: " + instance.getLoaderType() +
System.out.println(ZAnsi.header("Managing pack: " + instance.getName()));
System.out.println(ZAnsi.white("Version: " + instance.getMinecraftVersion()));
System.out.println(ZAnsi.white("Loader: " + instance.getLoaderType() +
(instance.getLoaderVersion() != null ? " " + instance.getLoaderVersion() : "")));
if (instance.isServerPack()) {
System.out.println(ZAnsi.green("Серверная сборка: v" + instance.getServerVersion()));
System.out.println(ZAnsi.green("Server pack: v" + instance.getServerVersion()));
}
List<String> options = new ArrayList<>();
options.add("Запустить сборку");
options.add("Launch pack");
if (instance.isServerPack()) {
options.add("Проверить обновления");
options.add("Check for updates");
}
options.add("Изменить версию лоадера");
options.add("Удалить сборку");
options.add("Назад");
options.add("Change loader version");
options.add("Delete pack");
options.add("Back");
ArrowMenu menu = new ArrowMenu("Действия", options);
ArrowMenu menu = new ArrowMenu("Actions", options);
int choice = menu.show();
if (choice == -1 || choice == options.size() - 1) return;
@@ -329,40 +325,40 @@ public class LaunchMenu {
private void checkAndUpdateServerPack(Instance instance) throws Exception {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.cyan("Проверка обновлений для " + instance.getName()));
System.out.println(ZAnsi.cyan("Checking updates for " + instance.getName()));
PackDownloader downloader = new PackDownloader(instance);
boolean hasUpdate = downloader.checkForUpdates(instance.getServerPackName());
if (!hasUpdate) {
System.out.println(ZAnsi.green("Сборка актуальна (v" + instance.getServerVersion() + ")"));
System.out.println(ZAnsi.green("Pack is up to date (v" + instance.getServerVersion() + ")"));
ConsoleUtils.pause();
return;
}
System.out.println(ZAnsi.brightYellow("Доступно обновление!"));
if (Input.confirm("Обновить сборку")) {
System.out.println(ZAnsi.brightYellow("Update available!"));
if (Input.confirm("Update pack")) {
boolean success = downloader.updatePack(instance.getServerPackName());
if (success) {
System.out.println(ZAnsi.brightGreen("Сборка успешно обновлена!"));
System.out.println(ZAnsi.brightGreen("Pack updated successfully!"));
} else {
System.out.println(ZAnsi.brightRed("Не удалось обновить сборку."));
System.out.println(ZAnsi.brightRed("Failed to update pack."));
}
} else {
System.out.println(ZAnsi.yellow("Обновление отменено."));
System.out.println(ZAnsi.yellow("Update cancelled."));
}
ConsoleUtils.pause();
}
private void changeLoaderVersion(Instance instance) throws Exception {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.cyan("Изменение версии лоадера для " + instance.getName()));
System.out.println(ZAnsi.cyan("Changing loader version for " + instance.getName()));
String currentLoader = instance.getLoaderType();
String mcVersion = instance.getMinecraftVersion();
if ("vanilla".equalsIgnoreCase(currentLoader)) {
System.out.println(ZAnsi.yellow("Это vanilla сборка. Нельзя изменить лоадер."));
System.out.println(ZAnsi.yellow("This is a vanilla instance. Cannot change loader."));
ConsoleUtils.pause();
return;
}
@@ -378,7 +374,7 @@ public class LaunchMenu {
if (newLoaderVersion == null) return;
System.out.println(ZAnsi.cyan("Переустановка лоадера " + currentLoader + " -> " + newLoaderVersion + "..."));
System.out.println(ZAnsi.cyan("Reinstalling loader " + currentLoader + " -> " + newLoaderVersion + "..."));
MinecraftLib lib = new MinecraftLib(instance);
boolean success;
@@ -393,12 +389,12 @@ public class LaunchMenu {
}
if (success) {
System.out.println(ZAnsi.brightGreen("Версия лоадера успешно изменена!"));
System.out.println(ZAnsi.brightGreen("Loader version changed successfully!"));
} else {
System.out.println(ZAnsi.brightRed("Не удалось изменить версию лоадера."));
System.out.println(ZAnsi.brightRed("Failed to change loader version."));
}
} catch (Exception e) {
System.out.println(ZAnsi.brightRed("Ошибка при смене лоадера: " + e.getMessage()));
System.out.println(ZAnsi.brightRed("Error changing loader: " + e.getMessage()));
}
ConsoleUtils.pause();
@@ -408,12 +404,12 @@ public class LaunchMenu {
ConsoleUtils.clearScreen();
List<String> confirmOptions = List.of(
"Да, удалить сборку",
"Нет, отменить"
"Yes, delete pack",
"No, cancel"
);
ArrowMenu confirmMenu = new ArrowMenu(
"Вы действительно хотите удалить сборку '" + instance.getName() + "'?",
"Are you sure you want to delete '" + instance.getName() + "'?",
confirmOptions
);
@@ -422,12 +418,12 @@ public class LaunchMenu {
if (choice == 0) {
boolean deleted = InstanceManager.deleteInstance(instance.getName());
if (deleted) {
System.out.println(ZAnsi.brightGreen("Сборка '" + instance.getName() + "' успешно удалена."));
System.out.println(ZAnsi.brightGreen("Pack '" + instance.getName() + "' deleted successfully."));
} else {
System.out.println(ZAnsi.brightRed("Не удалось удалить сборку."));
System.out.println(ZAnsi.brightRed("Failed to delete pack."));
}
} else {
System.out.println(ZAnsi.yellow("Удаление отменено."));
System.out.println(ZAnsi.yellow("Deletion cancelled."));
}
ConsoleUtils.pause();
@@ -436,16 +432,20 @@ public class LaunchMenu {
private void launchExistingInstance(Instance instance) {
if (instance.isServerPack() && !AuthManager.hasActivePass()) {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.brightRed("Для запуска серверной сборки требуется активная проходка!"));
System.out.println(ZAnsi.brightRed("Launching a server pack requires an active pass!"));
ConsoleUtils.pause();
return;
}
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName()));
System.out.println(ZAnsi.brightGreen("Launching pack: " + instance.getName()));
MinecraftLib lib = new MinecraftLib(instance);
LaunchOptions options = new LaunchOptions();
options.setMaxMemory(Config.getMaxMemory());
options.setWidth(Config.getWindowWidth());
options.setHeight(Config.getWindowHeight());
options.setJavaPath(Config.getJavaPath());
options.setUsername(AuthManager.getUsername());
options.setUuid(AuthManager.getUuid());
@@ -454,20 +454,18 @@ public class LaunchMenu {
try {
lib.launch(options);
} catch (Exception e) {
System.out.println(ZAnsi.brightRed("Ошибка при запуске: " + e.getMessage()));
System.out.println(ZAnsi.brightRed("Error launching: " + e.getMessage()));
e.printStackTrace();
}
ConsoleUtils.pause();
}
// ====================== Остальные вспомогательные методы ======================
private String askPackName() {
System.out.print(ZAnsi.white("\nВведите название новой сборки: "));
System.out.print(ZAnsi.white("\nEnter new pack name: "));
String name = Input.readLine().trim();
if (name.isEmpty()) {
System.out.println(ZAnsi.yellow("Отменено."));
System.out.println(ZAnsi.yellow("Cancelled."));
return null;
}
return name;
@@ -475,7 +473,7 @@ public class LaunchMenu {
private void createVanillaInstance() throws Exception {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.cyan("Получение списка версий Minecraft..."));
System.out.println(ZAnsi.cyan("Fetching Minecraft versions..."));
VersionInstaller versionInstaller = new VersionInstaller(null);
List<MinecraftVersion> allVersions = versionInstaller.getAvailableVersions();
@@ -483,9 +481,9 @@ public class LaunchMenu {
List<String> versionOptions = allVersions.stream()
.map(v -> v.getId() + " (" + v.getType() + ")")
.collect(Collectors.toList());
versionOptions.add("Назад");
versionOptions.add("Back");
ArrowMenu versionMenu = new ArrowMenu("Выбор версии Minecraft", versionOptions);
ArrowMenu versionMenu = new ArrowMenu("Select Minecraft version", versionOptions);
int versionChoice = versionMenu.show();
if (versionChoice == -1 || versionChoice == versionOptions.size() - 1) return;
@@ -497,7 +495,7 @@ public class LaunchMenu {
if (packName == null) return;
if (InstanceManager.getInstance(packName) != null) {
System.out.println(ZAnsi.brightRed("Сборка с таким именем уже существует!"));
System.out.println(ZAnsi.brightRed("A pack with this name already exists!"));
ConsoleUtils.pause();
return;
}
@@ -509,9 +507,9 @@ public class LaunchMenu {
boolean success = lib.installMinecraft(mcVersion);
if (success) {
System.out.println(ZAnsi.brightGreen("\n[OK] Vanilla сборка '" + packName + "' успешно создана!"));
System.out.println(ZAnsi.brightGreen("\n[OK] Vanilla pack '" + packName + "' created successfully!"));
} else {
System.out.println(ZAnsi.brightRed("\n[FAIL] Не удалось создать сборку."));
System.out.println(ZAnsi.brightRed("\n[FAIL] Failed to create pack."));
}
ConsoleUtils.pause();
@@ -519,7 +517,7 @@ public class LaunchMenu {
private void createCustomInstance() throws Exception {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.cyan("Получение списка версий Minecraft..."));
System.out.println(ZAnsi.cyan("Fetching Minecraft versions..."));
VersionInstaller versionInstaller = new VersionInstaller(null);
List<MinecraftVersion> allVersions = versionInstaller.getAvailableVersions();
@@ -527,9 +525,9 @@ public class LaunchMenu {
List<String> versionOptions = allVersions.stream()
.map(v -> v.getId() + " (" + v.getType() + ")")
.collect(Collectors.toList());
versionOptions.add("Назад");
versionOptions.add("Back");
ArrowMenu versionMenu = new ArrowMenu("Выбор версии Minecraft", versionOptions);
ArrowMenu versionMenu = new ArrowMenu("Select Minecraft version", versionOptions);
int versionChoice = versionMenu.show();
if (versionChoice == -1 || versionChoice == versionOptions.size() - 1) return;
@@ -538,7 +536,7 @@ public class LaunchMenu {
String mcVersion = selectedMc.getId();
List<String> loaderOptions = buildLoaderOptions(mcVersion);
ArrowMenu loaderMenu = new ArrowMenu("Выбор модлоадера для " + mcVersion, loaderOptions);
ArrowMenu loaderMenu = new ArrowMenu("Select mod loader for " + mcVersion, loaderOptions);
int loaderChoice = loaderMenu.show();
if (loaderChoice == -1 || loaderChoice == loaderOptions.size() - 1) return;
@@ -574,7 +572,7 @@ public class LaunchMenu {
if (packName == null) return;
if (InstanceManager.getInstance(packName) != null) {
System.out.println(ZAnsi.brightRed("Сборка с таким именем уже существует!"));
System.out.println(ZAnsi.brightRed("A pack with this name already exists!"));
ConsoleUtils.pause();
return;
}
@@ -594,9 +592,9 @@ public class LaunchMenu {
}
if (success) {
System.out.println(ZAnsi.brightGreen("\n[OK] Сборка '" + packName + "' успешно установлена!"));
System.out.println(ZAnsi.brightGreen("\n[OK] Pack '" + packName + "' installed successfully!"));
} else {
System.out.println(ZAnsi.brightRed("\n[FAIL] Не удалось установить сборку."));
System.out.println(ZAnsi.brightRed("\n[FAIL] Failed to install pack."));
}
ConsoleUtils.pause();
@@ -609,7 +607,7 @@ public class LaunchMenu {
if (isNeoForgeSupported(mcVersion)) options.add("NeoForge");
if (isForgeSupported(mcVersion)) options.add("Forge");
options.add("Vanilla");
options.add("Назад");
options.add("Back");
return options;
}
@@ -631,16 +629,16 @@ public class LaunchMenu {
}
private String askFabricLoaderVersion() throws Exception {
System.out.println(ZAnsi.cyan("Получение списка версий Fabric Loader..."));
System.out.println(ZAnsi.cyan("Fetching Fabric Loader versions..."));
List<String> versions = ZHttpClient.getFabricLoaderVersions();
List<String> options = versions.stream()
.limit(30)
.map(v -> "Fabric Loader " + v)
.collect(Collectors.toList());
options.add("Назад");
options.add("Back");
ArrowMenu menu = new ArrowMenu("Выбор версии Fabric Loader", options);
ArrowMenu menu = new ArrowMenu("Select Fabric Loader version", options);
int choice = menu.show();
if (choice == -1 || choice == options.size() - 1) return null;
@@ -648,7 +646,7 @@ public class LaunchMenu {
}
private String askForgeVersion(String mcVersion) throws Exception {
System.out.println(ZAnsi.cyan("Получение списка версий Forge для " + mcVersion + "..."));
System.out.println(ZAnsi.cyan("Fetching Forge versions for " + mcVersion + "..."));
List<String> allForgeVersions = getAllForgeVersions();
@@ -658,7 +656,7 @@ public class LaunchMenu {
.collect(Collectors.toList());
if (compatibleVersions.isEmpty()) {
System.out.println(ZAnsi.yellow("Не найдено совместимых версий Forge для " + mcVersion));
System.out.println(ZAnsi.yellow("No compatible Forge versions found for " + mcVersion));
ConsoleUtils.pause();
return null;
}
@@ -667,9 +665,9 @@ public class LaunchMenu {
.limit(30)
.map(v -> "Forge " + v)
.collect(Collectors.toList());
options.add("Назад");
options.add("Back");
ArrowMenu menu = new ArrowMenu("Выбор версии Forge для " + mcVersion, options);
ArrowMenu menu = new ArrowMenu("Select Forge version for " + mcVersion, options);
int choice = menu.show();
if (choice == -1 || choice == options.size() - 1) return null;
@@ -698,7 +696,7 @@ public class LaunchMenu {
}
private String askNeoForgeVersion(String mcVersion) throws Exception {
System.out.println(ZAnsi.cyan("Получение списка версий NeoForge для " + mcVersion + "..."));
System.out.println(ZAnsi.cyan("Fetching NeoForge versions for " + mcVersion + "..."));
List<String> allNeoForgeVersions = getAllNeoForgeVersions();
@@ -707,7 +705,7 @@ public class LaunchMenu {
.collect(Collectors.toList());
if (compatibleVersions.isEmpty()) {
System.out.println(ZAnsi.yellow("Не найдено совместимых версий NeoForge для " + mcVersion));
System.out.println(ZAnsi.yellow("No compatible NeoForge versions found for " + mcVersion));
ConsoleUtils.pause();
return null;
}
@@ -716,9 +714,9 @@ public class LaunchMenu {
.limit(30)
.map(v -> "NeoForge " + v)
.collect(Collectors.toList());
options.add("Назад");
options.add("Back");
ArrowMenu menu = new ArrowMenu("Выбор версии NeoForge для " + mcVersion, options);
ArrowMenu menu = new ArrowMenu("Select NeoForge version for " + mcVersion, options);
int choice = menu.show();
if (choice == -1 || choice == options.size() - 1) return null;
@@ -760,11 +758,10 @@ public class LaunchMenu {
index = end;
}
} catch (Exception e) {
// Skip if one maven doesn't have the artifact
}
}
versions.sort((a, b) -> b.compareTo(a));
return versions;
}
}
}
@@ -10,30 +10,20 @@ import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import java.io.IOException;
import java.util.List;
/**
* Экран входа/регистрации.
* Показывается при старте лаунчера, если нет сохранённой сессии.
*
* show() возвращает true пользователь вошёл/зарегистрировался
* false пользователь выбрал выход из лаунчера
*/
public class LoginMenu {
/**
* Главный экран выбора действия.
*/
public boolean show() throws IOException {
while (true) {
ConsoleUtils.clearScreen();
printBanner();
List<String> options = List.of(
"Войти в аккаунт",
"Создать аккаунт",
"Выйти из лаунчера"
"Sign In",
"Create Account",
"Exit Launcher"
);
ArrowMenu menu = new ArrowMenu("Добро пожаловать в ZernMC!", options);
ArrowMenu menu = new ArrowMenu("Welcome to ZernMC!", options);
int choice = menu.show();
if (choice == -1 || choice == 2) return false;
@@ -45,62 +35,56 @@ public class LoginMenu {
};
if (success) return true;
// Если не успех покажем меню снова (ошибка уже напечатана внутри методов)
}
}
/**
* Показывается когда пользователь уже вошёл предлагает выйти из аккаунта.
*/
public void showAccountMenu() throws IOException {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.header("=== Аккаунт ==="));
System.out.println(ZAnsi.header("=== Account ==="));
System.out.println();
System.out.println(ZAnsi.white(" Игрок: ") + ZAnsi.brightGreen(AuthManager.getUsername()));
System.out.println(ZAnsi.white(" Player: ") + ZAnsi.brightGreen(AuthManager.getUsername()));
System.out.println(ZAnsi.white(" UUID: ") + ZAnsi.cyan(AuthManager.getUuid()));
System.out.println();
List<String> options = List.of(
"Выйти из аккаунта",
"Назад"
"Log Out",
"Back"
);
ArrowMenu menu = new ArrowMenu("Управление аккаунтом", options);
ArrowMenu menu = new ArrowMenu("Account Management", options);
int choice = menu.show();
if (choice == 0) {
AuthManager.logout();
System.out.println(ZAnsi.yellow("Вы вышли из аккаунта."));
System.out.println(ZAnsi.yellow("Logged out."));
ConsoleUtils.pause();
}
}
// ====================== ПРИВАТНЫЕ МЕТОДЫ ======================
private boolean doLogin() throws IOException {
ConsoleUtils.clearScreen();
printBanner();
System.out.println(ZAnsi.cyan(" [ Вход в аккаунт ]"));
System.out.println(ZAnsi.cyan(" [ Sign In ]"));
System.out.println();
String username = Input.readLine(ZAnsi.white(" Имя пользователя: "));
String username = Input.readLine(ZAnsi.white(" Username: "));
if (username.isEmpty()) return false;
String password = readPassword(" Пароль: ");
String password = readPassword(" Password: ");
if (password.isEmpty()) return false;
System.out.println();
System.out.print(ZAnsi.cyan(" Выполняем вход..."));
System.out.print(ZAnsi.cyan(" Signing in..."));
AuthResult result = AuthManager.login(username, password);
if (result.success) {
System.out.println("\r" + ZAnsi.brightGreen(" Добро пожаловать, " + AuthManager.getUsername() + "! "));
System.out.println("\r" + ZAnsi.brightGreen(" Welcome, " + AuthManager.getUsername() + "! "));
ConsoleUtils.pause();
return true;
} else {
System.out.println("\r" + ZAnsi.brightRed(" Ошибка: " + result.error + " "));
System.out.println("\r" + ZAnsi.brightRed(" Error: " + result.error + " "));
ConsoleUtils.pause();
return false;
}
@@ -109,45 +93,41 @@ public class LoginMenu {
private boolean doRegister() throws IOException {
ConsoleUtils.clearScreen();
printBanner();
System.out.println(ZAnsi.cyan(" [ Создание аккаунта ]"));
System.out.println(ZAnsi.cyan(" [ Create Account ]"));
System.out.println();
System.out.println(ZAnsi.yellow(" Допустимые символы в имени: a-z, A-Z, 0-9, _"));
System.out.println(ZAnsi.yellow(" Длина имени: 3-16 символов | Длина пароля: от 6 символов"));
System.out.println(ZAnsi.yellow(" Allowed characters: a-z, A-Z, 0-9, _"));
System.out.println(ZAnsi.yellow(" Name length: 3-16 chars | Password length: 6+ chars"));
System.out.println();
String username = Input.readLine(ZAnsi.white(" Имя пользователя: "));
String username = Input.readLine(ZAnsi.white(" Username: "));
if (username.isEmpty()) return false;
String password = readPassword(" Пароль: ");
String password = readPassword(" Password: ");
if (password.isEmpty()) return false;
String confirm = readPassword(" Повторите пароль: ");
String confirm = readPassword(" Confirm password: ");
if (!password.equals(confirm)) {
System.out.println(ZAnsi.brightRed("\n Пароли не совпадают!"));
System.out.println(ZAnsi.brightRed("\n Passwords do not match!"));
ConsoleUtils.pause();
return false;
}
System.out.println();
System.out.print(ZAnsi.cyan(" Создаём аккаунт..."));
System.out.print(ZAnsi.cyan(" Creating account..."));
AuthResult result = AuthManager.register(username, password);
if (result.success) {
System.out.println("\r" + ZAnsi.brightGreen(" Аккаунт создан! Добро пожаловать, " + AuthManager.getUsername() + "! "));
System.out.println("\r" + ZAnsi.brightGreen(" Account created! Welcome, " + AuthManager.getUsername() + "! "));
ConsoleUtils.pause();
return true;
} else {
System.out.println("\r" + ZAnsi.brightRed(" Ошибка: " + result.error + " "));
System.out.println("\r" + ZAnsi.brightRed(" Error: " + result.error + " "));
ConsoleUtils.pause();
return false;
}
}
/**
* Читаем пароль стараемся скрыть вывод через Console,
* если недоступно (IDE/терминал без TTY) читаем обычным способом.
*/
private String readPassword(String prompt) throws IOException {
org.jline.terminal.Terminal passTerminal = org.jline.terminal.TerminalBuilder.builder()
.system(true)
@@ -165,27 +145,26 @@ public class LoginMenu {
int key = passTerminal.reader().read();
if (key == 27) {
// Escape sequence consume remaining bytes (arrow keys, etc.)
int next = passTerminal.reader().read();
if (next == 91) { // '[' arrow key sequence
passTerminal.reader().read(); // consume 'A'/'B'/'C'/'D'
if (next == 91) {
passTerminal.reader().read();
}
continue;
}
if (key == 13 || key == 10) { // Enter
if (key == 13 || key == 10) {
passTerminal.writer().println();
break;
} else if (key == 127 || key == 8) { // Backspace
} else if (key == 127 || key == 8) {
if (password.length() > 0) {
password.setLength(password.length() - 1);
passTerminal.writer().print("\b \b");
passTerminal.writer().flush();
}
} else if (key == 3) { // Ctrl+C
} else if (key == 3) {
passTerminal.writer().println();
System.exit(0);
} else if (key >= 32 && key < 127) { // Printable characters
} else if (key >= 32 && key < 127) {
password.append((char) key);
passTerminal.writer().print('*');
passTerminal.writer().flush();
@@ -18,17 +18,17 @@ public class ServerCheckMenu {
public void show() throws IOException {
while (true) {
ConsoleUtils.clearScreen();
System.out.println(ZAnsi.header("Диагностика подключения"));
System.out.println(ZAnsi.header("Connection Diagnostics"));
List<String> options = List.of(
"Проверить подключение к ZernMC серверу",
"Проверить доступ к Mojang (Minecraft)",
"Проверить доступ к Fabric Meta",
"Проверить доступ к Forge Maven",
"Назад в главное меню"
"Check ZernMC server connection",
"Check Mojang (Minecraft) access",
"Check Fabric Meta access",
"Check Forge Maven access",
"Back to main menu"
);
ArrowMenu menu = new ArrowMenu("Выберите проверку", options);
ArrowMenu menu = new ArrowMenu("Select check", options);
int choice = menu.show();
if (choice == -1 || choice == 4) {
@@ -49,20 +49,20 @@ public class ServerCheckMenu {
}
private void checkZernServer() {
System.out.println(ZAnsi.cyan("Проверка подключения к ZernMC серверу..."));
System.out.println(ZAnsi.cyan("Checking connection to ZernMC server..."));
try {
String response = ZHttpClient.get("/health");
System.out.println(ZAnsi.brightGreen("[OK] ZernMC сервер успешно подключён!"));
System.out.println(ZAnsi.white("Ответ сервера: ") + response);
System.out.println(ZAnsi.brightGreen("[OK] ZernMC server connected successfully!"));
System.out.println(ZAnsi.white("Server response: ") + response);
} catch (Exception e) {
System.out.println(ZAnsi.brightRed("[FAIL] Не удалось подключиться к ZernMC серверу"));
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
System.out.println(ZAnsi.brightRed("[FAIL] Could not connect to ZernMC server"));
System.out.println(ZAnsi.white("Error: ") + e.getMessage());
}
}
private void checkMojang() {
System.out.println(ZAnsi.cyan("Проверка доступа к Mojang..."));
System.out.println(ZAnsi.cyan("Checking Mojang access..."));
try {
HttpClient client = HttpClient.newBuilder()
@@ -77,18 +77,18 @@ public class ServerCheckMenu {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
System.out.println(ZAnsi.brightGreen("[OK] Mojang доступен"));
System.out.println(ZAnsi.brightGreen("[OK] Mojang is accessible"));
} else {
System.out.println(ZAnsi.brightRed("[FAIL] Mojang вернул код " + response.statusCode()));
System.out.println(ZAnsi.brightRed("[FAIL] Mojang returned code " + response.statusCode()));
}
} catch (Exception e) {
System.out.println(ZAnsi.brightRed("[FAIL] Нет доступа к Mojang"));
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
System.out.println(ZAnsi.brightRed("[FAIL] Cannot access Mojang"));
System.out.println(ZAnsi.white("Error: ") + e.getMessage());
}
}
private void checkFabric() {
System.out.println(ZAnsi.cyan("Проверка доступа к Fabric Meta..."));
System.out.println(ZAnsi.cyan("Checking Fabric Meta access..."));
try {
HttpClient client = HttpClient.newBuilder()
@@ -103,18 +103,18 @@ public class ServerCheckMenu {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
System.out.println(ZAnsi.brightGreen("[OK] Fabric Meta доступен"));
System.out.println(ZAnsi.brightGreen("[OK] Fabric Meta is accessible"));
} else {
System.out.println(ZAnsi.brightRed("[FAIL] Fabric Meta вернул код " + response.statusCode()));
System.out.println(ZAnsi.brightRed("[FAIL] Fabric Meta returned code " + response.statusCode()));
}
} catch (Exception e) {
System.out.println(ZAnsi.brightRed("[FAIL] Нет доступа к Fabric Meta"));
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
System.out.println(ZAnsi.brightRed("[FAIL] Cannot access Fabric Meta"));
System.out.println(ZAnsi.white("Error: ") + e.getMessage());
}
}
private void checkForge() {
System.out.println(ZAnsi.cyan("Проверка доступа к Forge Maven..."));
System.out.println(ZAnsi.cyan("Checking Forge Maven access..."));
try {
HttpClient client = HttpClient.newBuilder()
@@ -129,13 +129,13 @@ public class ServerCheckMenu {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
System.out.println(ZAnsi.brightGreen("[OK] Forge Maven доступен"));
System.out.println(ZAnsi.brightGreen("[OK] Forge Maven is accessible"));
} else {
System.out.println(ZAnsi.brightRed("[FAIL] Forge Maven вернул код " + response.statusCode()));
System.out.println(ZAnsi.brightRed("[FAIL] Forge Maven returned code " + response.statusCode()));
}
} catch (Exception e) {
System.out.println(ZAnsi.brightRed("[FAIL] Нет доступа к Forge Maven"));
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
System.out.println(ZAnsi.brightRed("[FAIL] Cannot access Forge Maven"));
System.out.println(ZAnsi.white("Error: ") + e.getMessage());
}
}
}
}
@@ -13,13 +13,13 @@ public class SettingsMenu {
public void show() throws IOException {
List<String> options = List.of(
"Настроить путь к Java",
"Настроить выделенную память (RAM)",
"Дополнительные JVM параметры",
"Назад в главное меню"
"Configure Java path",
"Configure allocated RAM",
"Additional JVM parameters",
"Back to main menu"
);
ArrowMenu menu = new ArrowMenu("Настройки лаунчера", options);
ArrowMenu menu = new ArrowMenu("Launcher Settings", options);
int choice = menu.show();
if (choice == -1 || choice == 3) return;
@@ -36,33 +36,33 @@ public class SettingsMenu {
}
private void configureJava() {
System.out.println(ZAnsi.cyan("Путь к Java:"));
System.out.println(ZAnsi.cyan("Java path:"));
System.out.println(" " + Config.getJreDir().toAbsolutePath());
System.out.println(ZAnsi.white("\nJava будет искаться автоматически в папке ~/.zernmc/jre/"));
System.out.println("Если нужно — положите туда свою версию Java.");
System.out.println(ZAnsi.white("\nJava will be searched automatically in ~/.zernmc/jre/"));
System.out.println("If needed, place your own Java version there.");
}
private void configureRam() {
System.out.println(ZAnsi.cyan("Настройка выделенной памяти"));
System.out.println(ZAnsi.cyan("RAM Allocation"));
System.out.println(Config.getRamInfo());
int newRam = Input.readInt(
ZAnsi.white("\nВведите новое значение RAM в MB (или 0 для отмены): "),
ZAnsi.white("\nEnter new RAM value in MB (or 0 to cancel): "),
0, 32768
);
if (newRam == 0) {
System.out.println(ZAnsi.yellow("Настройка отменена."));
System.out.println(ZAnsi.yellow("Setting cancelled."));
return;
}
Config.setMaxMemory(newRam);
System.out.println(ZAnsi.brightGreen("Выделенная память изменена на " + newRam + " MB"));
System.out.println(ZAnsi.brightGreen("Allocated RAM changed to " + newRam + " MB"));
}
private void configureJvmArgs() {
System.out.println(ZAnsi.yellow("Дополнительные JVM параметры"));
System.out.println("Пока в разработке.");
System.out.println("В будущем здесь будет список предустановленных оптимизаций.");
System.out.println(ZAnsi.yellow("Additional JVM parameters"));
System.out.println("Currently in development.");
System.out.println("A list of preset optimizations will be available in the future.");
}
}
}
@@ -18,12 +18,12 @@ public class UpdateMenu {
public void show() throws IOException {
List<String> options = List.of(
"Проверить обновления сборки (модпака)",
"Проверить обновления лаунчера",
"Назад в главное меню"
"Check pack updates",
"Check launcher updates",
"Back to main menu"
);
ArrowMenu menu = new ArrowMenu("Проверка обновлений", options);
ArrowMenu menu = new ArrowMenu("Update Check", options);
int choice = menu.show();
if (choice == -1 || choice == 2) return;
@@ -34,7 +34,7 @@ public class UpdateMenu {
try {
checkPackUpdates();
} catch (Exception e) {
System.out.println(ZAnsi.brightRed("Ошибка: " + e.getMessage()));
System.out.println(ZAnsi.brightRed("Error: " + e.getMessage()));
e.printStackTrace();
ConsoleUtils.pause();
}
@@ -44,7 +44,7 @@ public class UpdateMenu {
}
private void checkPackUpdates() throws Exception {
System.out.println(ZAnsi.cyan("Проверка обновлений сборок..."));
System.out.println(ZAnsi.cyan("Checking pack updates..."));
List<Instance> instances = InstanceManager.getAllInstances();
List<Instance> serverInstances = instances.stream()
@@ -52,12 +52,12 @@ public class UpdateMenu {
.collect(Collectors.toList());
if (serverInstances.isEmpty()) {
System.out.println(ZAnsi.yellow("Нет сборок, установленных с сервера."));
System.out.println(ZAnsi.yellow("No server-installed packs found."));
ConsoleUtils.pause();
return;
}
System.out.println(ZAnsi.cyan("\nПроверка обновлений для серверных сборок:\n"));
System.out.println(ZAnsi.cyan("\nChecking updates for server packs:\n"));
boolean hasUpdates = false;
List<Instance> updatableInstances = new ArrayList<>();
@@ -68,42 +68,41 @@ public class UpdateMenu {
try {
boolean hasUpdate = downloader.checkForUpdates(instance.getServerPackName());
if (hasUpdate) {
System.out.println(ZAnsi.yellow(instance.getName() + " - Есть обновление!"));
System.out.println(ZAnsi.yellow(instance.getName() + " - Update available!"));
updatableInstances.add(instance);
hasUpdates = true;
} else {
System.out.println(ZAnsi.green(instance.getName() + " - Актуальна"));
System.out.println(ZAnsi.green(instance.getName() + " - Up to date"));
}
} catch (Exception e) {
System.out.println(ZAnsi.red(instance.getName() + " - Ошибка проверки: " + e.getMessage()));
System.out.println(ZAnsi.red(instance.getName() + " - Check error: " + e.getMessage()));
}
}
if (!hasUpdates) {
System.out.println(ZAnsi.green("\nВсе сборки актуальны!"));
System.out.println(ZAnsi.green("\nAll packs are up to date!"));
ConsoleUtils.pause();
return;
}
// Предлагаем обновить каждую сборку отдельно
for (Instance instance : updatableInstances) {
System.out.println(ZAnsi.brightYellow("\nОбновить сборку '" + instance.getName() + "'?"));
if (Input.confirm("Обновить")) {
System.out.println(ZAnsi.cyan("Обновление " + instance.getName() + "..."));
System.out.println(ZAnsi.brightYellow("\nUpdate pack '" + instance.getName() + "'?"));
if (Input.confirm("Update")) {
System.out.println(ZAnsi.cyan("Updating " + instance.getName() + "..."));
PackDownloader downloader = new PackDownloader(instance);
try {
boolean success = downloader.updatePack(instance.getServerPackName());
if (success) {
System.out.println(ZAnsi.brightGreen(instance.getName() + " обновлен"));
System.out.println(ZAnsi.brightGreen(instance.getName() + " updated"));
} else {
System.out.println(ZAnsi.brightRed(instance.getName() + " не удалось обновить"));
System.out.println(ZAnsi.brightRed(instance.getName() + " update failed"));
}
} catch (Exception e) {
System.out.println(ZAnsi.brightRed(instance.getName() + ": " + e.getMessage()));
}
} else {
System.out.println(ZAnsi.yellow(" Пропущено: " + instance.getName()));
System.out.println(ZAnsi.yellow(" Skipped: " + instance.getName()));
}
}
@@ -111,28 +110,27 @@ public class UpdateMenu {
}
private void checkLauncherUpdates() {
System.out.println(ZAnsi.cyan("Проверка обновлений лаунчера..."));
System.out.println(ZAnsi.cyan("Checking launcher updates..."));
try {
String json = ZHttpClient.getLauncherVersionInfo();
String serverVersion = extractVersion(json);
String currentVersion = me.sashegdev.zernmc.launcher.utils.Version.getCurrentVersion();
System.out.println(ZAnsi.white("Текущая версия: ") + currentVersion);
System.out.println(ZAnsi.white("Версия на сервере: ") + serverVersion);
System.out.println(ZAnsi.white("Current version: ") + currentVersion);
System.out.println(ZAnsi.white("Server version: ") + serverVersion);
if (me.sashegdev.zernmc.launcher.utils.Version.isNewer(currentVersion, serverVersion)) {
System.out.println(ZAnsi.brightYellow("\nДоступна новая версия!"));
if (Input.confirm("Обновить лаунчер?")) {
// Обновление будет при следующем запуске
System.out.println(ZAnsi.green("Лаунчер будет обновлен при следующем запуске."));
System.out.println(ZAnsi.brightYellow("\nNew version available!"));
if (Input.confirm("Update launcher?")) {
System.out.println(ZAnsi.green("Launcher will be updated on next restart."));
}
} else {
System.out.println(ZAnsi.brightGreen("Лаунчер актуален."));
System.out.println(ZAnsi.brightGreen("Launcher is up to date."));
}
} catch (Exception e) {
System.out.println(ZAnsi.yellow("Не удалось проверить обновления лаунчера."));
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
System.out.println(ZAnsi.yellow("Could not check launcher updates."));
System.out.println(ZAnsi.white("Error: ") + e.getMessage());
}
ConsoleUtils.pause();
@@ -149,4 +147,4 @@ public class UpdateMenu {
return "unknown";
}
}
}
}
@@ -56,7 +56,7 @@ public class MinecraftLib {
boolean success = installer.install(minecraftVersion, loaderVersion);
if (success) {
// Сохраняем информацию в Instance
// Save info to Instance
instance.setMinecraftVersion(minecraftVersion);
instance.setLoaderType("fabric");
instance.setLoaderVersion(loaderVersion);
@@ -65,61 +65,61 @@ public class MinecraftLib {
}
/**
* Полная установка сборки (vanilla + loader + моды)
* Пока заглушка будем расширять
* Full pack install (vanilla + loader + mods)
* Stub - will be expanded
*/
public boolean installPack(String packName, String minecraftVersion, String loaderType, String loaderVersion) throws Exception {
System.out.println(ZAnsi.cyan("Начинается полная установка сборки: " + packName));
System.out.println(ZAnsi.cyan("Starting full pack install: " + packName));
// 1. Устанавливаем Minecraft
// 1. Install Minecraft
boolean mcInstalled = installMinecraft(minecraftVersion);
if (!mcInstalled) {
System.out.println(ZAnsi.brightRed("Не удалось установить Minecraft " + minecraftVersion));
System.out.println(ZAnsi.brightRed("Failed to install Minecraft " + minecraftVersion));
return false;
}
// 2. Устанавливаем лоадер
// 2. Install loader
if ("fabric".equalsIgnoreCase(loaderType)) {
boolean fabricInstalled = installFabric(minecraftVersion, loaderVersion);
if (!fabricInstalled) {
System.out.println(ZAnsi.brightRed("Не удалось установить Fabric"));
System.out.println(ZAnsi.brightRed("Failed to install Fabric"));
return false;
}
} else if ("forge".equalsIgnoreCase(loaderType)) {
boolean forgeInstalled = installForge(minecraftVersion, loaderVersion);
if (!forgeInstalled) {
System.out.println(ZAnsi.brightRed("Не удалось установить Forge"));
System.out.println(ZAnsi.brightRed("Failed to install Forge"));
return false;
}
} else if ("neoforge".equalsIgnoreCase(loaderType)) {
boolean neoforgeInstalled = installNeoForge(minecraftVersion, loaderVersion);
if (!neoforgeInstalled) {
System.out.println(ZAnsi.brightRed("Не удалось установить NeoForge"));
System.out.println(ZAnsi.brightRed("Failed to install NeoForge"));
return false;
}
}
// 3. В будущем здесь будет diff и скачивание модов
// 3. In the future: diff and mod download
System.out.println(ZAnsi.brightGreen("Базовая установка сборки завершена!"));
System.out.println(ZAnsi.brightGreen("Basic pack install complete!"));
return true;
}
//Запуск
//Launch
public void launch(LaunchOptions options) throws Exception {
System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName()));
System.out.println(ZAnsi.brightGreen("Launching pack: " + instance.getName()));
cleanupOldLoaders();
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
List<String> command = builder.build(options);
System.out.println(ZAnsi.cyan("Команда запуска (" + command.size() + " аргументов):"));
System.out.println(ZAnsi.cyan("Launch command (" + command.size() + " args):"));
command.forEach(arg -> System.out.println(" " + arg));
ProcessBuilder pb = new ProcessBuilder(command);
pb.directory(instance.getPath().toFile());
System.out.println(ZAnsi.brightGreen("\nЗапускаем Minecraft...\n"));
System.out.println(ZAnsi.brightGreen("\nStarting Minecraft...\n"));
ConsoleUtils.clearScreen();
Process process = pb.start();
@@ -132,7 +132,7 @@ public class MinecraftLib {
JFXLauncher.appendGameLog(line);
}
} catch (Exception e) {
JFXLauncher.appendGameLog("[Ошибка чтения вывода: " + e.getMessage() + "]");
JFXLauncher.appendGameLog("[Error reading output: " + e.getMessage() + "]");
}
});
outThread.setDaemon(true);
@@ -146,7 +146,7 @@ public class MinecraftLib {
JFXLauncher.appendGameLog("[ERR] " + line);
}
} catch (Exception e) {
JFXLauncher.appendGameLog("[Ошибка чтения ошибок: " + e.getMessage() + "]");
JFXLauncher.appendGameLog("[Error reading stderr: " + e.getMessage() + "]");
}
});
errThread.setDaemon(true);
@@ -156,7 +156,7 @@ public class MinecraftLib {
outThread.join(1000);
errThread.join(1000);
System.out.println(ZAnsi.yellow("\nMinecraft завершился с кодом: " + exitCode));
System.out.println(ZAnsi.yellow("\nMinecraft exited with code: " + exitCode));
}
private void safeDeleteDirectory(Path dir) {
@@ -202,9 +202,9 @@ public class MinecraftLib {
if (currentLoaderVer == null) return;
System.out.println(ZAnsi.yellow("Выполняем очистку старых версий лоадера..."));
System.out.println(ZAnsi.yellow("Cleaning old loader versions..."));
// Удаляем все старые fabric-loader / forge
// Delete all old fabric-loader / forge
Path libraries = instance.getPath().resolve("libraries");
if ("fabric".equals(loaderType)) {
@@ -36,18 +36,18 @@ public class PackDownloader {
}
/**
* Получить список доступных паков с сервера
* Get list of available packs from server
*/
public List<ServerPack> getAvailablePacks() throws Exception {
String accessToken = AuthManager.getAccessToken();
if (accessToken == null) {
throw new IOException("Не авторизован. Требуется проходка для просмотра сборок.");
throw new IOException("Not authenticated. Active pass required to view packs.");
}
if (!AuthManager.canViewPacks()) {
throw new IOException("Для просмотра сборок требуется активная проходка");
throw new IOException("Active pass required to view packs");
}
// Используем HttpURLConnection для GET с авторизацией
// Use HttpURLConnection for GET with auth
java.net.HttpURLConnection connection = null;
try {
java.net.URL url = new java.net.URL(ZHttpClient.getBaseUrl() + "/packs");
@@ -61,7 +61,7 @@ public class PackDownloader {
int responseCode = connection.getResponseCode();
if (responseCode == 403) {
throw new IOException("Для просмотра сборок требуется активная проходка");
throw new IOException("Active pass required to view packs");
}
StringBuilder response = new StringBuilder();
@@ -118,7 +118,7 @@ public class PackDownloader {
result.add(new ServerPack(name, version, minecraftVersion, loaderType,
loaderVersion, updatedAt, filesCount));
} catch (Exception e) {
System.err.println("Ошибка парсинга пака: " + e.getMessage());
System.err.println("Error parsing pack: " + e.getMessage());
}
}
@@ -126,7 +126,7 @@ public class PackDownloader {
}
/**
* Получить манифест пака
* Get pack manifest
*/
public PackManifest getPackManifest(String packName) throws Exception {
String response = ZHttpClient.get("/pack/" + packName);
@@ -134,18 +134,18 @@ public class PackDownloader {
}
/**
* Установить или обновить сборку с сервера
* Install or update a pack from the server
*/
public boolean installOrUpdatePack(String packName, ServerPack serverPack) throws Exception {
System.out.println(ZAnsi.cyan("Установка сборки " + packName + " с сервера..."));
System.out.println(ZAnsi.cyan("Installing pack " + packName + " from server..."));
// 1. Получаем манифест
// 1. Get manifest
PackManifest manifest = getPackManifest(packName);
// 2. Сначала устанавливаем Minecraft + Loader через MinecraftLib
// 2. First install Minecraft + Loader via MinecraftLib
MinecraftLib lib = new MinecraftLib(instance);
System.out.println(ZAnsi.cyan("Установка Minecraft " + manifest.getMinecraftVersion() + "..."));
System.out.println(ZAnsi.cyan("Installing Minecraft " + manifest.getMinecraftVersion() + "..."));
boolean needsMinecraftInstall = instance.getMinecraftVersion() == null ||
!instance.getMinecraftVersion().equals(manifest.getMinecraftVersion());
@@ -154,40 +154,40 @@ public class PackDownloader {
if ("fabric".equalsIgnoreCase(manifest.getLoaderType())) {
boolean success = lib.installFabric(manifest.getMinecraftVersion(), manifest.getLoaderVersion());
if (!success) {
System.err.println(ZAnsi.brightRed("Не удалось установить Fabric"));
System.err.println(ZAnsi.brightRed("Failed to install 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"));
System.err.println(ZAnsi.brightRed("Failed to install NeoForge"));
return false;
}
} else if ("forge".equalsIgnoreCase(manifest.getLoaderType())) {
boolean success = lib.installForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion());
if (!success) {
System.err.println(ZAnsi.brightRed("Не удалось установить Forge"));
System.err.println(ZAnsi.brightRed("Failed to install Forge"));
return false;
}
} else {
boolean success = lib.installMinecraft(manifest.getMinecraftVersion());
if (!success) {
System.err.println(ZAnsi.brightRed("Не удалось установить Vanilla Minecraft"));
System.err.println(ZAnsi.brightRed("Failed to install Vanilla Minecraft"));
return false;
}
}
} else {
System.out.println(ZAnsi.green("Minecraft уже установлен, пропускаем..."));
System.out.println(ZAnsi.green("Minecraft already installed, skipping..."));
}
// 3. Сканируем локальные файлы ТОЛЬКО если есть файлы для скачивания
// 3. Scan local files only if there are files to download
Map<String, String> localFiles = scanLocalFiles();
// Если в сборке нет файлов (только vanilla/loader), пропускаем diff
// If pack has no files (vanilla/loader only), skip diff
if (manifest.files == null || manifest.files.isEmpty()) {
System.out.println(ZAnsi.green("Сборка не содержит дополнительных файлов"));
System.out.println(ZAnsi.green("Pack contains no additional files"));
// Обновляем метаданные инстанса
// Update instance metadata
instance.setServerPack(true);
instance.setServerPackName(packName);
instance.setServerVersion(manifest.getVersion());
@@ -196,19 +196,19 @@ public class PackDownloader {
instance.setLoaderVersion(manifest.getLoaderVersion());
instance.setAssetIndex(manifest.getAssetIndex());
System.out.println(ZAnsi.brightGreen("Сборка успешно установлена!"));
System.out.println(ZAnsi.brightGreen("Pack installed successfully!"));
return true;
}
// 4. Отправляем diff запрос
System.out.println(ZAnsi.cyan("Проверка файлов сборки..."));
// 4. Send diff request
System.out.println(ZAnsi.cyan("Checking pack files..."));
DiffResponse diff = getDiff(packName, localFiles);
// 5. Применяем изменения
// 5. Apply changes
boolean success = applyDiff(diff, packName);
if (success) {
// 6. Обновляем метаданные инстанса
// 6. Update instance metadata
instance.setServerPack(true);
instance.setServerPackName(packName);
instance.setServerVersion(manifest.getVersion());
@@ -217,14 +217,14 @@ public class PackDownloader {
instance.setLoaderVersion(manifest.getLoaderVersion());
instance.setAssetIndex(manifest.getAssetIndex());
System.out.println(ZAnsi.brightGreen("Сборка успешно установлена!"));
System.out.println(ZAnsi.brightGreen("Pack installed successfully!"));
}
return success;
}
/**
* Проверить наличие обновлений для серверной сборки
* Check for server pack updates
*/
public boolean checkForUpdates(String packName) throws Exception {
if (!instance.isServerPack()) return false;
@@ -237,40 +237,40 @@ public class PackDownloader {
}
/**
* Обновить существующую серверную сборку
* Update an existing server pack
*/
public boolean updatePack(String packName) throws Exception {
System.out.println(ZAnsi.cyan("Проверка обновлений для " + instance.getName() + "..."));
System.out.println(ZAnsi.cyan("Checking updates for " + instance.getName() + "..."));
PackManifest manifest = getPackManifest(packName);
int serverVersion = manifest.getVersion();
if (serverVersion <= instance.getServerVersion()) {
System.out.println(ZAnsi.green("Сборка уже актуальна (v" + instance.getServerVersion() + ")"));
System.out.println(ZAnsi.green("Pack is already up to date (v" + instance.getServerVersion() + ")"));
return true;
}
System.out.println(ZAnsi.yellow("Доступно обновление: v" + instance.getServerVersion() + " → v" + serverVersion));
System.out.println(ZAnsi.yellow("Update available: v" + instance.getServerVersion() + " → v" + serverVersion));
// Сканируем локальные файлы
// Scan local files
Map<String, String> localFiles = scanLocalFiles();
// Получаем diff
// Get diff
DiffResponse diff = getDiff(packName, localFiles);
// Применяем изменения
// Apply changes
boolean success = applyDiff(diff, packName);
if (success) {
instance.setServerVersion(serverVersion);
System.out.println(ZAnsi.brightGreen("Сборка обновлена до v" + serverVersion));
System.out.println(ZAnsi.brightGreen("Pack updated to v" + serverVersion));
}
return success;
}
/**
* Сканирование локальных файлов и вычисление хешей
* Scan local files and compute hashes
*/
private Map<String, String> scanLocalFiles() throws IOException {
Map<String, String> files = new HashMap<>();
@@ -312,23 +312,23 @@ public class PackDownloader {
}
/**
* Отправить diff запрос на сервер
* Send diff request to server
*/
private DiffResponse getDiff(String packName, Map<String, String> localFiles) throws Exception {
String json = gson.toJson(localFiles);
// Получаем токен авторизации
// Get auth token
String accessToken = AuthManager.getAccessToken();
if (accessToken == null) {
throw new IOException("Не авторизован. Требуется проходка для скачивания сборок.");
throw new IOException("Not authenticated. Active pass required to download packs.");
}
if (!AuthManager.canDownloadPacks()) {
throw new IOException("Для скачивания сборок требуется активная проходка");
throw new IOException("Active pass required to download packs");
}
String url = ZHttpClient.getBaseUrl() + "/pack/" + packName + "/diff";
// Используем HttpURLConnection для полного контроля
// Use HttpURLConnection for full control
java.net.HttpURLConnection connection = null;
try {
java.net.URL urlObj = new java.net.URL(url);
@@ -342,7 +342,7 @@ public class PackDownloader {
connection.setConnectTimeout(30000);
connection.setReadTimeout(30000);
// Отправляем JSON
// Send JSON
try (java.io.OutputStream os = connection.getOutputStream()) {
byte[] input = json.getBytes("UTF-8");
os.write(input, 0, input.length);
@@ -351,7 +351,7 @@ public class PackDownloader {
int responseCode = connection.getResponseCode();
// Читаем ответ
// Read response
StringBuilder response = new StringBuilder();
try (java.io.InputStream is = responseCode < 400 ? connection.getInputStream() : connection.getErrorStream();
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(is, "UTF-8"))) {
@@ -364,7 +364,7 @@ public class PackDownloader {
String responseBody = response.toString();
if (responseCode == 403) {
throw new IOException("Для скачивания сборок требуется активная проходка. Обратитесь к администратору.");
throw new IOException("Active pass required to download packs. Contact the administrator.");
}
if (responseCode != 200) {
@@ -391,34 +391,34 @@ public class PackDownloader {
}
/**
* Применить diff (скачать новые файлы, удалить старые)
* Apply diff (download new files, delete old ones)
*/
private boolean applyDiff(DiffResponse diff, String packName) {
System.out.println(ZAnsi.cyan("\nПрименение изменений:"));
System.out.println(" Загрузить: " + diff.getToDownload().size() + " файлов");
System.out.println(" Удалить: " + diff.getToDelete().size() + " файлов");
System.out.println(ZAnsi.cyan("\nApplying changes:"));
System.out.println(" Download: " + diff.getToDownload().size() + " files");
System.out.println(" Delete: " + diff.getToDelete().size() + " files");
// Создаем директории если нужно
// Create directories if needed
try {
Files.createDirectories(instance.getPath());
} catch (IOException e) {
System.err.println(ZAnsi.red("Ошибка создания директорий: " + e.getMessage()));
System.err.println(ZAnsi.red("Error creating directories: " + e.getMessage()));
return false;
}
// Удаляем файлы
// Delete files
for (String filePath : diff.getToDelete()) {
Path fullPath = instance.getPath().resolve(filePath);
try {
if (Files.deleteIfExists(fullPath)) {
System.out.println(ZAnsi.yellow(" Удален: " + filePath));
System.out.println(ZAnsi.yellow(" Deleted: " + filePath));
}
} catch (IOException e) {
System.err.println(ZAnsi.red(" Ошибка удаления " + filePath + ": " + e.getMessage()));
System.err.println(ZAnsi.red(" Error deleting " + filePath + ": " + e.getMessage()));
}
}
// Скачиваем файлы
// Download files
AtomicInteger downloaded = new AtomicInteger(0);
int total = diff.getToDownload().size();
@@ -427,32 +427,32 @@ public class PackDownloader {
Path fullPath = instance.getPath().resolve(path);
try {
// Создаем директории
// Create directories
Files.createDirectories(fullPath.getParent());
// Скачиваем файл
// Download file
downloadFile(file, fullPath);
// Проверяем хеш
// Verify hash
String actualHash = calculateHash(fullPath);
if (!actualHash.equals(file.getHash())) {
throw new IOException("Хеш не совпадает! Ожидался: " + file.getHash() +
", получен: " + actualHash);
throw new IOException("Hash mismatch! Expected: " + file.getHash() +
", got: " + actualHash);
}
downloaded.incrementAndGet();
if (total > 0) {
ProgressBar.show("Скачивание", downloaded.get(), total, "файлов");
ProgressBar.show("Download", downloaded.get(), total, "files");
}
} catch (Exception e) {
System.err.println("\n" + ZAnsi.red(" Ошибка скачивания " + path + ": " + e.getMessage()));
System.err.println("\n" + ZAnsi.red(" Download error " + path + ": " + e.getMessage()));
return false;
}
}
if (total > 0) {
ProgressBar.finish("Скачивание");
ProgressBar.finish("Download");
}
return true;
@@ -26,7 +26,7 @@ public class FabricInstaller {
}
public boolean install(String minecraftVersion, String loaderVersion) throws Exception {
System.out.println(ZAnsi.cyan("Установка Fabric " + loaderVersion + " для Minecraft " + minecraftVersion));
System.out.println(ZAnsi.cyan("Installing Fabric " + loaderVersion + " for Minecraft " + minecraftVersion));
Path instancePath = instance.getPath();
cleanOldFabricLoaders();
@@ -34,7 +34,7 @@ public class FabricInstaller {
VersionInstaller versionInstaller = new VersionInstaller(instancePath);
String assetIndex = versionInstaller.install(minecraftVersion);
System.out.println(ZAnsi.green("Asset index получен: " + assetIndex));
System.out.println(ZAnsi.green("Asset index obtained: " + assetIndex));
instance.setAssetIndex(assetIndex);
instance.setMinecraftVersion(minecraftVersion);
@@ -46,12 +46,12 @@ public class FabricInstaller {
Path installerJar = instancePath.resolve("fabric-installer.jar");
if (!Files.exists(installerJar)) {
ProgressBar.show("Скачивание Fabric Installer", 0, 100, "%");
ProgressBar.show("Downloading Fabric Installer", 0, 100, "%");
downloadFileWithFallback(installerUrl, installerJar);
ProgressBar.finish("Fabric Installer скачан");
ProgressBar.finish("Fabric Installer downloaded");
}
System.out.println(ZAnsi.cyan("Запуск Fabric Installer..."));
System.out.println(ZAnsi.cyan("Running Fabric Installer..."));
String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion;
@@ -71,24 +71,24 @@ public class FabricInstaller {
int exitCode = process.waitFor();
if (exitCode != 0) {
System.out.println(ZAnsi.brightRed("Fabric Installer завершился с ошибкой (код " + exitCode + ")"));
System.out.println(ZAnsi.brightRed("Fabric Installer failed (code " + exitCode + ")"));
return false;
}
Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId);
if (Files.exists(fabricVersionDir)) {
System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!"));
System.out.println(ZAnsi.brightGreen("Fabric installed successfully!"));
instance.setLoaderType("fabric");
instance.setLoaderVersion(loaderVersion);
instance.setFabricVersionId(fabricVersionId); // СОХРАНЯЕМ
instance.setFabricVersionId(fabricVersionId);
ensureAssetIndexInFabricVersion(fabricVersionDir, assetIndex);
return true;
} else {
System.out.println(ZAnsi.brightRed("Fabric Installer отработал, но версия не найдена."));
System.out.println(ZAnsi.brightRed("Fabric Installer ran, but version not found."));
return false;
}
}
@@ -97,7 +97,7 @@ public class FabricInstaller {
try {
ZHttpClient.downloadFile(url, target);
} catch (Exception e) {
System.out.println(ZAnsi.yellow("Не удалось скачать Fabric Installer: " + e.getMessage()));
System.out.println(ZAnsi.yellow("Failed to download Fabric Installer: " + e.getMessage()));
throw e;
}
}
@@ -106,28 +106,28 @@ public class FabricInstaller {
Path versionJson = fabricVersionDir.resolve(fabricVersionDir.getFileName() + ".json");
if (!Files.exists(versionJson)) {
System.out.println(ZAnsi.yellow("JSON файл версии не найден: " + versionJson));
System.out.println(ZAnsi.yellow("Version JSON file not found: " + versionJson));
return;
}
String content = Files.readString(versionJson);
// Проверяем и исправляем asset index
// Check and fix asset index
if (!content.contains("\"assets\":\"" + assetIndex + "\"")) {
System.out.println(ZAnsi.yellow("Исправляем asset index в JSON файле версии..."));
System.out.println(ZAnsi.yellow("Fixing asset index in version JSON file..."));
// Заменяем assets на правильное значение
// Replace assets with correct value
content = content.replaceAll("\"assets\":\\s*\"[^\"]*\"", "\"assets\": \"" + assetIndex + "\"");
// Также проверяем assetIndex
// Also check assetIndex
if (content.contains("\"assetIndex\"")) {
content = content.replaceAll("\"assetIndex\":\\s*\"[^\"]*\"", "\"assetIndex\": \"" + assetIndex + "\"");
}
Files.writeString(versionJson, content);
System.out.println(ZAnsi.green("Asset index исправлен на: " + assetIndex));
System.out.println(ZAnsi.green("Asset index fixed to: " + assetIndex));
} else {
System.out.println(ZAnsi.green("Asset index в JSON версии правильный: " + assetIndex));
System.out.println(ZAnsi.green("Asset index in version JSON is correct: " + assetIndex));
}
}
@@ -135,7 +135,7 @@ public class FabricInstaller {
Path librariesDir = instance.getPath().resolve("libraries/net/fabricmc/fabric-loader");
if (!Files.exists(librariesDir)) return;
System.out.println(ZAnsi.yellow("Очистка старых версий Fabric Loader..."));
System.out.println(ZAnsi.yellow("Cleaning old Fabric Loader versions..."));
try (var stream = Files.walk(librariesDir)) {
stream.filter(Files::isDirectory)
@@ -155,18 +155,18 @@ public class FabricInstaller {
private String getLatestInstallerVersion() throws Exception {
try {
// Используем ZHttpClient с умным прокси
// Use ZHttpClient with smart proxy
String xml = ZHttpClient.downloadString("https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml");
int start = xml.indexOf("<latest>") + 8;
int end = xml.indexOf("</latest>", start);
return xml.substring(start, end).trim();
} catch (Exception e) {
System.out.println(ZAnsi.yellow("Ошибка получения версии Fabric Installer: " + e.getMessage()));
throw new Exception("Не удалось получить версию Fabric Installer", e);
System.out.println(ZAnsi.yellow("Error getting Fabric Installer version: " + e.getMessage()));
throw new Exception("Failed to get Fabric Installer version", e);
}
}
// под рефактор оставить
// under refactor - keep
private String downloadString(String url) throws Exception {
Exception lastException = null;
@@ -186,7 +186,7 @@ public class FabricInstaller {
throw new IOException("HTTP " + resp.statusCode());
} catch (Exception e) {
lastException = e;
System.out.println(ZAnsi.yellow("Попытка " + attempt + " не удалась: " + e.getMessage()));
System.out.println(ZAnsi.yellow("Attempt " + attempt + " failed: " + e.getMessage()));
if (attempt < 3) {
Thread.sleep(1000 * attempt);
}
@@ -207,7 +207,7 @@ public class FabricInstaller {
HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode() + " при скачивании " + url);
throw new IOException("HTTP " + response.statusCode() + " when downloading " + url);
}
}
}
@@ -26,59 +26,59 @@ public class ForgeInstaller {
}
public boolean install(String mcVersion, String forgeVersion) throws Exception {
System.out.println(ZAnsi.cyan("Установка Forge " + forgeVersion + " для Minecraft " + mcVersion));
System.out.println(ZAnsi.cyan("Installing Forge " + forgeVersion + " for Minecraft " + mcVersion));
// Шаг 1: Устанавливаем vanilla и получаем настоящий assetIndex
System.out.println(ZAnsi.cyan("Установка базовой версии Minecraft " + mcVersion + "..."));
// Step 1: Install vanilla and get real assetIndex
System.out.println(ZAnsi.cyan("Installing base Minecraft version " + mcVersion + "..."));
VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath());
String assetIndex = vanillaInstaller.install(mcVersion);
if (assetIndex == null || assetIndex.isEmpty()) {
System.out.println(ZAnsi.brightRed("Не удалось установить базовую версию Minecraft"));
System.out.println(ZAnsi.brightRed("Failed to install base Minecraft version"));
return false;
}
instance.setAssetIndex(assetIndex);
// Шаг 2: Создаём launcher_profiles.json
// Step 2: Create launcher_profiles.json
createLauncherProfile();
// Шаг 3: Скачиваем Forge Installer с прогресс-баром
// Step 3: Download Forge Installer with progress bar
String installerUrl = "https://maven.minecraftforge.net/net/minecraftforge/forge/"
+ mcVersion + "-" + forgeVersion
+ "/forge-" + mcVersion + "-" + forgeVersion + "-installer.jar";
Path installerJar = instance.getPath().resolve("forge-installer.jar");
System.out.println(ZAnsi.cyan("Скачивание Forge Installer..."));
System.out.println(ZAnsi.cyan("Downloading Forge Installer..."));
downloadFileWithProgress(installerUrl, installerJar);
// Шаг 4: Запускаем Forge Installer и показываем его вывод
System.out.println(ZAnsi.cyan("Запуск Forge Installer..."));
System.out.println(ZAnsi.yellow("Это может занять несколько минут. Пожалуйста, подождите...\n"));
// Step 4: Run Forge Installer and show its output
System.out.println(ZAnsi.cyan("Running Forge Installer..."));
System.out.println(ZAnsi.yellow("This may take a few minutes. Please wait...\n"));
boolean success = runForgeInstaller(installerJar);
// После успешной установки Forge, но перед сохранением метаданных
// After successful Forge install, before saving metadata
if (success) {
// Докачиваем пропущенные библиотеки
// Download missing libraries
try {
downloadMissingLibraries(mcVersion, forgeVersion);
} catch (Exception e) {
System.out.println(ZAnsi.yellow("Предупреждение: не удалось докачать некоторые библиотеки: " + e.getMessage()));
System.out.println(ZAnsi.yellow("Warning: could not download some libraries: " + e.getMessage()));
}
System.out.println(ZAnsi.brightGreen("\nForge " + forgeVersion + " успешно установлен!"));
System.out.println(ZAnsi.brightGreen("\nForge " + forgeVersion + " installed successfully!"));
instance.setMinecraftVersion(mcVersion);
instance.setLoaderType("forge");
instance.setLoaderVersion(forgeVersion);
// Очищаем временный файл установщика
// Clean up temporary installer file
Files.deleteIfExists(installerJar);
return true;
} else {
System.out.println(ZAnsi.brightRed("\nОшибка при установке Forge!"));
System.out.println(ZAnsi.brightRed("\nError installing Forge!"));
return false;
}
}
@@ -94,7 +94,7 @@ public class ForgeInstaller {
}
""";
Files.writeString(profilePath, minimalProfile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println(ZAnsi.yellow("Создан launcher_profiles.json"));
System.out.println(ZAnsi.yellow("Created launcher_profiles.json"));
}
private void downloadFileWithProgress(String url, Path target) throws Exception {
@@ -132,10 +132,10 @@ public class ForgeInstaller {
lastPercent = percent;
}
} else {
// Если размер неизвестен, показываем анимацию
// If size unknown, show animation
char[] spinner = {'|', '/', '-', '\\'};
int idx = (int) (totalRead / 1024) % 4;
System.out.print("\rСкачивание Forge Installer: " + ProgressBar.formatBytes(totalRead) + " " + spinner[idx]);
System.out.print("\rDownloading Forge Installer: " + ProgressBar.formatBytes(totalRead) + " " + spinner[idx]);
}
}
}
@@ -144,12 +144,12 @@ public class ForgeInstaller {
}
private boolean runForgeInstaller(Path installerJar) throws IOException, InterruptedException {
// Пробуем до 3 раз с разными опциями
// Try up to 3 times with different options
int maxRetries = 3;
int attempt = 1;
while (attempt <= maxRetries) {
System.out.println(ZAnsi.cyan("Попытка " + attempt + " из " + maxRetries));
System.out.println(ZAnsi.cyan("Attempt " + attempt + " of " + maxRetries));
ProcessBuilder pb = new ProcessBuilder(
"java",
@@ -158,7 +158,7 @@ public class ForgeInstaller {
"--installClient"
);
// Добавляем JVM аргументы для увеличения таймаутов
// Add JVM args for increased timeouts
pb.environment().put("JAVA_OPTS", "-Dhttp.connectionTimeout=60000 -Dhttp.socketTimeout=60000");
pb.directory(instance.getPath().toFile());
@@ -166,7 +166,7 @@ public class ForgeInstaller {
Process process = pb.start();
// Читаем вывод в реальном времени
// Read output in real time
StringBuilder output = new StringBuilder();
boolean hasErrors = false;
@@ -175,7 +175,7 @@ public class ForgeInstaller {
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
// Форматируем вывод Forge Installer
// Format Forge Installer output
if (line.contains("Downloading") || line.contains("Extracting")) {
System.out.println(ZAnsi.blue(" -> " + line));
} else if (line.contains("SUCCESS") || line.contains("successfully")) {
@@ -195,17 +195,17 @@ public class ForgeInstaller {
int exitCode = process.waitFor();
// Если успешно или нет ошибок скачивания
// If successful or no download errors
if (exitCode == 0 && !hasErrors) {
return true;
}
// Если ошибка и это не последняя попытка
// If error and not last attempt
if (attempt < maxRetries) {
System.out.println(ZAnsi.yellow("Ошибка при установке. Повторная попытка через 5 секунд..."));
System.out.println(ZAnsi.yellow("Install error. Retrying in 5 seconds..."));
Thread.sleep(5000);
// Очищаем временные файлы перед повтором
// Clean temp files before retry
Path librariesDir = instance.getPath().resolve("libraries");
if (Files.exists(librariesDir)) {
// Удаляем только частично скачанные библиотеки Forge
@@ -218,15 +218,15 @@ public class ForgeInstaller {
}
}
} else {
System.out.println(ZAnsi.brightRed("Forge Installer завершился с кодом ошибки: " + exitCode));
System.out.println(ZAnsi.brightRed("Forge Installer exited with error code: " + exitCode));
// Показываем возможное решение
// Show possible solution
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"));
System.out.println(ZAnsi.yellow("\nPossible solutions:"));
System.out.println(ZAnsi.yellow("1. Check your internet connection"));
System.out.println(ZAnsi.yellow("2. Run the launcher as administrator"));
System.out.println(ZAnsi.yellow("3. Temporarily disable antivirus/firewall"));
System.out.println(ZAnsi.yellow("4. Try installing a different Forge version"));
}
}
@@ -237,9 +237,9 @@ public class ForgeInstaller {
}
private void downloadMissingLibraries(String mcVersion, String forgeVersion) throws Exception {
System.out.println(ZAnsi.cyan("Проверка и докачка отсутствующих библиотек..."));
System.out.println(ZAnsi.cyan("Checking and downloading missing libraries..."));
// Список проблемных библиотек и их альтернативные URL
// List of problematic libraries and their alternate URLs
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");
@@ -252,7 +252,7 @@ public class ForgeInstaller {
Path target = librariesDir.resolve(entry.getKey());
if (!Files.exists(target)) {
Files.createDirectories(target.getParent());
System.out.println(ZAnsi.yellow("Докачка: " + target.getFileName()));
System.out.println(ZAnsi.yellow("Downloading: " + target.getFileName()));
for (int attempt = 1; attempt <= 3; attempt++) {
try {
@@ -27,14 +27,14 @@ public class NeoForgeInstaller {
}
public boolean install(String mcVersion, String neoForgeVersion) throws Exception {
System.out.println(ZAnsi.cyan("Установка NeoForge " + neoForgeVersion + " для Minecraft " + mcVersion));
System.out.println(ZAnsi.cyan("Installing NeoForge " + neoForgeVersion + " for Minecraft " + mcVersion));
System.out.println(ZAnsi.cyan("Установка базовой версии Minecraft " + mcVersion + "..."));
System.out.println(ZAnsi.cyan("Installing base Minecraft version " + mcVersion + "..."));
VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath());
String assetIndex = vanillaInstaller.install(mcVersion);
if (assetIndex == null || assetIndex.isEmpty()) {
System.out.println(ZAnsi.brightRed("Не удалось установить базовую версию Minecraft"));
System.out.println(ZAnsi.brightRed("Failed to install base Minecraft version"));
return false;
}
@@ -52,11 +52,11 @@ public class NeoForgeInstaller {
Path installerJar = instance.getPath().resolve("neoforge-installer.jar");
System.out.println(ZAnsi.cyan("Скачивание NeoForge Installer..."));
System.out.println(ZAnsi.cyan("Downloading NeoForge Installer..."));
downloadFileWithProgress(installerUrl, installerJar);
System.out.println(ZAnsi.cyan("Запуск NeoForge Installer..."));
System.out.println(ZAnsi.yellow("Это может занять несколько минут. Пожалуйста, подождите...\n"));
System.out.println(ZAnsi.cyan("Running NeoForge Installer..."));
System.out.println(ZAnsi.yellow("This may take a few minutes. Please wait...\n"));
boolean success = runNeoForgeInstaller(installerJar);
@@ -64,10 +64,10 @@ public class NeoForgeInstaller {
try {
downloadMissingLibraries(mcVersion, neoForgeVersion, mavenGroup, mavenArtifact);
} catch (Exception e) {
System.out.println(ZAnsi.yellow("Предупреждение: не удалось докачать некоторые библиотеки: " + e.getMessage()));
System.out.println(ZAnsi.yellow("Warning: could not download some libraries: " + e.getMessage()));
}
System.out.println(ZAnsi.brightGreen("\nNeoForge " + neoForgeVersion + " успешно установлен!"));
System.out.println(ZAnsi.brightGreen("\nNeoForge " + neoForgeVersion + " installed successfully!"));
instance.setMinecraftVersion(mcVersion);
instance.setLoaderType("neoforge");
instance.setLoaderVersion(neoForgeVersion);
@@ -75,7 +75,7 @@ public class NeoForgeInstaller {
Files.deleteIfExists(installerJar);
return true;
} else {
System.out.println(ZAnsi.brightRed("\nОшибка при установке NeoForge!"));
System.out.println(ZAnsi.brightRed("\nError installing NeoForge!"));
return false;
}
}
@@ -105,7 +105,7 @@ public class NeoForgeInstaller {
}
""";
Files.writeString(profilePath, minimalProfile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println(ZAnsi.yellow("Создан launcher_profiles.json"));
System.out.println(ZAnsi.yellow("Created launcher_profiles.json"));
}
private void downloadFileWithProgress(String url, Path target) throws Exception {
@@ -145,7 +145,7 @@ public class NeoForgeInstaller {
} else {
char[] spinner = {'|', '/', '-', '\\'};
int idx = (int) (totalRead / 1024) % 4;
System.out.print("\rСкачивание NeoForge Installer: " + ProgressBar.formatBytes(totalRead) + " " + spinner[idx]);
System.out.print("\rDownloading NeoForge Installer: " + ProgressBar.formatBytes(totalRead) + " " + spinner[idx]);
}
}
}
@@ -158,7 +158,7 @@ public class NeoForgeInstaller {
int attempt = 1;
while (attempt <= maxRetries) {
System.out.println(ZAnsi.cyan("Попытка " + attempt + " из " + maxRetries));
System.out.println(ZAnsi.cyan("Attempt " + attempt + " of " + maxRetries));
ProcessBuilder pb = new ProcessBuilder(
"java",
@@ -205,7 +205,7 @@ public class NeoForgeInstaller {
}
if (attempt < maxRetries) {
System.out.println(ZAnsi.yellow("Ошибка при установке. Повторная попытка через 5 секунд..."));
System.out.println(ZAnsi.yellow("Install error. Retrying in 5 seconds..."));
Thread.sleep(5000);
Path librariesDir = instance.getPath().resolve("libraries");
@@ -219,14 +219,14 @@ public class NeoForgeInstaller {
}
}
} else {
System.out.println(ZAnsi.brightRed("NeoForge Installer завершился с кодом ошибки: " + exitCode));
System.out.println(ZAnsi.brightRed("NeoForge Installer exited with error code: " + 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"));
System.out.println(ZAnsi.yellow("\nPossible solutions:"));
System.out.println(ZAnsi.yellow("1. Check your internet connection"));
System.out.println(ZAnsi.yellow("2. Run the launcher as administrator"));
System.out.println(ZAnsi.yellow("3. Temporarily disable antivirus/firewall"));
System.out.println(ZAnsi.yellow("4. Try installing a different NeoForge version"));
}
}
@@ -237,7 +237,7 @@ public class NeoForgeInstaller {
}
private void downloadMissingLibraries(String mcVersion, String neoForgeVersion, String mavenGroup, String mavenArtifact) throws Exception {
System.out.println(ZAnsi.cyan("Проверка и докачка отсутствующих библиотек..."));
System.out.println(ZAnsi.cyan("Checking and downloading missing libraries..."));
Map<String, String> alternativeUrls = new HashMap<>();
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar",
@@ -253,7 +253,7 @@ public class NeoForgeInstaller {
Path target = librariesDir.resolve(entry.getKey());
if (!Files.exists(target)) {
Files.createDirectories(target.getParent());
System.out.println(ZAnsi.yellow("Докачка: " + target.getFileName()));
System.out.println(ZAnsi.yellow("Downloading: " + target.getFileName()));
for (int attempt = 1; attempt <= 3; attempt++) {
try {
@@ -261,7 +261,7 @@ public class NeoForgeInstaller {
break;
} catch (Exception e) {
if (attempt == 3) throw e;
System.out.println(ZAnsi.yellow("Повторная попытка " + attempt + "/3..."));
System.out.println(ZAnsi.yellow("Retry " + attempt + "/3..."));
Thread.sleep(2000);
}
}
@@ -57,12 +57,12 @@ public class VersionInstaller {
}
public String install(String versionId) throws Exception {
System.out.println(ZAnsi.cyan("Полная установка Minecraft " + versionId + "..."));
System.out.println(ZAnsi.cyan("Full install of Minecraft " + versionId + "..."));
Path versionDir = minecraftDir.resolve("versions").resolve(versionId);
Files.createDirectories(versionDir);
String versionUrl = getVersionUrl(versionId);
if (versionUrl == null) throw new Exception("Версия " + versionId + " не найдена");
if (versionUrl == null) throw new Exception("Version " + versionId + " not found");
String versionJson = downloadString(versionUrl);
Files.writeString(versionDir.resolve(versionId + ".json"), versionJson);
@@ -73,8 +73,8 @@ public class VersionInstaller {
downloadFile(versionData.getJSONObject("downloads").getJSONObject("client").getString("url"),
versionDir.resolve(versionId + ".jar"), "client.jar");
// Библиотеки
System.out.println(ZAnsi.cyan("Скачивание библиотек..."));
// Libraries
System.out.println(ZAnsi.cyan("Downloading libraries..."));
downloadLibraries(versionData.getJSONArray("libraries"));
String assetIndex;
@@ -86,12 +86,12 @@ public class VersionInstaller {
System.out.println(ZAnsi.cyan("Asset index: " + assetIndex));
// Скачиваем ассеты используя правильный индекс
System.out.println(ZAnsi.cyan("Скачивание ассетов..."));
// Download assets using correct index
System.out.println(ZAnsi.cyan("Downloading assets..."));
downloadAssets(versionData, assetIndex);
System.out.println(ZAnsi.brightGreen("\nMinecraft " + versionId + " полностью установлен!"));
return assetIndex; // возвращаем "5" а не "1.20.1"
System.out.println(ZAnsi.brightGreen("\nMinecraft " + versionId + " fully installed!"));
return assetIndex;
}
private void downloadLibraries(JSONArray libraries) throws Exception {
@@ -111,32 +111,32 @@ public class VersionInstaller {
try {
downloadFile(url, target, "library");
} catch (Exception e) {
// Пропускаем проблемные библиотеки
// Skip problematic libraries
}
}
count++;
ProgressBar.show("Библиотеки", count, total, "файлов");
ProgressBar.show("Libraries", count, total, "files");
}
ProgressBar.finish("Библиотеки загружены");
ProgressBar.finish("Libraries downloaded");
}
private void downloadAssets(JSONObject versionData, String assetIndex) throws Exception {
// Находим URL для asset index
// Find URL for asset index
JSONObject assetIndexInfo = versionData.getJSONObject("assetIndex");
String indexUrl = assetIndexInfo.getString("url");
Path indexesDir = minecraftDir.resolve("assets/indexes");
Files.createDirectories(indexesDir);
Path indexPath = indexesDir.resolve(assetIndex + ".json"); // используем assetIndex
Path indexPath = indexesDir.resolve(assetIndex + ".json");
System.out.println(ZAnsi.cyan("Скачивание asset index (" + assetIndex + ")..."));
System.out.println(ZAnsi.cyan("Downloading asset index (" + assetIndex + ")..."));
downloadFile(indexUrl, indexPath, "asset index");
String jsonContent = Files.readString(indexPath);
JSONObject root = new JSONObject(jsonContent);
JSONObject objects = root.getJSONObject("objects");
System.out.println(ZAnsi.cyan("Скачивание " + objects.length() + " объектов ассетов (index: " + assetIndex + ")..."));
System.out.println(ZAnsi.cyan("Downloading " + objects.length() + " asset objects (index: " + assetIndex + ")..."));
int total = objects.length();
int[] success = {0};
@@ -146,7 +146,7 @@ public class VersionInstaller {
for (String key : objects.keySet()) {
JSONObject asset = objects.getJSONObject(key);
String hash = asset.getString("hash"); // вот это правильный хеш!
String hash = asset.getString("hash");
String url = "https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash;
Path target = minecraftDir.resolve("assets/objects")
@@ -160,19 +160,19 @@ public class VersionInstaller {
for (int attempt = 1; attempt <= 3; attempt++) {
try {
downloadFile(url, target, "");
synchronized (this) {
success[0]++;
ProgressBar.show("Ассеты", success[0], total, "файлов");
}
downloaded = true;
break;
} catch (Exception e) {
if (attempt == 3) {
synchronized (this) {
failed[0]++;
success[0]++;
ProgressBar.show("Assets", success[0], total, "files");
}
System.err.println("Не удалось скачать " + hash);
} else {
downloaded = true;
break;
} catch (Exception e) {
if (attempt == 3) {
synchronized (this) {
failed[0]++;
}
System.err.println("Failed to download " + hash);
} else {
try { Thread.sleep(500 * attempt); } catch (InterruptedException ignored) {}
}
}
@@ -184,17 +184,17 @@ public class VersionInstaller {
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
ProgressBar.finish("Ассеты загружены (" + success[0] + " успешно, " + failed[0] + " пропущено)");
ProgressBar.finish("Assets downloaded (" + success[0] + " ok, " + failed[0] + " skipped)");
if (failed[0] > 0) {
System.out.println(ZAnsi.yellow("Предупреждение: " + failed[0] + " файлов ассетов не удалось скачать."));
System.out.println(ZAnsi.yellow("Игра запустится, но некоторые текстуры/звуки могут отсутствовать."));
System.out.println(ZAnsi.yellow("Warning: " + failed[0] + " asset files could not be downloaded."));
System.out.println(ZAnsi.yellow("The game will launch, but some textures/sounds may be missing."));
}
}
public String getAssetIndexId(String versionId) throws Exception {
String versionUrl = getVersionUrl(versionId);
if (versionUrl == null) throw new Exception("Версия не найдена");
if (versionUrl == null) throw new Exception("Version not found");
String versionJson = downloadString(versionUrl);
JSONObject versionData = new JSONObject(versionJson);
@@ -202,7 +202,7 @@ public class VersionInstaller {
if (versionData.has("assetIndex") && versionData.getJSONObject("assetIndex").has("id")) {
return versionData.getJSONObject("assetIndex").getString("id"); // "5" для 1.20.1
}
return versionData.getString("assets"); // fallback (очень старые версии)
return versionData.getString("assets"); // fallback (very old versions)
}
private String getVersionUrl(String versionId) throws Exception {
@@ -222,7 +222,7 @@ public class VersionInstaller {
private void downloadFile(String url, Path target, String label) throws Exception {
if (!label.isEmpty()) {
ProgressBar.clearLine();
System.out.println(ZAnsi.cyan("Скачивание " + label + "..."));
System.out.println(ZAnsi.cyan("Downloading " + label + "..."));
}
HttpRequest request = HttpRequest.newBuilder()
@@ -233,8 +233,8 @@ public class VersionInstaller {
HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() != 200) {
if (label.isEmpty()) return; // для ассетов молча
throw new IOException("HTTP " + response.statusCode() + " при скачивании " + label);
if (label.isEmpty()) return; // for assets silently
throw new IOException("HTTP " + response.statusCode() + " while downloading " + label);
}
if (!label.isEmpty()) {
@@ -21,11 +21,12 @@ public class LaunchCommandBuilder {
}
public List<String> build(LaunchOptions options) throws Exception {
System.out.println(ZAnsi.cyan("Генерация команды запуска для " + instance.getName() + "..."));
System.out.println(ZAnsi.cyan("Generating launch command for " + instance.getName() + "..."));
List<String> command = new ArrayList<>();
String javaPath = "java";
String javaPath = options.getJavaPath() != null && !options.getJavaPath().isEmpty()
? options.getJavaPath() : "java";
command.add(javaPath);
command.addAll(getJvmArguments(options));
@@ -53,7 +54,7 @@ public class LaunchCommandBuilder {
// Fallback if classpath is empty
if (classpath.isEmpty() || classpath.equals(instance.getPath().resolve("versions").resolve(getVersionId()).resolve(getVersionId() + ".jar").toAbsolutePath().toString())) {
System.out.println(ZAnsi.yellow(" manifest classpath пустой, использую vanilla classpath"));
System.out.println(ZAnsi.yellow(" manifest classpath empty, using vanilla classpath"));
command.add("-cp");
command.add(buildVanillaClasspath());
command.add(getVanillaMainClass());
@@ -83,15 +84,15 @@ public class LaunchCommandBuilder {
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()));
System.out.println(ZAnsi.green("Found version.json: " + versionJson.getFileName()));
return new VersionManifest(json);
} else {
System.out.println(ZAnsi.yellow("version.json не найден для " + instance.getName()));
System.out.println(ZAnsi.yellow("version.json not found for " + instance.getName()));
System.out.println(ZAnsi.yellow(" loaderType=" + instance.getLoaderType() + " mcVersion=" + instance.getMinecraftVersion() + " loaderVersion=" + instance.getLoaderVersion()));
System.out.println(ZAnsi.yellow(" path=" + instance.getPath()));
}
} catch (Exception e) {
System.out.println(ZAnsi.yellow("Не удалось загрузить version.json: " + e.getMessage()));
System.out.println(ZAnsi.yellow("Failed to load version.json: " + e.getMessage()));
}
return null;
}
@@ -251,9 +252,9 @@ public class LaunchCommandBuilder {
String assetIndex = instance.getAssetIndex();
if (assetIndex == null || assetIndex.isEmpty()) {
assetIndex = instance.getMinecraftVersion();
System.out.println(ZAnsi.yellow("Asset index не найден, использую версию: " + assetIndex));
System.out.println(ZAnsi.yellow("Asset index not found, using version: " + assetIndex));
} else {
System.out.println(ZAnsi.green("Использую asset index: " + assetIndex));
System.out.println(ZAnsi.green("Using asset index: " + assetIndex));
}
args.add(assetIndex);
args.add("--username");
@@ -332,7 +333,7 @@ public class LaunchCommandBuilder {
if (Files.exists(fallbackPath)) {
paths.add(fallbackPath.toAbsolutePath().toString());
} else {
System.out.println(ZAnsi.yellow(" Библиотека не найдена: " + lib.name));
System.out.println(ZAnsi.yellow(" Library not found: " + lib.name));
}
}
}
@@ -37,5 +37,7 @@ public class LaunchOptions {
public void setExtraJvmArgs(List<String> extraJvmArgs) { this.extraJvmArgs = extraJvmArgs; }
public int getWidth() { return width; }
public void setWidth(int width) { this.width = width; }
public int getHeight() { return height; }
public void setHeight(int height) { this.height = height; }
}
@@ -5,8 +5,9 @@ import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.InfoCmp;
import java.nio.charset.StandardCharsets;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class ArrowMenu {
@@ -15,19 +16,22 @@ public class ArrowMenu {
private final List<String> options;
private int selected = 0;
private final Terminal terminal;
private final InputStream rawInput;
private static final int VISIBLE_ITEMS = 7;
public ArrowMenu(String title, List<String> options) throws IOException {
this.title = title;
this.options = options;
System.setProperty("jline.terminal", "unsupported");
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");
System.setProperty("jline.terminal", isWindows ? "win" : "unsupported");
this.terminal = TerminalBuilder.builder()
.system(true)
.jna(false)
.jna(isWindows)
.jansi(true)
.encoding(StandardCharsets.UTF_8)
.build();
this.rawInput = terminal.input();
}
public int show() throws IOException {
@@ -38,56 +42,58 @@ public class ArrowMenu {
try {
while (true) {
printPagedMenu();
int key = terminal.reader().read();
int b = rawInput.read();
if (b == -1) continue;
if (key == 'w' || key == 'W' || key == 'ц' || key == 'Ц'
|| key == 'k' || key == 'K' || key == 'л' || key == 'Л') {
// w/W/k/K or ц (0xD1 0x86) = up
// s/S/j/J or ы (0xD1 0x8B) = down
if (b == 'w' || b == 'W' || b == 'k' || b == 'K') {
selected = (selected - 1 + options.size()) % options.size();
}
else if (key == 's' || key == 'S' || key == 'ы' || key == 'Ы'
|| key == 'j' || key == 'J' || key == 'о' || key == 'О') {
else if (b == 's' || b == 'S' || b == 'j' || b == 'J') {
selected = (selected + 1) % options.size();
}
else if (key == 13 || key == 10) {
return selected;
}
else if (key == 27) {
// ESC sequences: arrows + cyrillic start byte
else if (b == 0x1B) {
int next = nonBlockingRead();
if (next == -1) {
return -1;
}
if (next == 91) {
if (next == 0x5B) { // '['
int arrow = nonBlockingRead();
if (arrow == 65) {
if (arrow == 0x41) { // Up
selected = (selected - 1 + options.size()) % options.size();
} else if (arrow == 66) {
} else if (arrow == 0x42) { // Down
selected = (selected + 1) % options.size();
}
} else if (next == 79) {
int arrow = nonBlockingRead();
if (arrow == 68) {
selected = (selected - 1 + options.size()) % options.size();
} else if (arrow == 70) {
return options.size() - 1;
}
}
}
else if (b == 0xD1) {
int second = nonBlockingRead();
if (second == 0x86) { // ц
selected = (selected - 1 + options.size()) % options.size();
} else if (second == 0x8B) { // ы
selected = (selected + 1) % options.size();
}
}
else if (b == 13 || b == 10) {
return selected;
}
}
} finally {
terminal.puts(InfoCmp.Capability.cursor_visible);
terminal.close();
}
}
private int nonBlockingRead() throws IOException {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 200) {
int available = terminal.reader().available();
if (available > 0) {
return terminal.reader().read();
while (System.currentTimeMillis() - startTime < 100) {
if (rawInput.available() > 0) {
return rawInput.read();
}
try {
Thread.sleep(5);
Thread.sleep(2);
} catch (InterruptedException e) {
return -1;
}
@@ -119,9 +125,9 @@ public class ArrowMenu {
}
sb.append("\n")
.append(ZAnsi.white("W/S (Ц/Ы) или ↑/↓ - перемещение | Enter - выбрать | Esc - назад"));
.append(ZAnsi.white("W/S or \u2191/\u2193 - navigate | Enter - select | Esc - back"));
System.out.print(sb);
System.out.flush();
}
}
}
@@ -15,6 +15,7 @@ import me.sashegdev.zernmc.launcher.minecraft.Instance;
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
import me.sashegdev.zernmc.launcher.minecraft.MinecraftLib;
import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder;
import me.sashegdev.zernmc.launcher.utils.Config;
import java.io.BufferedReader;
import java.io.InputStream;
@@ -49,6 +50,7 @@ public class JFXLauncher extends Application {
private static StringBuilder launcherLogBuffer = new StringBuilder();
private static StringBuilder gameLogBuffer = new StringBuilder();
private static Path gameLogFile;
private static Path launcherLogFile;
private Stage mainStage;
private static volatile String installProgressLabel = "";
@@ -103,6 +105,7 @@ public class JFXLauncher extends Application {
}
public static void appendGameLog(String log) {
System.out.println("[GAME] " + log);
synchronized (gameLogBuffer) {
gameLogBuffer.append(log).append("\n");
@@ -167,15 +170,15 @@ public class JFXLauncher extends Application {
String serverVersion = getServerVersion();
if (serverVersion != null && !serverVersion.isEmpty()) {
System.out.println("[JFX] Загрузка assets через мета для версии " + serverVersion);
System.out.println("[JFX] Loading assets via meta for version " + serverVersion);
if (downloadAssetsFromMeta(serverVersion)) {
System.out.println("[JFX] Assets загружены через мета");
System.out.println("[JFX] Assets loaded via meta");
return;
}
System.out.println("[JFX] Мета недоступна, использую fallback");
System.out.println("[JFX] Meta unavailable, using fallback");
}
System.out.println("[JFX] Извлечение assets из JAR...");
System.out.println("[JFX] Extracting assets from JAR...");
Path jarPath = Paths.get(JFXLauncher.class.getProtectionDomain().getCodeSource().getLocation().toURI());
if (Files.exists(jarPath) && jarPath.toString().endsWith(".jar")) {
try (JarFile jar = new JarFile(jarPath.toFile())) {
@@ -195,10 +198,10 @@ public class JFXLauncher extends Application {
}
}
}
System.out.println("[JFX] Assets извлечены из JAR");
System.out.println("[JFX] Assets extracted from JAR");
}
} catch (Exception e) {
System.out.println("[JFX] Ошибка извлечения assets: " + e.getMessage());
System.out.println("[JFX] Error extracting assets: " + e.getMessage());
}
}
@@ -268,7 +271,7 @@ public class JFXLauncher extends Application {
return true;
}
} catch (Exception e) {
System.out.println("[JFX] Ошибка загрузки через мета: " + e.getMessage());
System.out.println("[JFX] Error loading via meta: " + e.getMessage());
return false;
}
}
@@ -278,8 +281,15 @@ public class JFXLauncher extends Application {
this.mainStage = stage;
try {
// Initialize launcher log file
Path logsDir = Paths.get("logs");
Files.createDirectories(logsDir);
launcherLogFile = logsDir.resolve("launcher.log");
Files.writeString(launcherLogFile, "=== Launcher Log " + LocalDateTime.now() + " ===\n",
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
extractAssets();
log("Запуск JFX UI...");
log("Starting JFX UI...");
startServer();
WebView webView = new WebView();
@@ -289,7 +299,7 @@ public class JFXLauncher extends Application {
engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
log("[UI] Load state: " + oldState + " -> " + newState);
if (newState == Worker.State.SUCCEEDED) {
log("Страница загружена");
log("Page loaded");
} else if (newState == Worker.State.FAILED) {
log("[UI] Load FAILED: " + engine.getLoadWorker().getException());
}
@@ -308,27 +318,17 @@ public class JFXLauncher extends Application {
stage.setScene(new Scene(webView));
stage.show();
log("Окно отображено");
log("Window displayed");
stage.setOnCloseRequest(e -> {
log("Закрытие...");
log("Closing...");
LaunchService.killAllProcesses();
if (server != null) server.stop(1);
try {
java.net.HttpURLConnection conn = (java.net.HttpURLConnection)
new java.net.URL("http://localhost:" + PORT + "/api/exit-parent").openConnection();
conn.setRequestMethod("POST");
conn.setConnectTimeout(1000);
conn.getResponseCode();
conn.disconnect();
} catch (Exception ignored) {}
if (server != null) server.stop(0);
Platform.exit();
});
} catch (Exception e) {
log("Ошибка: " + e.getMessage());
log("Error: " + e.getMessage());
e.printStackTrace();
throw new RuntimeException(e);
}
@@ -351,6 +351,9 @@ public class JFXLauncher extends Application {
server.createContext("/api/mc-versions", this::handleMCVersions);
server.createContext("/api/loader-versions", this::handleLoaderVersions);
server.createContext("/api/packs", this::handlePacks);
server.createContext("/api/settings", this::handleSettings);
server.createContext("/api/activate-pass", this::handleActivatePass);
server.createContext("/api/register", this::handleRegister);
server.createContext("/api/shutdown", this::handleShutdown);
server.createContext("/api/exit", this::handleExit);
server.createContext("/api/exit-parent", this::handleExitParent);
@@ -359,7 +362,7 @@ public class JFXLauncher extends Application {
server.setExecutor(Executors.newCachedThreadPool());
server.start();
log("HTTP сервер на порту " + PORT);
log("HTTP server on port " + PORT);
}
private void stopServer() {
@@ -369,7 +372,7 @@ public class JFXLauncher extends Application {
private void handleLogin(HttpExchange exchange) {
try {
if (!"POST".equals(exchange.getRequestMethod())) {
sendJson(exchange, Map.of("success", false, "error", "Метод не поддерживается"));
sendJson(exchange, Map.of("success", false, "error", "Method not supported"));
return;
}
@@ -383,7 +386,7 @@ public class JFXLauncher extends Application {
data.put("username", result.getData().getUsername());
data.put("token", result.getData().getToken());
sendJson(exchange, Map.of("success", true, "data", data));
log("Вход: " + username);
log("Login: " + username);
} else {
sendJson(exchange, Map.of("success", false, "error", result.getError()));
}
@@ -395,7 +398,7 @@ public class JFXLauncher extends Application {
private void handleAccount(HttpExchange exchange) {
try {
if (!api.isLoggedIn()) {
sendJson(exchange, Map.of("success", false, "error", "Не авторизован"));
sendJson(exchange, Map.of("success", false, "error", "Not authenticated"));
return;
}
Map<String, Object> data = new HashMap<>();
@@ -418,7 +421,7 @@ public class JFXLauncher extends Application {
data.put("role", AuthManager.getRole());
data.put("roleName", AuthManager.getRoleName());
sendJson(exchange, Map.of("success", true, "data", data, "autoLogin", true));
log("Автологин выполнен: " + AuthManager.getUsername());
log("Auto-login performed: " + AuthManager.getUsername());
} else {
sendJson(exchange, Map.of("success", false, "autoLogin", false));
}
@@ -446,20 +449,30 @@ public class JFXLauncher extends Application {
private void handleLaunch(HttpExchange exchange) {
try {
if (!api.isLoggedIn()) {
sendJson(exchange, Map.of("success", false, "error", "Не авторизован"));
sendJson(exchange, Map.of("success", false, "error", "Not authenticated"));
return;
}
Map<String, String> body = parseJson(exchange.getRequestBody());
String name = body.get("name");
Instance instance = InstanceManager.getInstance(name);
if (instance == null) {
sendJson(exchange, Map.of("success", false, "error", "Pack not found"));
return;
}
if (instance.isServerPack() && !AuthManager.hasActivePass()) {
sendJson(exchange, Map.of("success", false, "error", "Server pack requires an active pass"));
return;
}
var result = api.launch(name);
if (result.isSuccess()) {
Map<String, Object> data = new HashMap<>();
data.put("pid", result.getData().getPid());
data.put("status", result.getData().getStatus());
sendJson(exchange, Map.of("success", true, "data", data));
log("Запущено: " + name);
log("Launched: " + name);
} else {
sendJson(exchange, Map.of("success", false, "error", result.getError()));
}
@@ -470,13 +483,13 @@ public class JFXLauncher extends Application {
private void handleInstall(HttpExchange exchange) {
if (installInProgress) {
sendJson(exchange, Map.of("success", false, "error", "Установка уже выполняется"));
sendJson(exchange, Map.of("success", false, "error", "Installation already in progress"));
return;
}
try {
if (!api.isLoggedIn()) {
sendJson(exchange, Map.of("success", false, "error", "Не авторизован"));
sendJson(exchange, Map.of("success", false, "error", "Not authenticated"));
return;
}
@@ -486,7 +499,7 @@ public class JFXLauncher extends Application {
String loader = body.get("loader");
String loaderVersion = body.get("loaderVersion");
log("Установка: " + name + " " + version + " " + loader + (loaderVersion != null ? " " + loaderVersion : ""));
log("Install: " + name + " " + version + " " + loader + (loaderVersion != null ? " " + loaderVersion : ""));
var createResult = api.instances().createInstance(name);
if (!createResult.isSuccess()) {
@@ -502,10 +515,10 @@ public class JFXLauncher extends Application {
instance.setLoaderVersion(loaderVersion);
}
sendJson(exchange, Map.of("success", true, "message", "Установка началась"));
sendJson(exchange, Map.of("success", true, "message", "Installation started"));
setInstallInProgress(true);
setInstallProgress("Подготовка...", 0, 100);
setInstallProgress("Preparing...", 0, 100);
Thread installThread = new Thread(() -> {
try {
@@ -520,12 +533,12 @@ public class JFXLauncher extends Application {
setInstallInProgress(false);
if (success) {
log("Установлено: " + name);
log("Installed: " + name);
} else {
log("Ошибка установки: " + name);
log("Install error: " + name);
}
} catch (Exception e) {
log("Ошибка установки: " + e.getMessage());
log("Install error: " + e.getMessage());
setInstallInProgress(false);
}
});
@@ -535,7 +548,7 @@ public class JFXLauncher extends Application {
sendJson(exchange, Map.of("success", false, "error", "Instance not found"));
}
} catch (Exception e) {
log("Ошибка установки: " + e.getMessage());
log("Install error: " + e.getMessage());
setInstallInProgress(false);
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
}
@@ -684,6 +697,90 @@ public class JFXLauncher extends Application {
}
}
private void handleActivatePass(HttpExchange exchange) {
try {
if (!api.isLoggedIn()) {
sendJson(exchange, Map.of("success", false, "error", "Not authenticated"));
return;
}
Map<String, String> body = parseJson(exchange.getRequestBody());
String code = body.get("code");
if (code == null || code.isEmpty()) {
sendJson(exchange, Map.of("success", false, "error", "Enter pass code"));
return;
}
var result = api.activatePass(code);
if (result.isSuccess()) {
sendJson(exchange, Map.of("success", true, "message", "Pass activated!"));
log("Pass activated");
} else {
sendJson(exchange, Map.of("success", false, "error", result.getError()));
}
} catch (Exception e) {
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
}
}
private void handleRegister(HttpExchange exchange) {
try {
Map<String, String> body = parseJson(exchange.getRequestBody());
String username = body.get("username");
String password = body.get("password");
if (username == null || password == null || username.isEmpty() || password.isEmpty()) {
sendJson(exchange, Map.of("success", false, "error", "Fill all fields"));
return;
}
var result = api.register(username, password);
if (result.isSuccess()) {
Map<String, Object> data = new HashMap<>();
data.put("username", result.getData().getUsername());
data.put("token", result.getData().getToken());
sendJson(exchange, Map.of("success", true, "data", data));
log("Registered: " + username);
} else {
sendJson(exchange, Map.of("success", false, "error", result.getError()));
}
} catch (Exception e) {
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
}
}
private void handleSettings(HttpExchange exchange) {
try {
if ("POST".equals(exchange.getRequestMethod())) {
Map<String, String> body = parseJson(exchange.getRequestBody());
if (body.containsKey("maxMemory")) {
Config.setMaxMemory(Integer.parseInt(body.get("maxMemory")));
}
if (body.containsKey("windowWidth")) {
Config.setWindowWidth(Integer.parseInt(body.get("windowWidth")));
}
if (body.containsKey("windowHeight")) {
Config.setWindowHeight(Integer.parseInt(body.get("windowHeight")));
}
if (body.containsKey("extraJvmArgs")) {
Config.setExtraJvmArgs(body.get("extraJvmArgs"));
}
if (body.containsKey("javaPath")) {
Config.setJavaPath(body.get("javaPath"));
}
sendJson(exchange, Map.of("success", true));
return;
}
Map<String, Object> data = new HashMap<>();
data.put("maxMemory", Config.getMaxMemory());
data.put("serverUrl", Config.getServerUrl());
data.put("instancesDir", Config.getInstancesDir().toString());
data.put("windowWidth", Config.getWindowWidth());
data.put("windowHeight", Config.getWindowHeight());
data.put("extraJvmArgs", Config.getExtraJvmArgs());
data.put("javaPath", Config.getJavaPath());
sendJson(exchange, Map.of("success", true, "data", data));
} catch (Exception e) {
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
}
}
private Map<String, String> parseQuery(String query) {
Map<String, String> params = new HashMap<>();
if (query != null && !query.isEmpty()) {
@@ -700,13 +797,13 @@ public class JFXLauncher extends Application {
private void handleShutdown(HttpExchange exchange) {
log("Shutdown request received...");
LaunchService.killAllProcesses();
if (server != null) server.stop(1);
if (server != null) server.stop(0);
Platform.exit();
System.exit(0);
}
private void handleExit(HttpExchange exchange) {
log("Выход...");
log("Exiting...");
LaunchService.killAllProcesses();
if (mainStage != null) mainStage.close();
Platform.exit();
@@ -714,7 +811,7 @@ public class JFXLauncher extends Application {
}
private void handleExitParent(HttpExchange exchange) {
log("Завершение родительского процесса...");
log("Terminating parent process...");
LaunchService.killAllProcesses();
if (mainStage != null) mainStage.close();
Platform.exit();
@@ -782,6 +879,12 @@ public class JFXLauncher extends Application {
launcherLogBuffer.append(entry);
}
System.out.println("[JFX] " + msg);
if (launcherLogFile != null) {
try {
Files.writeString(launcherLogFile, entry,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (Exception ignored) {}
}
for (LogConsumer consumer : logConsumers) {
try { consumer.onLog(entry); } catch (Exception ignored) {}
}
@@ -14,10 +14,13 @@ public class Config {
private static final Properties props = new Properties();
// Настройки
private static int maxMemory = 4096; // будет перезаписано умной логикой
private static int maxMemory = 4096;
private static String serverUrl = "http://87.120.187.36:1582";
private static String lastUsername = "Player";
private static int windowWidth = 1280;
private static int windowHeight = 720;
private static String extraJvmArgs = "";
private static String javaPath = "java";
static {
load();
@@ -36,9 +39,13 @@ public class Config {
maxMemory = Integer.parseInt(props.getProperty("maxMemory", "4096"));
serverUrl = props.getProperty("serverUrl", serverUrl);
lastUsername = props.getProperty("lastUsername", lastUsername);
windowWidth = Integer.parseInt(props.getProperty("windowWidth", "1280"));
windowHeight = Integer.parseInt(props.getProperty("windowHeight", "720"));
extraJvmArgs = props.getProperty("extraJvmArgs", "");
javaPath = props.getProperty("javaPath", "java");
} catch (Exception e) {
System.err.println(ZAnsi.brightRed("Не удалось загрузить конфиг: ") + e.getMessage());
System.err.println(ZAnsi.brightRed("Failed to load config: ") + e.getMessage());
}
}
@@ -47,40 +54,34 @@ public class Config {
props.setProperty("maxMemory", String.valueOf(maxMemory));
props.setProperty("serverUrl", serverUrl);
props.setProperty("lastUsername", lastUsername);
props.setProperty("windowWidth", String.valueOf(windowWidth));
props.setProperty("windowHeight", String.valueOf(windowHeight));
props.setProperty("extraJvmArgs", extraJvmArgs);
props.setProperty("javaPath", javaPath);
try (var os = Files.newOutputStream(CONFIG_FILE)) {
props.store(os, "ZernMC Launcher Configuration");
}
} catch (IOException e) {
System.err.println(ZAnsi.brightRed("Не удалось сохранить конфиг: ") + e.getMessage());
System.err.println(ZAnsi.brightRed("Failed to save config: ") + e.getMessage());
}
}
/**
* Умная рекомендация RAM:
* - минимум 1.5 GB
* - рекомендуется totalRAM - 30%
* - максимум 70% от доступной RAM
*/
private static void applySmartRamRecommendation() {
long totalRamMB = Runtime.getRuntime().maxMemory() / (1024 * 1024); // в MB
long totalRamMB = Runtime.getRuntime().maxMemory() / (1024 * 1024);
// Рекомендуемое значение = total - 30%
long recommended = (long) (totalRamMB * 0.70); // 70% от доступной
long recommended = (long) (totalRamMB * 0.70);
// Ограничения
recommended = Math.max(1536, recommended); // минимум 1.5 GB
recommended = Math.min(recommended, totalRamMB - 1024); // оставляем минимум 1 GB системе
recommended = Math.max(1536, recommended);
recommended = Math.min(recommended, totalRamMB - 1024);
// Если текущее значение сильно отличается от рекомендуемого корректируем
if (Math.abs(maxMemory - recommended) > 1024) { // разница больше 1 GB
if (Math.abs(maxMemory - recommended) > 1024) {
maxMemory = (int) recommended;
save(); // сохраняем умную рекомендацию
System.out.println(ZAnsi.cyan("Автоматически рекомендовано RAM: " + maxMemory + " MB"));
save();
System.out.println(ZAnsi.cyan("Auto-recommended RAM: " + maxMemory + " MB"));
}
}
// Getters & Setters
public static int getMaxMemory() {
return maxMemory;
}
@@ -94,7 +95,6 @@ public class Config {
}
public static void setMaxMemory(int memory) {
// Защита от слишком маленьких/больших значений
if (memory < 1024) memory = 1536;
if (memory > 32768) memory = 32768;
@@ -127,11 +127,44 @@ public class Config {
return CONFIG_DIR;
}
/**
* Полезная информация для пользователя
*/
public static int getWindowWidth() {
return windowWidth;
}
public static void setWindowWidth(int width) {
windowWidth = Math.max(640, width);
save();
}
public static int getWindowHeight() {
return windowHeight;
}
public static void setWindowHeight(int height) {
windowHeight = Math.max(480, height);
save();
}
public static String getExtraJvmArgs() {
return extraJvmArgs;
}
public static void setExtraJvmArgs(String args) {
extraJvmArgs = args != null ? args : "";
save();
}
public static String getJavaPath() {
return javaPath;
}
public static void setJavaPath(String path) {
javaPath = path != null && !path.isEmpty() ? path : "java";
save();
}
public static String getRamInfo() {
long totalMB = Runtime.getRuntime().maxMemory() / (1024 * 1024);
return "Доступно RAM: " + totalMB + " MB | Рекомендуется: " + maxMemory + " MB";
return "Available RAM: " + totalMB + " MB | Recommended: " + maxMemory + " MB";
}
}
}
@@ -10,10 +10,9 @@ public class ConsoleUtils {
}
public static void pause() {
System.out.print(ZAnsi.white("\nНажмите Enter для продолжения..."));
System.out.print(ZAnsi.white("\nPress Enter to continue..."));
try {
System.in.read();
// Очищаем буфер ввода
while (System.in.available() > 0) {
System.in.read();
}
@@ -36,4 +35,4 @@ public class ConsoleUtils {
public static void separator() {
System.out.println(ZAnsi.white("────────────────────────────────────────────────────────────"));
}
}
}
@@ -7,9 +7,6 @@ import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Scanner;
/**
* Улучшенный Input с поддержкой кириллицы и confirm через ArrowMenu
*/
public class Input {
private static final Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8);
@@ -30,7 +27,7 @@ public class Input {
System.out.print(prompt);
return Integer.parseInt(scanner.nextLine().trim());
} catch (NumberFormatException e) {
System.out.println(ZAnsi.brightRed("Некорректное число. Попробуйте ещё раз."));
System.out.println(ZAnsi.brightRed("Invalid number. Try again."));
}
}
}
@@ -41,20 +38,16 @@ public class Input {
if (value >= min && value <= max) {
return value;
}
System.out.println(ZAnsi.brightRed("Значение должно быть от " + min + " до " + max + "."));
System.out.println(ZAnsi.brightRed("Value must be between " + min + " and " + max + "."));
}
}
/**
* Новый confirm через ArrowMenu
* @throws IOException
*/
public static boolean confirm(String question) throws IOException {
ConsoleUtils.clearScreen();
List<String> options = List.of(
"Да",
"Нет"
"Yes",
"No"
);
ArrowMenu menu = new ArrowMenu(question, options);
@@ -63,28 +56,17 @@ public class Input {
return choice == 0;
}
/**
* Альтернативный confirm без очистки экрана
* @throws IOException
*/
public static boolean confirmInline(String question) throws IOException {
List<String> options = List.of("Да", "Нет");
List<String> options = List.of("Yes", "No");
ArrowMenu menu = new ArrowMenu(question, options);
int choice = menu.show();
return choice == 0;
}
/**
* Закрытие сканнера (вызывать при выходе из программы, если нужно)
*/
public static void close() {
scanner.close();
}
/**
* Очищает буфер ввода от оставшихся символов
*/
public static void flushInput() {
try {
while (System.in.available() > 0) {
@@ -93,4 +75,4 @@ public class Input {
} catch (IOException e) {
}
}
}
}
@@ -10,9 +10,6 @@ public class ProgressBar {
private static String currentLabel = "";
private static long currentTotal = 0;
/**
* Прогресс по количеству файлов (для библиотек и общего прогресса)
*/
public static void show(String label, long current, long total, String unit) {
currentLabel = label;
currentTotal = total;
@@ -39,9 +36,6 @@ public class ProgressBar {
System.out.flush();
}
/**
* Прогресс по байтам для одного файла (реальный прогресс)
*/
public static void showDownload(String label, long downloaded, long totalBytes) {
currentLabel = label;
currentTotal = totalBytes;
@@ -99,7 +93,7 @@ public class ProgressBar {
setInProgress.invoke(null, false);
} catch (Exception ignored) {}
System.out.println("\r" + ZAnsi.brightGreen(message + " завершено"));
System.out.println("\r" + ZAnsi.brightGreen(message + " done"));
System.out.flush();
}
@@ -113,4 +107,4 @@ public class ProgressBar {
if (bytes < 1024 * 1024) return DF.format(bytes / 1024.0) + " KB";
return DF.format(bytes / (1024.0 * 1024)) + " MB";
}
}
}
@@ -29,14 +29,9 @@ public class ZHttpClient {
private static String BASE_URL = "http://87.120.187.36:1582";
// Глобальный прокси режим (для обратной совместимости)
private static final AtomicBoolean useProxyMode = new AtomicBoolean(false);
private static final AtomicBoolean proxyTested = new AtomicBoolean(false);
/**
* Переопределить URL сервера (для тестов).
* Внимание: не потокобезопасно, использовать только в тестах.
*/
public static void setBaseUrl(String url) {
BASE_URL = url;
}
@@ -45,7 +40,6 @@ public class ZHttpClient {
return BASE_URL;
}
// Умное проксирование по сервисам
public enum ServiceType {
ZERN_SERVER("http://87.120.187.36:1582", true),
FABRIC_META("https://meta.fabricmc.net", false),
@@ -69,17 +63,15 @@ public class ZHttpClient {
public boolean isAlwaysDirect() { return alwaysDirect; }
}
// Статусы сервисов
private static final Map<ServiceType, Boolean> serviceProxyMode = new ConcurrentHashMap<>();
private static final Map<ServiceType, Integer> serviceFailCount = new ConcurrentHashMap<>();
private static final Map<ServiceType, Long> serviceLastCheckTime = new ConcurrentHashMap<>();
private static final Map<ServiceType, Boolean> serviceHealthy = new ConcurrentHashMap<>();
private static final int MAX_FAILS_BEFORE_PROXY = 2;
private static final long HEALTH_CHECK_INTERVAL_MS = 60000; // 1 минута
private static final long CHECK_TIMEOUT_MS = 7000; // 7 секунд на проверку
private static final long HEALTH_CHECK_INTERVAL_MS = 60000;
private static final long CHECK_TIMEOUT_MS = 7000;
// Статистика
private static int directSuccessCount = 0;
private static int proxySuccessCount = 0;
private static int directFailCount = 0;
@@ -92,9 +84,6 @@ public class ZHttpClient {
}
}
/**
* Вызывать один раз при запуске лаунчера
*/
public static void checkAllServicesOnStartup() {
checkAllServicesOnStartup(false);
}
@@ -121,16 +110,16 @@ public class ZHttpClient {
if (verbose) {
System.out.println(isHealthy ?
ZAnsi.green(" " + service.name() + " - OK") :
ZAnsi.red(" " + service.name() + " - НЕ ДОСТУПЕН (критично!)"));
ZAnsi.red(" " + service.name() + " - NOT ACCESSIBLE (critical!)"));
}
} else {
if (isHealthy) {
if (verbose) {
System.out.println(ZAnsi.green(" " + service.name() + " - прямое подключение работает"));
System.out.println(ZAnsi.green(" " + service.name() + " - direct connection works"));
}
} else {
if (verbose) {
System.out.println(ZAnsi.yellow(" " + service.name() + " - НЕ ДОСТУПЕН, будет использован прокси"));
System.out.println(ZAnsi.yellow(" " + service.name() + " - NOT ACCESSIBLE, proxy will be used"));
}
serviceProxyMode.put(service, true);
serviceFailCount.put(service, MAX_FAILS_BEFORE_PROXY);
@@ -140,7 +129,7 @@ public class ZHttpClient {
if (!serviceHealthy.get(ServiceType.ZERN_SERVER)) {
if (verbose) {
System.out.println(ZAnsi.brightRed("Критическая ошибка: Zern сервер недоступен!"));
System.out.println(ZAnsi.brightRed("Critical error: Zern server is unreachable!"));
}
}
@@ -151,22 +140,19 @@ public class ZHttpClient {
}
}
/**
* Принудительная проверка Mojang-сервисов (рекомендуется вызывать перед установкой сборки)
*/
public static void forceCheckMojangServices() {
System.out.println(ZAnsi.cyan("Принудительная проверка Mojang сервисов..."));
System.out.println(ZAnsi.cyan("Forcing Mojang services check..."));
for (ServiceType service : List.of(ServiceType.MOJANG_META, ServiceType.MOJANG_RESOURCES)) {
boolean healthy = checkServiceHealth(service);
serviceHealthy.put(service, healthy);
if (healthy) {
System.out.println(ZAnsi.green(" " + service.name() + " доступен напрямую"));
System.out.println(ZAnsi.green(" " + service.name() + " accessible directly"));
serviceProxyMode.put(service, false);
serviceFailCount.put(service, 0);
} else {
System.out.println(ZAnsi.yellow(" " + service.name() + " недоступен → прокси режим активирован"));
System.out.println(ZAnsi.yellow(" " + service.name() + " not accessible -> proxy mode activated"));
serviceProxyMode.put(service, true);
serviceFailCount.put(service, MAX_FAILS_BEFORE_PROXY);
}
@@ -177,9 +163,6 @@ public class ZHttpClient {
return checkDirectConnection(service.getBaseUrl());
}
/**
* Улучшенная проверка прямого подключения
*/
private static boolean checkDirectConnection(String baseUrl) {
String testUrl = baseUrl;
@@ -199,7 +182,7 @@ public class ZHttpClient {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
int code = response.statusCode();
return code == 200 || code == 404; // 404 для ресурсов нормально
return code == 200 || code == 404;
} catch (Exception e) {
return false;
}
@@ -230,7 +213,7 @@ public class ZHttpClient {
if (isHealthy && serviceProxyMode.get(service)) {
serviceProxyMode.put(service, false);
serviceFailCount.put(service, 0);
System.out.println(ZAnsi.green("[NET] " + service.name() + " восстановлен, переключен на прямое подключение"));
System.out.println(ZAnsi.green("[NET] " + service.name() + " restored, switched to direct connection"));
} else if (!isHealthy && !serviceProxyMode.get(service)) {
int fails = serviceFailCount.getOrDefault(service, 0) + 1;
serviceFailCount.put(service, fails);
@@ -238,7 +221,7 @@ public class ZHttpClient {
if (fails >= MAX_FAILS_BEFORE_PROXY) {
serviceProxyMode.put(service, true);
System.out.println(ZAnsi.yellow("[NET] " + service.name() + " недоступен, включен прокси режим"));
System.out.println(ZAnsi.yellow("[NET] " + service.name() + " unavailable, proxy mode enabled"));
}
}
}
@@ -289,14 +272,11 @@ public class ZHttpClient {
if (fails >= MAX_FAILS_BEFORE_PROXY && !serviceProxyMode.get(service)) {
serviceProxyMode.put(service, true);
System.out.println(ZAnsi.yellow("[NET] " + service.name() + " заблокирован, переключаемся на прокси"));
System.out.println(ZAnsi.yellow("[NET] " + service.name() + " blocked, switching to proxy"));
}
}
/**
* Универсальный GET с умным прокси + автоматическим fallback
*/
public static String getWithSmartProxy(String url) throws IOException, InterruptedException {
// Попытка прямого подключения
if (!shouldUseProxyForUrl(url)) {
try {
HttpRequest request = HttpRequest.newBuilder()
@@ -321,11 +301,9 @@ public class ZHttpClient {
directFailCount++;
markServiceAsBlocked(url);
}
// Если ошибка соединения пробуем через прокси
}
}
// Через прокси
try {
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
String proxyUrl = BASE_URL + "/download?url=" + encodedUrl;
@@ -347,13 +325,10 @@ public class ZHttpClient {
return response.body();
} catch (Exception e) {
throw new IOException("Не удалось получить данные ни напрямую, ни через прокси: " + e.getMessage(), e);
throw new IOException("Failed to fetch data directly or via proxy: " + e.getMessage(), e);
}
}
/**
* Скачивание файла с умным прокси + fallback
*/
public static void downloadFileWithSmartProxy(String url, Path target) throws Exception {
if (!shouldUseProxyForUrl(url)) {
try {
@@ -375,11 +350,9 @@ public class ZHttpClient {
directFailCount++;
markServiceAsBlocked(url);
}
// fallback на прокси ниже
}
}
// Скачивание через прокси
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
String proxyUrl = BASE_URL + "/proxy/download?url=" + encodedUrl;
@@ -399,8 +372,6 @@ public class ZHttpClient {
proxySuccessCount++;
}
// ====================== СТАРЫЕ МЕТОДЫ (обновлённые) ======================
public static String get(String endpoint) throws IOException, InterruptedException {
checkAllServicesOnStartup();
@@ -415,7 +386,6 @@ public class ZHttpClient {
.header("User-Agent", "ZernMC-Launcher/1.0")
.GET();
// ===== ДОБАВИТЬ ТОКЕН АВТОРИЗАЦИИ =====
String accessToken = AuthManager.getAccessToken();
if (accessToken != null && !accessToken.equals("0")) {
requestBuilder.header("Authorization", "Bearer " + accessToken);
@@ -442,7 +412,6 @@ public class ZHttpClient {
.header("User-Agent", "ZernMC-Launcher/1.0")
.GET();
// ===== ДОБАВИТЬ ТОКЕН АВТОРИЗАЦИИ =====
String accessToken = AuthManager.getAccessToken();
if (accessToken != null && !accessToken.equals("0")) {
requestBuilder.header("Authorization", "Bearer " + accessToken);
@@ -458,12 +427,10 @@ public class ZHttpClient {
proxySuccessCount++;
return response.body();
} catch (Exception e) {
throw new IOException("Ошибка прокси: " + e.getMessage(), e);
throw new IOException("Proxy error: " + e.getMessage(), e);
}
}
// ====================== МЕТОДЫ ДЛЯ EXTERNAL РЕСУРСОВ ======================
public static List<String> getFabricLoaderVersions() throws IOException, InterruptedException {
String url = "https://meta.fabricmc.net/v2/versions/loader";
return parseFabricVersionsFromJson(getWithSmartProxy(url));
@@ -518,15 +485,13 @@ public class ZHttpClient {
return versions;
}
// ====================== ВСПОМОГАТЕЛЬНЫЕ ======================
public static String getLauncherVersionInfo() throws IOException, InterruptedException {
return get("/launcher/version");
}
public static void forceProxyMode() {
useProxyMode.set(true);
System.out.println(ZAnsi.yellow("Принудительно включен глобальный прокси режим"));
System.out.println(ZAnsi.yellow("Global proxy mode forced on"));
}
public static void disableProxyMode() {
@@ -537,7 +502,7 @@ public class ZHttpClient {
serviceFailCount.put(type, 0);
}
}
System.out.println(ZAnsi.green("Режим прокси выключен"));
System.out.println(ZAnsi.green("Proxy mode disabled"));
}
public static boolean isProxyMode() {
@@ -545,18 +510,18 @@ public class ZHttpClient {
}
public static void printStats() {
System.out.println(ZAnsi.cyan("\n=== Статистика сети ==="));
System.out.println(ZAnsi.white("Глобальный прокси: ") + (useProxyMode.get() ? "ВКЛ" : "ВЫКЛ"));
System.out.println(ZAnsi.white("Прямых успехов: ") + directSuccessCount);
System.out.println(ZAnsi.white("Прямых неудач: ") + directFailCount);
System.out.println(ZAnsi.white("Прокси успехов: ") + proxySuccessCount);
System.out.println(ZAnsi.cyan("\n=== Network Stats ==="));
System.out.println(ZAnsi.white("Global proxy: ") + (useProxyMode.get() ? "ON" : "OFF"));
System.out.println(ZAnsi.white("Direct successes: ") + directSuccessCount);
System.out.println(ZAnsi.white("Direct failures: ") + directFailCount);
System.out.println(ZAnsi.white("Proxy successes: ") + proxySuccessCount);
System.out.println(ZAnsi.cyan("\nСтатус сервисов:"));
System.out.println(ZAnsi.cyan("\nService status:"));
for (ServiceType type : ServiceType.values()) {
if (type.isAlwaysDirect()) continue;
String status = serviceProxyMode.get(type) ? ZAnsi.red("ПРОКСИ") : ZAnsi.green("ПРЯМО");
String status = serviceProxyMode.get(type) ? ZAnsi.red("PROXY") : ZAnsi.green("DIRECT");
String health = serviceHealthy.get(type) ? ZAnsi.green("[+]") : ZAnsi.red("[-]");
System.out.println(ZAnsi.white(" " + type.name() + ": ") + status + " " + health);
}
}
}
}
+313 -173
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="ru">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -7,222 +7,362 @@
<link rel="stylesheet" href="style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
</head>
<body>
<canvas id="grid-canvas"></canvas>
<canvas id="bg-canvas"></canvas>
<div id="app">
<!-- Login Screen -->
<div id="login-screen" class="screen hidden">
<div id="login-screen" class="screen">
<div class="login-container">
<div class="logo-section">
<div class="logo-placeholder">
<svg width="80" height="80" viewBox="0 0 80 80" fill="none">
<rect width="80" height="80" rx="20" fill="#e94560"/>
<path d="M25 40 L40 25 L55 40 L40 55 Z" fill="white"/>
<div class="login-brand">
<div class="brand-icon">
<svg width="56" height="56" viewBox="0 0 56 56" fill="none">
<rect width="56" height="56" rx="14" fill="url(#brandGrad)"/>
<path d="M18 28 L28 18 L38 28 L28 38 Z" fill="white" opacity="0.9"/>
<defs>
<linearGradient id="brandGrad" x1="0" y1="0" x2="56" y2="56">
<stop offset="0%" stop-color="#e94560"/>
<stop offset="100%" stop-color="#ff6b6b"/>
</linearGradient>
</defs>
</svg>
</div>
<h1 class="app-title">ZernMC Launcher</h1>
<p class="app-version">v<span id="version">1.0.9</span></p>
<h1 class="brand-title">ZernMC</h1>
<p class="brand-sub">Launcher <span id="version">1.0.9</span></p>
</div>
<form id="login-form" class="login-form">
<div class="input-group">
<input type="text" id="username" name="username" placeholder="Имя пользователя" required autocomplete="username">
<div class="field">
<input type="text" id="username" placeholder="Username" autocomplete="username" required>
<label for="username">Username</label>
</div>
<div class="input-group">
<input type="password" id="password" name="password" placeholder="Пароль" required autocomplete="current-password">
<div class="field">
<input type="password" id="password" placeholder="Password" autocomplete="current-password" required>
<label for="password">Password</label>
</div>
<button type="submit" class="btn-primary">
<span class="btn-text">Войти</span>
<div class="btn-loader hidden"></div>
<p id="login-error" class="error-msg hidden"></p>
<button type="submit" class="btn-primary" id="login-btn">
<span class="btn-label">Sign In</span>
<div class="spinner hidden"></div>
</button>
<p id="login-error" class="error-message hidden"></p>
<p class="login-hint">New account will be created automatically on first login</p>
</form>
</div>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="overlay hidden">
<div class="loader-ring"></div>
<p class="loader-text">Loading...</p>
</div>
<!-- Main Screen -->
<div id="main-screen" class="screen hidden">
<div class="main-layout">
<!-- Left Sidebar -->
<div class="shell">
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo-small">
<svg width="40" height="40" viewBox="0 0 40 40" fill="none">
<rect width="40" height="40" rx="10" fill="#e94560"/>
<path d="M12 20 L20 12 L28 20 L20 28 Z" fill="white"/>
<div class="sidebar-top">
<div class="sidebar-brand">
<svg width="32" height="32" viewBox="0 0 56 56" fill="none">
<rect width="56" height="56" rx="14" fill="url(#brandGrad2)"/>
<path d="M18 28 L28 18 L38 28 L28 38 Z" fill="white" opacity="0.9"/>
<defs>
<linearGradient id="brandGrad2" x1="0" y1="0" x2="56" y2="56">
<stop offset="0%" stop-color="#e94560"/>
<stop offset="100%" stop-color="#ff6b6b"/>
</linearGradient>
</defs>
</svg>
</div>
<div class="header-info">
<h1 class="header-title">ZernMC</h1>
<span class="header-version">v<span id="header-version">1.0.9</span></span>
</div>
</div>
<div class="sidebar-content">
<div class="instances-section">
<h3 class="section-label">ZernMC сборки</h3>
<div id="zernmc-instances-list" class="instances-list">
<!-- ZernMC instances -->
<div class="sidebar-brand-text">
<span class="sidebar-brand-name">ZernMC</span>
<span class="sidebar-brand-ver">v<span id="header-version">1.0.9</span></span>
</div>
</div>
<div class="instances-section">
<h3 class="section-label">Локальные сборки</h3>
<div id="local-instances-list" class="instances-list">
<!-- Local instances -->
<nav class="sidebar-nav">
<button class="nav-btn active" data-view="packs" title="Packs">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>
Packs
</button>
<button class="nav-btn" data-view="news" title="News">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9h4"/><path d="M18 14h-8"/><path d="M15 18h-5"/><path d="M10 6h8v4h-8V6Z"/></svg>
News
</button>
<button class="nav-btn" data-view="settings" title="Settings">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
Settings
</button>
</nav>
<div class="sidebar-section">
<div class="section-header">
<span class="section-title">Server Packs</span>
</div>
<div id="server-packs-list" class="pack-list"></div>
</div>
<button id="download-btn" class="btn-download">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
Скачать сборку
</button>
<div class="sidebar-section" id="local-packs-section">
<div class="section-header">
<span class="section-title">Local Packs</span>
<button class="btn-icon" id="add-pack-btn" title="Add pack">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
</div>
<div id="local-packs-list" class="pack-list"></div>
</div>
</div>
<div class="sidebar-footer">
<span class="username-display" id="username-display"></span>
<div class="account-badges">
<span id="account-status" class="badge"></span>
<span id="account-role" class="badge role-badge"></span>
<div class="sidebar-bottom">
<div class="user-card">
<div class="user-avatar" id="user-avatar">Z</div>
<div class="user-info">
<span class="user-name" id="username-display">Player</span>
<span class="user-badges">
<span id="account-status" class="badge badge-free">FREE</span>
<span id="account-role" class="badge badge-role hidden"></span>
</span>
</div>
<button class="btn-icon" id="logout-btn" title="Log out">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
</button>
</div>
<button class="btn-logout" id="logout-btn" title="Выйти">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
</button>
<button class="btn-logout" id="close-btn" title="Закрыть" onclick="app.shutdownLauncher()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<div class="logs-section">
<div class="logs-header">
<h2>Логи</h2>
<button class="btn-clear-logs" id="clear-logs">Очистить</button>
<main class="content">
<!-- Packs View -->
<div id="view-packs" class="view active">
<div class="view-header">
<div>
<h2 class="view-title" id="selected-pack-title">Select a pack</h2>
<p class="view-subtitle" id="selected-pack-meta">Choose a pack from the sidebar to get started</p>
</div>
<div class="view-actions">
<button id="update-btn" class="btn-secondary hidden">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
Update
</button>
<button id="delete-pack-btn" class="btn-secondary btn-danger hidden">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
Delete
</button>
</div>
</div>
<div id="logs-container" class="logs-container">
<div class="log-entry info">Ожидание запуска...</div>
<div class="pack-detail" id="pack-detail">
<div class="pack-empty" id="pack-empty-state">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" opacity="0.2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
<h3>No pack selected</h3>
<p>Select a pack from the sidebar or add a new one</p>
</div>
<div id="pack-detail-content" class="pack-detail-content hidden">
<div class="pack-hero">
<div class="pack-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg>
</div>
<div>
<h3 id="detail-name" class="detail-name">pack</h3>
<div class="detail-tags">
<span class="tag tag-mc" id="detail-mc">1.21</span>
<span class="tag tag-loader" id="detail-loader">fabric</span>
<span class="tag tag-server hidden" id="detail-server">v1</span>
</div>
</div>
</div>
<div class="pack-stats">
<div class="stat"><span class="stat-value" id="detail-loader-ver">-</span><span class="stat-label">Loader Ver</span></div>
<div class="stat"><span class="stat-value" id="detail-files">0</span><span class="stat-label">Files</span></div>
<div class="stat"><span class="stat-value" id="detail-size">-</span><span class="stat-label">Size</span></div>
</div>
<div id="pack-description" class="pack-description">
<p id="pack-description-text" class="pack-description-text">Loading description...</p>
<div id="pack-gallery" class="pack-gallery">
</div>
</div>
</div>
</div>
<div class="play-bar" id="play-bar">
<div class="play-bar-info">
<span id="play-bar-name">Select a pack</span>
</div>
<button id="play-btn" class="btn-play" disabled>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Play
</button>
</div>
</div>
<!-- News View -->
<div id="view-news" class="view">
<div class="view-header">
<h2 class="view-title">News</h2>
</div>
<div class="news-grid">
<article class="news-card news-placeholder">
<div class="news-card-badge">Coming Soon</div>
<h3>ZernMC Server Updates</h3>
<p>News and announcements will appear here. Stay tuned for the latest updates about the server and launcher.</p>
<time>Soon</time>
</article>
<article class="news-card news-placeholder">
<div class="news-card-badge">Info</div>
<h3>Launcher v1.0.9</h3>
<p>English UI, JavaFX redesign, improved pack management, and more. Check the GitHub for the full changelog.</p>
<time>v1.0.9</time>
</article>
<article class="news-card news-placeholder">
<div class="news-card-badge">Guide</div>
<h3>Getting Started</h3>
<p>Install a pack, activate your pass via the website, and start playing. Need help? Contact a moderator.</p>
<time>Guide</time>
</article>
</div>
</div>
<!-- Settings View -->
<div id="view-settings" class="view">
<div class="view-header">
<h2 class="view-title">Settings</h2>
</div>
<div class="settings-grid">
<div class="setting-card">
<div class="setting-info">
<h4>Activate Pass</h4>
<p>Enter your pass code to access server packs</p>
</div>
<div class="setting-control setting-pass">
<input type="text" id="pass-code" placeholder="Pass code" class="pass-input">
<button id="activate-pass-btn" class="btn-primary btn-sm">Activate</button>
</div>
</div>
<div class="setting-card">
<div class="setting-info">
<h4>Allocated RAM</h4>
<p id="ram-info">Loading...</p>
</div>
<div class="setting-control">
<input type="range" id="ram-slider" min="1024" max="16384" step="512" value="4096">
<span class="setting-value" id="ram-value">4 GB</span>
</div>
</div>
<div class="setting-card">
<div class="setting-info">
<h4>Game Resolution</h4>
<p>Width x Height</p>
</div>
<div class="setting-control" style="gap:6px">
<input type="number" id="win-width" min="640" max="7680" step="1" value="1280" class="setting-input" style="width:80px">
<span style="color:var(--text-muted)">x</span>
<input type="number" id="win-height" min="480" max="4320" step="1" value="720" class="setting-input" style="width:80px">
</div>
</div>
<div class="setting-card">
<div class="setting-info">
<h4>Extra JVM Arguments</h4>
<p>Additional Java VM options</p>
</div>
<div class="setting-control">
<input type="text" id="jvm-args" placeholder="-XX:+UseZGC" class="setting-input" style="width:280px">
</div>
</div>
<div class="setting-card">
<div class="setting-info">
<h4>Java Path</h4>
<p id="java-path">~/.zernmc/jre/</p>
</div>
<div class="setting-control">
<input type="text" id="java-path-input" placeholder="java" class="setting-input" style="width:280px">
</div>
</div>
<div class="setting-card">
<div class="setting-info">
<h4>Server</h4>
<p id="server-url">http://87.120.187.36:1582</p>
</div>
<div class="setting-control">
<span class="setting-badge" id="server-status">Checking...</span>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Right Panel - Play Button -->
<div class="right-panel">
<button id="play-btn" class="btn-play">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
ИГРАТЬ
</button>
<!-- Install Modal -->
<div id="install-modal" class="modal-backdrop hidden">
<div class="modal">
<div class="modal-head">
<h3>Install Pack</h3>
<button class="modal-close" id="close-modal-btn">&times;</button>
</div>
<div class="modal-body">
<div class="modal-tabs">
<button class="modal-tab active" data-tab="zernmc">Server Pack</button>
<button class="modal-tab" data-tab="custom">Custom</button>
</div>
<div id="tab-zernmc" class="modal-tab-content active">
<div class="field">
<label>Server Pack</label>
<select id="zernmc-pack-select">
<option value="">Loading...</option>
</select>
</div>
<div class="field">
<label>Local Name</label>
<input type="text" id="zernmc-instance-name" placeholder="my-cool-pack">
</div>
<button id="install-zernmc-btn" class="btn-primary">Download & Install</button>
</div>
<div id="tab-custom" class="modal-tab-content">
<div class="field">
<label>Minecraft Version</label>
<div class="select-wrap">
<select id="mc-version-select"><option>Loading...</option></select>
</div>
</div>
<div class="field">
<label>Mod Loader</label>
<div class="select-wrap">
<select id="loader-select">
<option value="vanilla">Vanilla (no loader)</option>
<option value="fabric">Fabric</option>
<option value="forge">Forge</option>
<option value="neoforge">NeoForge</option>
</select>
</div>
</div>
<div class="field hidden" id="loader-ver-field">
<label>Loader Version</label>
<div class="select-wrap">
<select id="loader-ver-select"><option>Select loader version</option></select>
</div>
</div>
<div class="field">
<label>Local Name</label>
<input type="text" id="custom-instance-name" placeholder="my-minecraft">
</div>
<button id="install-custom-btn" class="btn-primary">Download & Install</button>
</div>
<div id="install-progress" class="install-progress hidden">
<div class="progress-track">
<div class="progress-fill" id="progress-fill"></div>
</div>
<p class="progress-label" id="progress-label">Installing...</p>
</div>
</div>
</div>
</div>
<!-- Download Modal -->
<div id="download-modal" class="modal hidden">
<div class="modal-content">
<div class="modal-header">
<h2>Скачать сборку</h2>
<button class="modal-close" id="close-download-modal">&times;</button>
</div>
<div class="modal-tabs">
<button class="tab-btn active" data-tab="zernmc">ZernMC сборки</button>
<button class="tab-btn" data-tab="vanilla">Чистый Minecraft</button>
</div>
<div id="tab-zernmc" class="tab-content active">
<div class="form-group">
<label>Выберите сборку</label>
<select id="zernmc-pack-select" class="select-input">
<option value="">Загрузка...</option>
</select>
</div>
<div class="form-group">
<label>Название сборки (системное)</label>
<input type="text" id="zernmc-instance-name" class="text-input" placeholder="my-zernmc-pack">
</div>
<button id="install-zernmc-btn" class="btn-install">
Скачать и установить
</button>
</div>
<div id="tab-vanilla" class="tab-content">
<div class="form-group">
<label>Версия Minecraft</label>
<div class="custom-dropdown" id="mc-version-dropdown">
<div class="dropdown-trigger">
<span class="dropdown-value">Выберите версию</span>
<span class="dropdown-arrow"></span>
</div>
<div class="dropdown-list"></div>
</div>
</div>
<div class="form-group">
<label>Лоадер</label>
<div class="custom-dropdown" id="loader-dropdown">
<div class="dropdown-trigger">
<span class="dropdown-value">Vanilla (без лоадера)</span>
<span class="dropdown-arrow"></span>
</div>
<div class="dropdown-list">
<div class="dropdown-item selected" data-value="vanilla">Vanilla (без лоадера)</div>
<div class="dropdown-item" data-value="fabric">Fabric</div>
<div class="dropdown-item" data-value="forge">Forge</div>
<div class="dropdown-item" data-value="neoforge">NeoForge</div>
</div>
</div>
</div>
<div id="loader-version-group" class="form-group hidden">
<label>Версия лоадера</label>
<div class="custom-dropdown" id="loader-version-dropdown">
<div class="dropdown-trigger">
<span class="dropdown-value">Выберите версию</span>
<span class="dropdown-arrow"></span>
</div>
<div class="dropdown-list"></div>
</div>
</div>
<div class="form-group">
<label>Название сборки</label>
<input type="text" id="vanilla-instance-name" class="text-input" placeholder="my-minecraft">
</div>
<button id="install-vanilla-btn" class="btn-install">
Скачать и установить
</button>
</div>
<div id="download-progress" class="download-progress hidden">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill"></div>
</div>
<p class="progress-text" id="progress-text">Загрузка...</p>
</div>
</div>
</div>
<!-- Loading Overlay -->
<div id="loading-overlay" class="loading-overlay hidden">
<div class="loader"></div>
<p>Загрузка...</p>
</div>
<!-- Notification Toast -->
<div id="toast" class="toast hidden"></div>
</div>
<script src="launcher.js"></script>
</body>
</html>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff