diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5cf1396 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +logs/ +__pycache__/ +./.venv/ +launcher/target +server/builds +server/packs +server/data +jre +.vscode \ No newline at end of file diff --git a/launcher/dependency-reduced-pom.xml b/launcher/dependency-reduced-pom.xml new file mode 100644 index 0000000..7cb45e3 --- /dev/null +++ b/launcher/dependency-reduced-pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + me.sashegdev + ZernMCLauncher + 1.0.2 + + + + maven-shade-plugin + 3.5.0 + + + package + + shade + + + ../server/builds/ZernMCLauncher.jar + + + ${mainClass} + + + + + + + + com.akathist.maven.plugins.launch4j + launch4j-maven-plugin + 2.5.0 + + + l4j + package + + launch4j + + + ../server/builds/ZernMCLauncher.exe + ../server/builds/ZernMCLauncher.jar + console + false + + jre21 + 21 + + + 1.0.0.0 + 1.0.0 + ZernMC Launcher + 1.0.0.0 + 1.0.0 + ZernMC Launcher + + + + + + + maven-antrun-plugin + 3.1.0 + + + package + + run + + + + ${project.version} + + + + + + + + + + + + + 21 + me.sashegdev.zernmc.launcher.Main + 21 + UTF-8 + + diff --git a/launcher/pom.xml b/launcher/pom.xml new file mode 100644 index 0000000..54cb25c --- /dev/null +++ b/launcher/pom.xml @@ -0,0 +1,158 @@ + + + + 4.0.0 + me.sashegdev + ZernMCLauncher + 1.0.2 + jar + + + 21 + 21 + UTF-8 + me.sashegdev.zernmc.launcher.Main + + + + + + org.apache.httpcomponents + httpclient + 4.5.14 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.15.2 + + + + + com.google.code.gson + gson + 2.10.1 + + + + + org.json + json + 20231013 + + + + + org.fusesource.jansi + jansi + 2.4.1 + + + + + me.tongfei + progressbar + 0.9.5 + + + + + commons-io + commons-io + 2.15.1 + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.0 + + + package + + shade + + + ../server/builds/ZernMCLauncher.jar + + + ${mainClass} + + + + + + + + + + com.akathist.maven.plugins.launch4j + launch4j-maven-plugin + 2.5.0 + + + l4j + package + + launch4j + + + ../server/builds/ZernMCLauncher.exe + ../server/builds/ZernMCLauncher.jar + console + false + + jre21 + 21 + + + 1.0.0.0 + 1.0.0 + ZernMC Launcher + 1.0.0.0 + 1.0.0 + ZernMC Launcher + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 3.1.0 + + + package + run + + + ${project.version} + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java new file mode 100644 index 0000000..bb894f7 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/Main.java @@ -0,0 +1,157 @@ +package me.sashegdev.zernmc.launcher; + +import me.sashegdev.zernmc.launcher.menu.*; +import me.sashegdev.zernmc.launcher.ui.ArrowMenu; +import me.sashegdev.zernmc.launcher.utils.*; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; + +public class Main { + + private static final String CURRENT_VERSION = Version.getCurrentVersion(); + + public static void main(String[] args) { + System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true"); + ZAnsi.install(); + + System.out.print("\033[H\033[2J"); + System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION)); + + checkAndAutoUpdateLauncher(); + + try { + mainLoop(); + } catch (Exception e) { + System.err.println(ZAnsi.brightRed("Критическая ошибка: " + e.getMessage())); + e.printStackTrace(); + } finally { + ZAnsi.uninstall(); + } + } + + private static void checkAndAutoUpdateLauncher() { + System.out.println(ZAnsi.cyan("Проверка обновлений лаунчера...")); + + try { + String json = ZHttpClient.getLauncherVersionInfo(); + String serverVersion = extractVersion(json); + + System.out.println(ZAnsi.white("Текущая версия: ") + CURRENT_VERSION); + System.out.println(ZAnsi.white("Версия на сервере: ") + serverVersion); + + if (Version.isNewer(CURRENT_VERSION, serverVersion)) { + System.out.println(ZAnsi.brightYellow("\nДоступна новая версия лаунчера! (" + serverVersion + ")")); + System.out.println(ZAnsi.cyan("Начинается автоматическое обновление...\n")); + + performAutoUpdate(serverVersion); + restartLauncher(); + } else { + System.out.println(ZAnsi.brightGreen("Лаунчер актуален.")); + } + + } catch (Exception e) { + System.out.println(ZAnsi.yellow("Не удалось проверить обновления лаунчера.")); + System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage()); + } + } + + private static void performAutoUpdate(String newVersion) throws Exception { + String downloadUrl = ZHttpClient.getBaseUrl() + "/launcher/download?type=jar"; + Path currentJar = getCurrentJarPath(); + Path tempJar = currentJar.getParent().resolve("zernmc-launcher-new.jar"); + + System.out.println(ZAnsi.cyan("Скачивание версии " + newVersion + "...")); + + HttpClient client = HttpClient.newBuilder().build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(java.net.URI.create(downloadUrl)) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofFile(tempJar)); + + if (response.statusCode() != 200) { + throw new IOException("Сервер вернул код: " + response.statusCode()); + } + + long size = Files.size(tempJar); + System.out.println(ZAnsi.brightGreen("Скачано успешно (" + (size / 1024) + " KB)")); + + // Заменяем текущий jar + Files.move(tempJar, currentJar, StandardCopyOption.REPLACE_EXISTING); + + System.out.println(ZAnsi.brightGreen("Обновление успешно установлено!")); + } + + private static void restartLauncher() { + try { + String javaPath = System.getProperty("java.home") + "/bin/java"; + String jarPath = getCurrentJarPath().toAbsolutePath().toString(); + + System.out.println(ZAnsi.brightGreen("Перезапуск лаунчера с новой версией...")); + + new ProcessBuilder(javaPath, "-jar", jarPath) + .inheritIO() + .start(); + + System.exit(0); + } catch (Exception e) { + System.err.println(ZAnsi.brightRed("Не удалось перезапустить лаунчер.")); + System.exit(1); + } + } + + private static String extractVersion(String json) { + try { + return json.replaceAll(".*\"version\"\\s*:\\s*\"([^\"]+)\".*", "$1"); + } catch (Exception e) { + return "unknown"; + } + } + + private static Path getCurrentJarPath() { + try { + return Path.of(Main.class.getProtectionDomain() + .getCodeSource() + .getLocation() + .toURI()); + } catch (Exception e) { + return Path.of("zernmc-launcher-1.0-jar-with-dependencies.jar"); + } + } + + private static void mainLoop() throws Exception { + while (true) { + List options = List.of( + "Запустить игру", + "Проверка обновлений", + "Настройки", + "Проверка подключения к серверам Zern", + "Выход" + ); + + ArrowMenu menu = new ArrowMenu("Главное меню", options); + int choice = menu.show(); + + if (choice == -1 || choice == 4) { + System.out.print("\033[H\033[2J"); + System.out.println(ZAnsi.yellow("До свидания!")); + break; + } + + switch (choice) { + case 0 -> new LaunchMenu().show(); + case 1 -> new UpdateMenu().show(); + case 2 -> new SettingsMenu().show(); + case 3 -> new ServerCheckMenu().show(); + } + } + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java new file mode 100644 index 0000000..2deec3b --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java @@ -0,0 +1,348 @@ +package me.sashegdev.zernmc.launcher.menu; + +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.installer.VersionInstaller; +import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions; +import me.sashegdev.zernmc.launcher.minecraft.model.MinecraftVersion; +import me.sashegdev.zernmc.launcher.ui.ArrowMenu; +import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; +import me.sashegdev.zernmc.launcher.utils.ZHttpClient; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class LaunchMenu { + + public void show() throws Exception { + while (true) { + ConsoleUtils.clearScreen(); + List instances = InstanceManager.getAllInstances(); + + List options = instances.stream() + .map(Instance::toString) + .collect(Collectors.toList()); + + options.add("Установить новую сборку"); + options.add("Назад в главное меню"); + + ArrowMenu menu = new ArrowMenu("Управление сборками", options); + int choice = menu.show(); + + if (choice == -1) break; + if (choice == options.size() - 1) break; // Назад + + if (choice == instances.size()) { + installNewPack(); + continue; + } + + Instance selected = instances.get(choice); + manageInstance(selected); + } + } + + private void installNewPack() throws IOException { + ConsoleUtils.clearScreen(); + System.out.println(ZAnsi.cyan("Получение списка версий Minecraft...")); + + try { + VersionInstaller versionInstaller = new VersionInstaller(null); + List allVersions = versionInstaller.getAvailableVersions(); + + List versionOptions = allVersions.stream() + .map(v -> v.getId() + " (" + v.getType() + ")") + .collect(Collectors.toList()); + versionOptions.add("Назад"); + + ArrowMenu versionMenu = new ArrowMenu("Выбор версии Minecraft", versionOptions); + int versionChoice = versionMenu.show(); + + if (versionChoice == -1 || versionChoice == versionOptions.size() - 1) return; + + MinecraftVersion selectedMc = allVersions.get(versionChoice); + String mcVersion = selectedMc.getId(); + + // === Выбор лоадера с правильной проверкой поддержки === + List loaderOptions = buildLoaderOptions(mcVersion); + ArrowMenu loaderMenu = new ArrowMenu("Выбор модлоадера для " + mcVersion, loaderOptions); + int loaderChoice = loaderMenu.show(); + + if (loaderChoice == -1 || loaderChoice == loaderOptions.size() - 1) return; + + String selectedLoader = loaderOptions.get(loaderChoice); + + if (selectedLoader.contains("Vanilla")) { + createVanillaInstance(mcVersion); + return; + } + + String loaderType = selectedLoader.contains("Fabric") ? "fabric" : "forge"; + + String loaderVersion; + if (loaderType.equals("fabric")) { + loaderVersion = askFabricLoaderVersion(); + } else { + loaderVersion = askForgeVersion(mcVersion); + } + + if (loaderVersion == null) return; + + String packName = askPackName(); + if (packName == null) return; + + InstanceManager.createInstanceFolder(packName); + Instance newInstance = InstanceManager.getInstance(packName); + + MinecraftLib lib = new MinecraftLib(newInstance); + + boolean success = loaderType.equals("fabric") + ? lib.installFabric(mcVersion, loaderVersion) + : lib.installForge(mcVersion, loaderVersion); + + if (success) { + System.out.println(ZAnsi.brightGreen("\nСборка '" + packName + "' успешно установлена!")); + } + + } catch (Exception e) { + System.out.println(ZAnsi.brightRed("Ошибка: " + e.getMessage())); + } + + ConsoleUtils.pause(); + } + + // ====================== Вспомогательные методы ====================== + + private List buildLoaderOptions(String mcVersion) { + List options = new ArrayList<>(); + + if (isFabricSupported(mcVersion)) { + options.add("Fabric"); + } + if (isForgeSupported(mcVersion)) { + options.add("Forge"); + } + options.add("Vanilla"); + options.add("Назад"); + + return options; + } + + private boolean isFabricSupported(String version) { + // Fabric стабильно работает с 1.14+ + return version.matches("^1\\.(1[4-9]|[2-9]\\d).*"); + } + + private boolean isForgeSupported(String version) { + // Forge поддерживает примерно до 1.21.4 на текущий момент + // Для версий 1.22+ и экспериментальных — отключаем + if (version.matches("^1\\.2[2-9].*") || version.matches("^\\d{2}.*")) { + return false; + } + return version.matches("^1\\.(1[2-9]|[2-9]\\d).*") || + version.matches("^1\\.20.*") || version.matches("^1\\.21.*"); + } + + 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() + + (instance.getLoaderVersion() != null ? " " + instance.getLoaderVersion() : ""))); + + List options = new ArrayList<>(); + options.add("Запустить сборку"); + options.add("Изменить версию лоадера"); + options.add("Удалить сборку"); + options.add("Назад"); + + ArrowMenu menu = new ArrowMenu("Действия", options); + int choice = menu.show(); + + if (choice == -1 || choice == 3) return; // Esc или Назад + + switch (choice) { + case 0 -> launchExistingInstance(instance); + case 1 -> changeLoaderVersion(instance); + case 2 -> deleteInstance(instance); + } + } + } + + private void changeLoaderVersion(Instance instance) throws Exception { + ConsoleUtils.clearScreen(); + System.out.println(ZAnsi.cyan("Изменение версии лоадера для " + instance.getName())); + + String currentLoader = instance.getLoaderType(); + String mcVersion = instance.getMinecraftVersion(); + + if ("vanilla".equalsIgnoreCase(currentLoader)) { + System.out.println(ZAnsi.yellow("Это vanilla сборка. Нельзя изменить лоадер.")); + ConsoleUtils.pause(); + return; + } + + String newLoaderVersion; + if ("fabric".equalsIgnoreCase(currentLoader)) { + newLoaderVersion = askFabricLoaderVersion(); + } else { + newLoaderVersion = askForgeVersion(mcVersion); + } + + if (newLoaderVersion == null) return; + + System.out.println(ZAnsi.cyan("Переустановка лоадера " + currentLoader + " → " + newLoaderVersion + "...")); + + MinecraftLib lib = new MinecraftLib(instance); + boolean success; + + try { + if ("fabric".equalsIgnoreCase(currentLoader)) { + success = lib.installFabric(mcVersion, newLoaderVersion); + } else { + success = lib.installForge(mcVersion, newLoaderVersion); + } + + if (success) { + System.out.println(ZAnsi.brightGreen("Версия лоадера успешно изменена!")); + } + } catch (Exception e) { + System.out.println(ZAnsi.brightRed("Ошибка при смене лоадера: " + e.getMessage())); + } + + ConsoleUtils.pause(); + } + + private void deleteInstance(Instance instance) throws IOException { + System.out.println(ZAnsi.brightRed("Вы действительно хотите удалить сборку '" + instance.getName() + "'?")); + System.out.print(ZAnsi.white("Введите 'да' для подтверждения: ")); + String confirm = new java.util.Scanner(System.in).nextLine().trim(); + + if ("да".equalsIgnoreCase(confirm)) { + InstanceManager.getInstance(instance.getName()); + System.out.println(ZAnsi.brightGreen("Сборка удалена.")); + } else { + System.out.println(ZAnsi.yellow("Отменено.")); + } + ConsoleUtils.pause(); + } + + private String askFabricLoaderVersion() throws Exception { + System.out.println(ZAnsi.cyan("Получение списка версий Fabric Loader...")); + List versions = ZHttpClient.getFabricLoaderVersions(); + + List options = versions.stream() + .limit(30) // увеличил до 30 + .map(v -> "Fabric Loader " + v) + .collect(Collectors.toList()); + options.add("Назад"); + + ArrowMenu menu = new ArrowMenu("Выбор версии Fabric Loader", options); + int choice = menu.show(); + + if (choice == -1 || choice == options.size() - 1) return null; + return versions.get(choice); + } + + private String askForgeVersion(String mcVersion) throws Exception { + System.out.println(ZAnsi.cyan("Получение списка версий Forge для " + mcVersion + "...")); + + // Получаем все версии Forge из Maven + List allForgeVersions = getAllForgeVersions(); + + // Фильтруем только те, которые подходят под нашу версию Minecraft + List compatibleVersions = allForgeVersions.stream() + .filter(v -> v.startsWith(mcVersion + "-")) + .map(v -> v.substring(mcVersion.length() + 1)) // убираем "1.20.1-" + .collect(Collectors.toList()); + + if (compatibleVersions.isEmpty()) { + System.out.println(ZAnsi.yellow("Не найдено совместимых версий Forge для " + mcVersion)); + ConsoleUtils.pause(); + return null; + } + + List options = compatibleVersions.stream() + .map(v -> "Forge " + v) + .collect(Collectors.toList()); + options.add("Назад"); + + ArrowMenu menu = new ArrowMenu("Выбор версии Forge для " + mcVersion, options); + int choice = menu.show(); + + if (choice == -1 || choice == options.size() - 1) return null; + + return compatibleVersions.get(choice); + } + + private String askPackName() { + System.out.print(ZAnsi.white("\nВведите название новой сборки: ")); + String name = new java.util.Scanner(System.in).nextLine().trim(); + if (name.isEmpty()) { + System.out.println(ZAnsi.yellow("Отменено.")); + return null; + } + return name; + } + + private void createVanillaInstance(String mcVersion) throws Exception { + String packName = askPackName(); + if (packName == null) return; + + InstanceManager.createInstanceFolder(packName); + Instance newInstance = InstanceManager.getInstance(packName); + + MinecraftLib lib = new MinecraftLib(newInstance); + boolean success = lib.installMinecraft(mcVersion); + + if (success) { + System.out.println(ZAnsi.brightGreen("\nVanilla сборка '" + packName + "' успешно создана!")); + } + } + + private void launchExistingInstance(Instance instance) { + ConsoleUtils.clearScreen(); + System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName())); + + MinecraftLib lib = new MinecraftLib(instance); + LaunchOptions options = new LaunchOptions(); + + try { + lib.launch(options); + } catch (Exception e) { + System.out.println(ZAnsi.brightRed("Ошибка при запуске: " + e.getMessage())); + } + + ConsoleUtils.pause(); + } + + private List getAllForgeVersions() throws Exception { + String metadataUrl = "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml"; + + String xml = ZHttpClient.downloadString(metadataUrl); // добавь этот метод в ZHttpClient, если его нет + + // Парсим простым способом (без XML парсера) + List versions = new ArrayList<>(); + int index = 0; + + while ((index = xml.indexOf("", index)) != -1) { + int start = index + 9; + int end = xml.indexOf("", start); + if (end == -1) break; + + String version = xml.substring(start, end).trim(); + versions.add(version); + index = end; + } + + // Сортируем по убыванию (новые сверху) + versions.sort((a, b) -> b.compareTo(a)); + + return versions; + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/ServerCheckMenu.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/ServerCheckMenu.java new file mode 100644 index 0000000..68bd045 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/ServerCheckMenu.java @@ -0,0 +1,103 @@ +package me.sashegdev.zernmc.launcher.menu; + +import me.sashegdev.zernmc.launcher.ui.ArrowMenu; +import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; +import me.sashegdev.zernmc.launcher.utils.ZHttpClient; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.List; + +public class ServerCheckMenu { + + public void show() throws IOException { + List options = List.of( + "Проверить подключение к ZernMC серверу", + "Проверить доступ к Mojang (Minecraft)", + "Проверить доступ к Fabric Meta", + "Назад в главное меню" + ); + + ArrowMenu menu = new ArrowMenu("Диагностика подключения", options); + int choice = menu.show(); + + if (choice == -1 || choice == 4) return; + + ConsoleUtils.clearScreen(); + + switch (choice) { + case 0 -> checkZernServer(); + case 1 -> checkMojang(); + case 2 -> checkFabric(); + } + + ConsoleUtils.pause(); + } + + private void checkZernServer() { + System.out.println(ZAnsi.cyan("Проверка подключения к ZernMC серверу...")); + try { + String response = ZHttpClient.get("/health"); + System.out.println(ZAnsi.brightGreen("Сервер успешно подключён!")); + System.out.println("Ответ: " + response); + } catch (Exception e) { + System.out.println(ZAnsi.brightRed("Не удалось подключиться к ZernMC серверу")); + System.out.println("Ошибка: " + e.getMessage()); + } + } + + private void checkMojang() { + System.out.println(ZAnsi.cyan("Проверка доступа к Mojang...")); + try { + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(8)) + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json")) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + System.out.println(ZAnsi.brightGreen("Mojang доступен")); + } else { + System.out.println(ZAnsi.brightRed("Mojang вернул код " + response.statusCode())); + } + } catch (Exception e) { + System.out.println(ZAnsi.brightRed("Нет доступа к Mojang")); + System.out.println("Ошибка: " + e.getMessage()); + } + } + + private void checkFabric() { + System.out.println(ZAnsi.cyan("Проверка доступа к Fabric Meta...")); + try { + HttpClient client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(8)) + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("https://meta.fabricmc.net/v2/versions")) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + System.out.println(ZAnsi.brightGreen("Fabric Meta доступен")); + } else { + System.out.println(ZAnsi.brightRed("Fabric Meta вернул код " + response.statusCode())); + } + } catch (Exception e) { + System.out.println(ZAnsi.brightRed("Нет доступа к Fabric Meta")); + System.out.println("Ошибка: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/SettingsMenu.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/SettingsMenu.java new file mode 100644 index 0000000..7c16845 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/SettingsMenu.java @@ -0,0 +1,68 @@ +package me.sashegdev.zernmc.launcher.menu; + +import me.sashegdev.zernmc.launcher.ui.ArrowMenu; +import me.sashegdev.zernmc.launcher.utils.Config; +import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; +import me.sashegdev.zernmc.launcher.utils.Input; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; + +import java.io.IOException; +import java.util.List; + +public class SettingsMenu { + + public void show() throws IOException { + List options = List.of( + "Настроить путь к Java", + "Настроить выделенную память (RAM)", + "Дополнительные JVM параметры", + "Назад в главное меню" + ); + + ArrowMenu menu = new ArrowMenu("Настройки лаунчера", options); + int choice = menu.show(); + + if (choice == -1 || choice == 3) return; + + ConsoleUtils.clearScreen(); + + switch (choice) { + case 0 -> configureJava(); + case 1 -> configureRam(); + case 2 -> configureJvmArgs(); + } + + ConsoleUtils.pause(); + } + + private void configureJava() { + System.out.println(ZAnsi.cyan("Путь к Java:")); + System.out.println(" " + Config.getJreDir().toAbsolutePath()); + System.out.println(ZAnsi.white("\nJava будет искаться автоматически в папке ~/.zernmc/jre/")); + System.out.println("Если нужно — положите туда свою версию Java."); + } + + private void configureRam() { + System.out.println(ZAnsi.cyan("Настройка выделенной памяти")); + System.out.println(Config.getRamInfo()); + + int newRam = Input.readInt( + ZAnsi.white("\nВведите новое значение RAM в MB (или 0 для отмены): "), + 0, 32768 + ); + + if (newRam == 0) { + System.out.println(ZAnsi.yellow("Настройка отменена.")); + return; + } + + Config.setMaxMemory(newRam); + System.out.println(ZAnsi.brightGreen("Выделенная память изменена на " + newRam + " MB")); + } + + private void configureJvmArgs() { + System.out.println(ZAnsi.yellow("Дополнительные JVM параметры")); + System.out.println("Пока в разработке."); + System.out.println("В будущем здесь будет список предустановленных оптимизаций."); + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/UpdateMenu.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/UpdateMenu.java new file mode 100644 index 0000000..5e4f802 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/UpdateMenu.java @@ -0,0 +1,36 @@ +package me.sashegdev.zernmc.launcher.menu; + +import me.sashegdev.zernmc.launcher.ui.ArrowMenu; +import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; + +import java.io.IOException; +import java.util.List; + +public class UpdateMenu { + + public void show() throws IOException { + List options = List.of( + "Проверить обновления сборки (модпака)", + "Проверить обновления лаунчера", + "Назад в главное меню" + ); + + ArrowMenu menu = new ArrowMenu("Проверка обновлений", options); + int choice = menu.show(); + + if (choice == -1 || choice == 2) return; + + ConsoleUtils.clearScreen(); + + if (choice == 0) { + System.out.println("Проверка обновлений сборки..."); + System.out.println("Дифф обновлений пока в заглушке (сборки ещё не загружены)"); + System.out.println(" Эндпоинт: POST /pack/{pack_name}/diff"); + } else { + System.out.println("Проверка обновлений лаунчера..."); + System.out.println("Версия лаунчера актуальна (заглушка)"); + } + + ConsoleUtils.pause(); + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Installer.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Installer.java new file mode 100644 index 0000000..8b009ff --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Installer.java @@ -0,0 +1,19 @@ +package me.sashegdev.zernmc.launcher.minecraft; + +import me.sashegdev.zernmc.launcher.utils.ZAnsi; + +public class Installer { + + public static boolean installPack(String packName, Instance instance) { + System.out.println(ZAnsi.cyan("Начинается установка сборки: " + packName)); + + // TODO: + // 1. Получить манифест пака (/pack/{packName}) + // 2. Запустить diff + // 3. Скачать недостающие файлы + // 4. Установить Minecraft + Loader (через MinecraftLib) + + System.out.println(ZAnsi.yellow("Установка пока в разработке...")); + return false; + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java new file mode 100644 index 0000000..a2de872 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java @@ -0,0 +1,110 @@ +package me.sashegdev.zernmc.launcher.minecraft; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class Instance { + + private final String name; + private final Path path; + + private String minecraftVersion; + private String loaderType; // vanilla, fabric, forge + private String loaderVersion; + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + public Instance(String name, Path path) { + this.name = name; + this.path = path; + loadMetadata(); + } + + public String getName() { return name; } + public Path getPath() { return path; } + + public String getMinecraftVersion() { return minecraftVersion; } + public void setMinecraftVersion(String minecraftVersion) { + this.minecraftVersion = minecraftVersion; + saveMetadata(); + } + + public String getLoaderType() { return loaderType != null ? loaderType : "vanilla"; } + public void setLoaderType(String loaderType) { + this.loaderType = loaderType; + saveMetadata(); + } + + public String getLoaderVersion() { return loaderVersion; } + public void setLoaderVersion(String loaderVersion) { + this.loaderVersion = loaderVersion; + saveMetadata(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(name); + + if (minecraftVersion != null) { + sb.append(" [").append(minecraftVersion); + + if (!"vanilla".equalsIgnoreCase(getLoaderType())) { + sb.append(" + ").append(getLoaderType()); + if (loaderVersion != null) { + sb.append(" ").append(loaderVersion); + } + } + sb.append("]"); + } else { + sb.append(" [?]"); + } + + return sb.toString(); + } + + // ====================== Метаданные ====================== + + private void loadMetadata() { + Path metaFile = path.resolve("instance.json"); + if (!Files.exists(metaFile)) return; + + try { + String json = Files.readString(metaFile); + InstanceMeta meta = GSON.fromJson(json, InstanceMeta.class); + + this.minecraftVersion = meta.minecraftVersion; + this.loaderType = meta.loaderType; + this.loaderVersion = meta.loaderVersion; + } catch (Exception e) { + // игнорируем, если файл повреждён + } + } + + private void saveMetadata() { + Path metaFile = path.resolve("instance.json"); + InstanceMeta meta = new InstanceMeta(minecraftVersion, loaderType, loaderVersion); + + try { + Files.writeString(metaFile, GSON.toJson(meta)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // Внутренний класс для сериализации + private static class InstanceMeta { + String minecraftVersion; + String loaderType; + String loaderVersion; + + public InstanceMeta(String minecraftVersion, String loaderType, String loaderVersion) { + this.minecraftVersion = minecraftVersion; + this.loaderType = loaderType; + this.loaderVersion = loaderVersion; + } + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/InstanceManager.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/InstanceManager.java new file mode 100644 index 0000000..8baa8a4 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/InstanceManager.java @@ -0,0 +1,43 @@ +package me.sashegdev.zernmc.launcher.minecraft; + +import me.sashegdev.zernmc.launcher.utils.Config; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +public class InstanceManager { + + private static final Path INSTANCES_DIR = Config.getInstancesDir(); + + public static List getAllInstances() throws IOException { + if (!Files.exists(INSTANCES_DIR)) { + Files.createDirectories(INSTANCES_DIR); + return List.of(); + } + + return Files.list(INSTANCES_DIR) + .filter(Files::isDirectory) + .map(path -> new Instance(path.getFileName().toString(), path)) + .collect(Collectors.toList()); + } + + public static Instance getInstance(String name) { + Path instancePath = INSTANCES_DIR.resolve(name); + if (Files.exists(instancePath) && Files.isDirectory(instancePath)) { + return new Instance(name, instancePath); + } + return null; + } + + public static boolean createInstanceFolder(String name) throws IOException { + Path path = INSTANCES_DIR.resolve(name); + if (Files.exists(path)) { + return false; + } + Files.createDirectories(path); + return true; + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java new file mode 100644 index 0000000..fdb9b12 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java @@ -0,0 +1,146 @@ +package me.sashegdev.zernmc.launcher.minecraft; + +import me.sashegdev.zernmc.launcher.minecraft.installer.FabricInstaller; +import me.sashegdev.zernmc.launcher.minecraft.installer.ForgeInstaller; +import me.sashegdev.zernmc.launcher.minecraft.installer.VersionInstaller; +import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder; +import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions; +import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; + +import java.util.List; + +/** + * Главный фасад MinecraftLib — точка входа для всей логики установки и запуска + */ +public class MinecraftLib { + + private final Instance instance; + + public MinecraftLib(Instance instance) { + this.instance = instance; + } + + // ====================== УСТАНОВКА ====================== + + /** + * Установить vanilla версию Minecraft + */ + public boolean installMinecraft(String versionId) throws Exception { + VersionInstaller installer = new VersionInstaller(instance.getPath()); + boolean success = installer.install(versionId); + + if (success) { + instance.setMinecraftVersion(versionId); + instance.setLoaderType("vanilla"); + } + return success; + } + + public boolean installForge(String minecraftVersion, String forgeVersion) throws Exception { + ForgeInstaller installer = new ForgeInstaller(instance); + return installer.install(minecraftVersion, forgeVersion); + } + + /** + * Установить Fabric для выбранной версии Minecraft + */ + public boolean installFabric(String minecraftVersion, String loaderVersion) throws Exception { + FabricInstaller installer = new FabricInstaller(instance); + boolean success = installer.install(minecraftVersion, loaderVersion); + + if (success) { + // Сохраняем информацию в Instance + instance.setMinecraftVersion(minecraftVersion); + instance.setLoaderType("fabric"); + instance.setLoaderVersion(loaderVersion); + } + return success; + } + + /** + * Полная установка сборки (vanilla + loader + моды) + * Пока заглушка — будем расширять + */ + public boolean installPack(String packName, String minecraftVersion, String loaderType, String loaderVersion) throws Exception { + System.out.println(ZAnsi.cyan("Начинается полная установка сборки: " + packName)); + + // 1. Устанавливаем Minecraft + boolean mcInstalled = installMinecraft(minecraftVersion); + if (!mcInstalled) { + System.out.println(ZAnsi.brightRed("Не удалось установить Minecraft " + minecraftVersion)); + return false; + } + + // 2. Устанавливаем лоадер + if ("fabric".equalsIgnoreCase(loaderType)) { + boolean fabricInstalled = installFabric(minecraftVersion, loaderVersion); + if (!fabricInstalled) { + System.out.println(ZAnsi.brightRed("Не удалось установить Fabric")); + return false; + } + } else if ("forge".equalsIgnoreCase(loaderType)) { + System.out.println(ZAnsi.yellow("Forge пока не поддерживается")); + return false; + } + + // 3. В будущем здесь будет diff и скачивание модов + + System.out.println(ZAnsi.brightGreen("Базовая установка сборки завершена!")); + return true; + } + + // ====================== ЗАПУСК ====================== + + /** + * Сгенерировать команду запуска (пока заглушка) + */ + public List buildLaunchCommand(LaunchOptions options) throws Exception { + System.out.println(ZAnsi.cyan("Генерация команды запуска для " + instance.getName() + "...")); + + // TODO: Полная реализация LaunchCommandBuilder (перенос из MLL) + + System.out.println(ZAnsi.yellow("Генерация команды запуска пока в разработке")); + return List.of("java", "-jar", "placeholder.jar", "--version", instance.getName()); + } + + /** + * Запустить сборку + */ + public void launch(LaunchOptions options) throws Exception { + System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName())); + + LaunchCommandBuilder builder = new LaunchCommandBuilder(instance); + List command = builder.build(options); + + System.out.println(ZAnsi.cyan("Команда запуска (" + command.size() + " аргументов):")); + for (String arg : command) { + System.out.println(" " + arg); + } + + // === Реальный запуск === + ProcessBuilder pb = new ProcessBuilder(command); + pb.directory(instance.getPath().toFile()); + + // Важно: перенаправляем вывод Minecraft в консоль лаунчера + pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); + pb.redirectError(ProcessBuilder.Redirect.INHERIT); + pb.redirectInput(ProcessBuilder.Redirect.INHERIT); + + System.out.println(ZAnsi.brightGreen("\nЗапускаем Minecraft...\n")); + ConsoleUtils.clearScreen(); // очищаем TUI перед запуском игры + + Process process = pb.start(); + + // Ждём завершения игры + int exitCode = process.waitFor(); + + System.out.println(ZAnsi.yellow("\nMinecraft завершился с кодом: " + exitCode)); + } + + // ====================== ГЕТТЕРЫ ====================== + + public Instance getInstance() { + return instance; + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/FabricInstaller.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/FabricInstaller.java new file mode 100644 index 0000000..4da0be8 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/FabricInstaller.java @@ -0,0 +1,134 @@ +package me.sashegdev.zernmc.launcher.minecraft.installer; + +import me.sashegdev.zernmc.launcher.minecraft.Instance; +import me.sashegdev.zernmc.launcher.utils.ProgressBar; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; + +public class FabricInstaller { + + private final Instance instance; + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(15)) + .build(); + + public FabricInstaller(Instance instance) { + this.instance = instance; + } + + public boolean install(String minecraftVersion, String loaderVersion) throws Exception { + System.out.println(ZAnsi.cyan("Установка Fabric " + loaderVersion + " для Minecraft " + minecraftVersion)); + + Path instancePath = instance.getPath(); + + cleanOldFabricLoaders(); + + // Шаг 1: Установка vanilla версии (если ещё не установлена) + VersionInstaller versionInstaller = new VersionInstaller(instancePath); + boolean mcOk = versionInstaller.install(minecraftVersion); + if (!mcOk) { + System.out.println(ZAnsi.brightRed("Не удалось установить Minecraft " + minecraftVersion)); + return false; + } + + // Шаг 2: Скачивание и запуск Fabric Installer + String installerVersion = getLatestInstallerVersion(); + String installerUrl = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/" + + installerVersion + "/fabric-installer-" + installerVersion + ".jar"; + + Path installerJar = instancePath.resolve("fabric-installer.jar"); + + ProgressBar.show("Скачивание Fabric Installer", 0, 100, "%"); + downloadFile(installerUrl, installerJar); + ProgressBar.finish("Fabric Installer скачан"); + + // Шаг 3: Запуск Fabric Installer + System.out.println(ZAnsi.cyan("Запуск Fabric Installer...")); + + ProcessBuilder pb = new ProcessBuilder( + "java", "-jar", installerJar.toAbsolutePath().toString(), + "client", + "-dir", instancePath.toAbsolutePath().toString(), + "-mcversion", minecraftVersion, + "-loader", loaderVersion, + "-noprofile", + "-snapshot" + ); + + pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); + pb.redirectError(ProcessBuilder.Redirect.INHERIT); + + Process process = pb.start(); + int exitCode = process.waitFor(); + + if (exitCode != 0) { + System.out.println(ZAnsi.brightRed("Fabric Installer завершился с ошибкой (код " + exitCode + ")")); + return false; + } + + // Шаг 4: Проверка, что Fabric версия появилась + String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion; + Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId); + + if (Files.exists(fabricVersionDir)) { + System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!")); + System.out.println("Версия: " + fabricVersionId); + return true; + } else { + System.out.println(ZAnsi.brightRed("Fabric Installer отработал, но версия не найдена.")); + return false; + } + } + + private void cleanOldFabricLoaders() throws IOException { + Path librariesDir = instance.getPath().resolve("libraries/net/fabricmc/fabric-loader"); + if (!Files.exists(librariesDir)) return; + + System.out.println(ZAnsi.yellow("Очистка старых версий Fabric Loader...")); + + try (var stream = Files.walk(librariesDir)) { + stream.filter(Files::isDirectory) + .filter(dir -> dir.getFileName().toString().startsWith("0.")) + .forEach(dir -> { + try { + // Удаляем папку версии + Files.walk(dir) + .sorted((a,b) -> b.compareTo(a)) // удаляем файлы перед папками + .forEach(p -> { + try { Files.deleteIfExists(p); } + catch (IOException ignored) {} + }); + } catch (IOException ignored) {} + }); + } + } + + private String getLatestInstallerVersion() throws Exception { + String url = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml"; + String xml = downloadString(url); + + int start = xml.indexOf("") + 8; + int end = xml.indexOf("", start); + return xml.substring(start, end).trim(); + } + + private String downloadString(String url) throws Exception { + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build(); + HttpResponse resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + if (resp.statusCode() != 200) throw new IOException("HTTP " + resp.statusCode()); + return resp.body(); + } + + private void downloadFile(String url, Path target) throws Exception { + HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build(); + httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target)); + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java new file mode 100644 index 0000000..16c3dfd --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/ForgeInstaller.java @@ -0,0 +1,115 @@ +package me.sashegdev.zernmc.launcher.minecraft.installer; + +import me.sashegdev.zernmc.launcher.minecraft.Instance; +import me.sashegdev.zernmc.launcher.utils.ProgressBar; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +public class ForgeInstaller { + + private final Instance instance; + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(java.time.Duration.ofSeconds(30)) + .build(); + + public ForgeInstaller(Instance instance) { + this.instance = instance; + } + + public boolean install(String mcVersion, String forgeVersion) throws Exception { + System.out.println(ZAnsi.cyan("Установка Forge " + forgeVersion + " для Minecraft " + mcVersion)); + + // Шаг 1: Полная установка vanilla + System.out.println(ZAnsi.cyan("Установка базовой версии Minecraft " + mcVersion + "...")); + VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath()); + boolean vanillaSuccess = vanillaInstaller.install(mcVersion); + + if (!vanillaSuccess) { + System.out.println(ZAnsi.brightRed("Не удалось установить базовую версию Minecraft")); + return false; + } + + // Шаг 2: Создаём launcher_profiles.json (критично для Forge) + createLauncherProfile(); + + // Шаг 3: Скачиваем и запускаем Forge Installer + String installerUrl = "https://maven.minecraftforge.net/net/minecraftforge/forge/" + + mcVersion + "-" + forgeVersion + + "/forge-" + mcVersion + "-" + forgeVersion + "-installer.jar"; + + Path installerJar = instance.getPath().resolve("forge-installer.jar"); + + ProgressBar.show("Скачивание Forge Installer", 0, 100, "%"); + downloadFile(installerUrl, installerJar); + ProgressBar.finish("Forge Installer скачан (" + ProgressBar.formatBytes(Files.size(installerJar)) + ")"); + + System.out.println(ZAnsi.cyan("Запуск Forge Installer...")); + + ProcessBuilder pb = new ProcessBuilder( + "java", + "-jar", + installerJar.toAbsolutePath().toString(), + "--installClient" + ); + + pb.directory(instance.getPath().toFile()); + pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); + pb.redirectError(ProcessBuilder.Redirect.INHERIT); + + Process process = pb.start(); + int exitCode = process.waitFor(); + + if (exitCode != 0) { + System.out.println(ZAnsi.brightRed("Forge Installer завершился с ошибкой (код " + exitCode + ")")); + return false; + } + + System.out.println(ZAnsi.brightGreen("Forge " + forgeVersion + " успешно установлен!")); + + instance.setMinecraftVersion(mcVersion); + instance.setLoaderType("forge"); + instance.setLoaderVersion(forgeVersion); + + return true; + } + + /** + * Создаёт минимальный launcher_profiles.json — Forge без него отказывается работать + */ + private void createLauncherProfile() throws IOException { + Path profilePath = instance.getPath().resolve("launcher_profiles.json"); + + if (Files.exists(profilePath)) return; + + String minimalProfile = """ + { + "profiles": {}, + "selectedProfile": "Default" + } + """; + + Files.writeString(profilePath, minimalProfile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + System.out.println(ZAnsi.yellow("Создан launcher_profiles.json")); + } + + private void downloadFile(String url, Path target) throws Exception { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target)); + + if (response.statusCode() != 200) { + throw new IOException("Не удалось скачать Forge installer (HTTP " + response.statusCode() + ")"); + } + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/VersionInstaller.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/VersionInstaller.java new file mode 100644 index 0000000..73c3d65 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/installer/VersionInstaller.java @@ -0,0 +1,206 @@ +package me.sashegdev.zernmc.launcher.minecraft.installer; + +import me.sashegdev.zernmc.launcher.minecraft.model.MinecraftVersion; +import me.sashegdev.zernmc.launcher.utils.ProgressBar; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +public class VersionInstaller { + + private final Path minecraftDir; + private final HttpClient httpClient; + + public VersionInstaller(Path minecraftDir) { + this.minecraftDir = minecraftDir; + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .build(); + } + + // getAvailableVersions() оставляем как было (с исправлением времени) + + public List getAvailableVersions() throws Exception { + String jsonString = downloadString("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"); + JSONObject root = new JSONObject(jsonString); + JSONArray versionsArray = root.getJSONArray("versions"); + + List versions = new ArrayList<>(); + + for (int i = 0; i < versionsArray.length(); i++) { + JSONObject v = versionsArray.getJSONObject(i); + String id = v.getString("id"); + String type = v.getString("type"); + String releaseTimeStr = v.getString("releaseTime").replace("Z", "").replace("+00:00", ""); + String url = v.getString("url"); + + LocalDateTime releaseTime = LocalDateTime.parse(releaseTimeStr); + versions.add(new MinecraftVersion(id, type, releaseTime, url)); + } + + versions.sort((a, b) -> b.getReleaseTime().compareTo(a.getReleaseTime())); + return versions; + } + + public boolean install(String versionId) throws Exception { + System.out.println(ZAnsi.cyan("Полная установка Minecraft " + versionId + "...")); + + Path versionDir = minecraftDir.resolve("versions").resolve(versionId); + Files.createDirectories(versionDir); + + String versionUrl = getVersionUrl(versionId); + if (versionUrl == null) throw new Exception("Версия " + versionId + " не найдена"); + + String versionJson = downloadString(versionUrl); + Files.writeString(versionDir.resolve(versionId + ".json"), versionJson); + + JSONObject versionData = new JSONObject(versionJson); + + // client.jar + downloadFile(versionData.getJSONObject("downloads").getJSONObject("client").getString("url"), + versionDir.resolve(versionId + ".jar"), "client.jar"); + + // Библиотеки + System.out.println(ZAnsi.cyan("Скачивание библиотек...")); + downloadLibraries(versionData.getJSONArray("libraries")); + + // Ассеты + if (versionData.has("assetIndex")) { + System.out.println(ZAnsi.cyan("Скачивание ассетов...")); + downloadAssets(versionData); + } + + System.out.println(ZAnsi.brightGreen("\nMinecraft " + versionId + " полностью установлен!")); + return true; + } + + private void downloadLibraries(JSONArray libraries) throws Exception { + int total = libraries.length(); + int count = 0; + + for (int i = 0; i < total; i++) { + JSONObject lib = libraries.getJSONObject(i); + if (lib.has("downloads") && lib.getJSONObject("downloads").has("artifact")) { + JSONObject art = lib.getJSONObject("downloads").getJSONObject("artifact"); + String url = art.getString("url"); + String path = art.getString("path"); + + Path target = minecraftDir.resolve("libraries").resolve(path); + Files.createDirectories(target.getParent()); + + try { + downloadFile(url, target, "library"); + } catch (Exception e) { + // Многие библиотеки могут быть пропущены — это нормально + } + } + count++; + if (count % 20 == 0) ProgressBar.show("Библиотеки", count, total, ""); + } + ProgressBar.finish("Библиотеки загружены"); + } + + private void downloadAssets(JSONObject versionData) throws Exception { + JSONObject assetIndexInfo = versionData.getJSONObject("assetIndex"); + String indexUrl = assetIndexInfo.getString("url"); + String indexId = versionData.getString("assets"); + + Path indexPath = minecraftDir.resolve("assets/indexes").resolve(indexId + ".json"); + Files.createDirectories(indexPath.getParent()); + downloadFile(indexUrl, indexPath, "asset index"); + + String assetsJson = new String(Files.readAllBytes(indexPath)); + JSONObject objects = new JSONObject(assetsJson).getJSONObject("objects"); + + System.out.println(ZAnsi.cyan("Скачивание " + objects.length() + " объектов ассетов...")); + + int count = 0; + int failed = 0; + + for (String hash : objects.keySet()) { + String url = "https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash; + Path target = minecraftDir.resolve("assets/objects") + .resolve(hash.substring(0, 2)) + .resolve(hash); + + Files.createDirectories(target.getParent()); + + try { + downloadFile(url, target, ""); // пустой label = ассет + count++; + } catch (Exception e) { + failed++; + } + + ProgressBar.show("Ассеты", count, objects.length(), "файлов"); + } + + ProgressBar.finish("Ассеты загружены (" + count + " успешно, " + failed + " пропущено)"); + } + + + + // === Вспомогательные методы === + + private String getVersionUrl(String versionId) throws Exception { + for (MinecraftVersion v : getAvailableVersions()) { + if (v.getId().equals(versionId)) return v.getUrl(); + } + return null; + } + + private String downloadString(String url) throws Exception { + HttpRequest req = HttpRequest.newBuilder().uri(URI.create(url)).GET().build(); + HttpResponse resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString()); + if (resp.statusCode() != 200) throw new IOException("HTTP " + resp.statusCode()); + return resp.body(); + } + + private void downloadFile(String url, Path target, String label) throws Exception { + if (!label.isEmpty()) { + ProgressBar.clearLine(); + System.out.println(ZAnsi.cyan("Скачивание " + label + "...")); + } + + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target)); + + if (response.statusCode() != 200) { + // Для ассетов 404 — это нормально, просто пропускаем + if (label.isEmpty()) { + return; // тихий пропуск для ассетов + } + throw new IOException("HTTP " + response.statusCode()); + } + + if (!label.isEmpty()) { + long size = Files.size(target); + ProgressBar.finish(label + " (" + ProgressBar.formatBytes(size) + ")"); + } + + } catch (Exception e) { + if (!label.isEmpty()) { + // Для важных файлов (client.jar, библиотеки, index) — ошибка + throw e; + } + // Для ассетов — просто пропускаем молча + } + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java new file mode 100644 index 0000000..912aa93 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java @@ -0,0 +1,162 @@ +package me.sashegdev.zernmc.launcher.minecraft.launch; + +import me.sashegdev.zernmc.launcher.minecraft.Instance; +import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +/** + * Генерирует полную команду запуска Minecraft (Vanilla / Fabric / Forge) + */ +public class LaunchCommandBuilder { + + private final Instance instance; + + public LaunchCommandBuilder(Instance instance) { + this.instance = instance; + } + + public List build(LaunchOptions options) throws Exception { + System.out.println(ZAnsi.cyan("Генерация команды запуска для " + instance.getName() + "...")); + + List command = new ArrayList<>(); + + // 1. Путь к Java + String javaPath = getJavaPath(); + command.add(javaPath); + + // 2. JVM аргументы + command.addAll(getJvmArguments(options)); + + // 3. Natives + Path nativesDir = instance.getPath().resolve("natives"); + command.add("-Djava.library.path=" + nativesDir.toAbsolutePath()); + + // 4. Classpath + String classpath = buildClasspath(); + command.add("-cp"); + command.add(classpath); + + // 5. Главный класс + String mainClass = getMainClass(); + command.add(mainClass); + + // 6. Аргументы Minecraft + command.addAll(getMinecraftArguments(options)); + + return command; + } + + private String getJavaPath() { + // Пока берём системную java. Позже можно добавить выбор из ~/.zernmc/jre/ + return "java"; + } + + private List getJvmArguments(LaunchOptions options) { + List jvmArgs = new ArrayList<>(); + + // Выделенная память + int ramMB = options.getMaxMemory() > 0 ? options.getMaxMemory() : 2048; + jvmArgs.add("-Xmx" + ramMB + "M"); + jvmArgs.add("-Xms" + Math.max(512, ramMB / 2) + "M"); + + // Стандартные оптимизации + jvmArgs.add("-XX:+UseG1GC"); + jvmArgs.add("-XX:+UnlockExperimentalVMOptions"); + jvmArgs.add("-XX:G1NewSizePercent=20"); + jvmArgs.add("-XX:G1ReservePercent=20"); + jvmArgs.add("-XX:MaxGCPauseMillis=50"); + jvmArgs.add("-XX:G1HeapRegionSize=32M"); + + // Дополнительные JVM аргументы из настроек пользователя + if (options.getExtraJvmArgs() != null && !options.getExtraJvmArgs().isEmpty()) { + jvmArgs.addAll(options.getExtraJvmArgs()); + } + + return jvmArgs; + } + + private String buildClasspath() throws Exception { + List paths = new ArrayList<>(); + + String versionId = getVersionId(); + Path versionsDir = instance.getPath().resolve("versions"); + + // 1. Основной jar версии (fabric-loader-...-1.20.1.jar) + paths.add(versionsDir.resolve(versionId).resolve(versionId + ".jar").toAbsolutePath().toString()); + + // 2. Все библиотеки + Path librariesDir = instance.getPath().resolve("libraries"); + if (Files.exists(librariesDir)) { + try (var stream = Files.walk(librariesDir)) { + stream.filter(p -> p.toString().endsWith(".jar")) + .map(p -> p.toAbsolutePath().toString()) + .forEach(paths::add); + } + } + + // Для Windows используем ";" вместо ":" + String separator = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":"; + return String.join(separator, paths); + } + + private String getMainClass() { + String loaderType = instance.getLoaderType().toLowerCase(); + + if ("fabric".equals(loaderType)) { + String loaderVer = instance.getLoaderVersion(); + if (loaderVer != null && loaderVer.startsWith("0.9")) { + return "net.fabricmc.loader.impl.launch.knot.KnotClient"; + } else { + // Для более новых версий Fabric (0.14+) + return "net.fabricmc.loader.impl.launch.knot.KnotClient"; + } + } + else if ("forge".equals(loaderType)) { + return "net.minecraftforge.client.loading.ClientModLoader"; + } + else { + return "net.minecraft.client.main.Main"; // Vanilla + } + } + + private List getMinecraftArguments(LaunchOptions options) { + List args = new ArrayList<>(); + + args.add("--version"); + args.add(instance.getName()); + + args.add("--gameDir"); + args.add(instance.getPath().toAbsolutePath().toString()); + + args.add("--assetsDir"); + args.add(instance.getPath().resolve("assets").toAbsolutePath().toString()); + + if (options.getUsername() != null) { + args.add("--username"); + args.add(options.getUsername()); + } else { + args.add("--username"); + args.add("Player"); + } + + // Можно добавить --width, --height, --server и т.д. позже + + return args; + } + + private String getVersionId() { + if ("vanilla".equalsIgnoreCase(instance.getLoaderType())) { + return instance.getMinecraftVersion(); + } else { + // Для Fabric/Forge версия выглядит как fabric-loader-... или forge-... + return instance.getMinecraftVersion() + "-" + instance.getLoaderType() + "-" + instance.getLoaderVersion(); + } + } + + +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/model/LaunchOptions.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/model/LaunchOptions.java new file mode 100644 index 0000000..13677a3 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/model/LaunchOptions.java @@ -0,0 +1,36 @@ +package me.sashegdev.zernmc.launcher.minecraft.model; + +import java.util.ArrayList; +import java.util.List; + +public class LaunchOptions { + private String username = "Player"; + private String uuid = "00000000-0000-0000-0000-000000000000"; + private String accessToken = "token"; + private int maxMemory = 4096; + private boolean fullscreen = false; + private String javaPath = "java"; + private List extraJvmArgs = new ArrayList<>(); + + // Геттеры и сеттеры + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + + public String getUuid() { return uuid; } + public void setUuid(String uuid) { this.uuid = uuid; } + + public String getAccessToken() { return accessToken; } + public void setAccessToken(String accessToken) { this.accessToken = accessToken; } + + public int getMaxMemory() { return maxMemory; } + public void setMaxMemory(int maxMemory) { this.maxMemory = maxMemory; } + + public boolean isFullscreen() { return fullscreen; } + public void setFullscreen(boolean fullscreen) { this.fullscreen = fullscreen; } + + public String getJavaPath() { return javaPath; } + public void setJavaPath(String javaPath) { this.javaPath = javaPath; } + + public List getExtraJvmArgs() { return extraJvmArgs; } + public void setExtraJvmArgs(List extraJvmArgs) { this.extraJvmArgs = extraJvmArgs; } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/model/MinecraftVersion.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/model/MinecraftVersion.java new file mode 100644 index 0000000..9e5dcc6 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/model/MinecraftVersion.java @@ -0,0 +1,27 @@ +package me.sashegdev.zernmc.launcher.minecraft.model; + +import java.time.LocalDateTime; + +public class MinecraftVersion { + private final String id; + private final String type; // release, snapshot, old_beta, old_alpha + private final LocalDateTime releaseTime; + private final String url; + + public MinecraftVersion(String id, String type, LocalDateTime releaseTime, String url) { + this.id = id; + this.type = type; + this.releaseTime = releaseTime; + this.url = url; + } + + public String getId() { return id; } + public String getType() { return type; } + public LocalDateTime getReleaseTime() { return releaseTime; } + public String getUrl() { return url; } + + @Override + public String toString() { + return id + " (" + type + ")"; + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/ui/ArrowMenu.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/ui/ArrowMenu.java new file mode 100644 index 0000000..2d948b0 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/ui/ArrowMenu.java @@ -0,0 +1,90 @@ +package me.sashegdev.zernmc.launcher.ui; + +import me.sashegdev.zernmc.launcher.utils.ZAnsi; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.utils.InfoCmp; + +import java.io.IOException; +import java.util.List; + +public class ArrowMenu { + + private final String title; + private final List options; + private int selected = 0; + private final Terminal terminal; + + private static final int VISIBLE_ITEMS = 7; // сколько строк показывать в списке + + public ArrowMenu(String title, List options) throws IOException { + this.title = title; + this.options = options; + this.terminal = TerminalBuilder.builder() + .system(true) + .jna(true) + .build(); + } + + public int show() throws IOException { + terminal.enterRawMode(); + terminal.puts(InfoCmp.Capability.clear_screen); + terminal.puts(InfoCmp.Capability.cursor_invisible); + + try { + while (true) { + printPagedMenu(); + int key = terminal.reader().read(); + + if (key == 'w' || key == 'W' || key == 'ц' || key == 'Ц') { // Up + selected = (selected - 1 + options.size()) % options.size(); + } + else if (key == 's' || key == 'S' || key == 'ы' || key == 'Ы') { // Down + selected = (selected + 1) % options.size(); + } + else if (key == 13 || key == 10) { // Enter + return selected; + } + else if (key == 27) { // Esc + return -1; + } + } + } finally { + terminal.puts(InfoCmp.Capability.cursor_visible); + terminal.close(); + } + } + + private void printPagedMenu() { + StringBuilder sb = new StringBuilder(); + sb.append("\033[H\033[2J"); + + // Заголовок (фиксированный) + sb.append(ZAnsi.header("=== ZernMC Launcher ===")).append("\n\n"); + sb.append(ZAnsi.yellow(title)).append("\n\n"); + + // Вычисляем диапазон отображаемых элементов + int start = Math.max(0, selected - (VISIBLE_ITEMS / 2)); + int end = Math.min(options.size(), start + VISIBLE_ITEMS); + + // Если в конце списка — подтягиваем вверх + if (end - start < VISIBLE_ITEMS && start > 0) { + start = Math.max(0, end - VISIBLE_ITEMS); + } + + for (int i = start; i < end; i++) { + String line = options.get(i); + if (i == selected) { + sb.append(ZAnsi.selected(line)).append("\n"); + } else { + sb.append(ZAnsi.white(" " + line)).append("\n"); + } + } + + // Подсказка внизу (фиксированная) + sb.append("\n") + .append(ZAnsi.white("W/S (Ц/Ы) - перемещение | Enter - выбрать | Esc - назад")); + + System.out.print(sb); + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Config.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Config.java new file mode 100644 index 0000000..9c31311 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Config.java @@ -0,0 +1,123 @@ +package me.sashegdev.zernmc.launcher.utils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +public class Config { + + private static final Path CONFIG_DIR = Path.of(System.getProperty("user.home"), ".zernmc"); + private static final Path CONFIG_FILE = CONFIG_DIR.resolve("launcher.properties"); + + private static final Properties props = new Properties(); + + // Настройки + private static int maxMemory = 4096; // будет перезаписано умной логикой + private static String serverUrl = "http://87.120.187.36:1582"; + private static String lastUsername = "Player"; + + static { + load(); + applySmartRamRecommendation(); + } + + private static void load() { + try { + Files.createDirectories(CONFIG_DIR); + if (Files.exists(CONFIG_FILE)) { + try (var is = Files.newInputStream(CONFIG_FILE)) { + props.load(is); + } + } + + maxMemory = Integer.parseInt(props.getProperty("maxMemory", "4096")); + serverUrl = props.getProperty("serverUrl", serverUrl); + lastUsername = props.getProperty("lastUsername", lastUsername); + + } catch (Exception e) { + System.err.println(ZAnsi.brightRed("Не удалось загрузить конфиг: ") + e.getMessage()); + } + } + + public static void save() { + try { + props.setProperty("maxMemory", String.valueOf(maxMemory)); + props.setProperty("serverUrl", serverUrl); + props.setProperty("lastUsername", lastUsername); + + try (var os = Files.newOutputStream(CONFIG_FILE)) { + props.store(os, "ZernMC Launcher Configuration"); + } + } catch (IOException e) { + System.err.println(ZAnsi.brightRed("Не удалось сохранить конфиг: ") + e.getMessage()); + } + } + + /** + * Умная рекомендация RAM: + * - минимум 1.5 GB + * - рекомендуется totalRAM - 30% + * - максимум 70% от доступной RAM + */ + private static void applySmartRamRecommendation() { + long totalRamMB = Runtime.getRuntime().maxMemory() / (1024 * 1024); // в MB + + // Рекомендуемое значение = total - 30% + long recommended = (long) (totalRamMB * 0.70); // 70% от доступной + + // Ограничения + recommended = Math.max(1536, recommended); // минимум 1.5 GB + recommended = Math.min(recommended, totalRamMB - 1024); // оставляем минимум 1 GB системе + + // Если текущее значение сильно отличается от рекомендуемого — корректируем + if (Math.abs(maxMemory - recommended) > 1024) { // разница больше 1 GB + maxMemory = (int) recommended; + save(); // сохраняем умную рекомендацию + System.out.println(ZAnsi.cyan("Автоматически рекомендовано RAM: " + maxMemory + " MB")); + } + } + + // Getters & Setters + public static int getMaxMemory() { + return maxMemory; + } + + public static void setMaxMemory(int memory) { + // Защита от слишком маленьких/больших значений + if (memory < 1024) memory = 1536; + if (memory > 32768) memory = 32768; + + maxMemory = memory; + save(); + } + + public static String getServerUrl() { + return serverUrl; + } + + public static String getLastUsername() { + return lastUsername; + } + + public static void setLastUsername(String username) { + lastUsername = username; + save(); + } + + public static Path getInstancesDir() { + return CONFIG_DIR.resolve("instances"); + } + + public static Path getJreDir() { + return CONFIG_DIR.resolve("jre"); + } + + /** + * Полезная информация для пользователя + */ + public static String getRamInfo() { + long totalMB = Runtime.getRuntime().maxMemory() / (1024 * 1024); + return "Доступно RAM: " + totalMB + " MB | Рекомендуется: " + maxMemory + " MB"; + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ConsoleUtils.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ConsoleUtils.java new file mode 100644 index 0000000..bde04d3 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ConsoleUtils.java @@ -0,0 +1,39 @@ +package me.sashegdev.zernmc.launcher.utils; + +import java.io.IOException; + +public class ConsoleUtils { + + public static void clearScreen() { + System.out.print("\033[H\033[2J"); + System.out.flush(); + } + + public static void pause() { + System.out.print(ZAnsi.white("\nНажмите Enter для продолжения...")); + try { + System.in.read(); + // Очищаем буфер ввода + while (System.in.available() > 0) { + System.in.read(); + } + } catch (IOException ignored) {} + } + + public static void printHeader(String subtitle) { + clearScreen(); + System.out.println(ZAnsi.header("=== ZernMC Launcher ===")); + if (subtitle != null && !subtitle.isEmpty()) { + System.out.println(ZAnsi.yellow(subtitle)); + } + System.out.println(); + } + + public static void printHeader() { + printHeader(null); + } + + public static void separator() { + System.out.println(ZAnsi.white("────────────────────────────────────────────────────────────")); + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Input.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Input.java new file mode 100644 index 0000000..4ac8c30 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Input.java @@ -0,0 +1,38 @@ +package me.sashegdev.zernmc.launcher.utils; + +import java.util.Scanner; + +public class Input { + + private static final Scanner scanner = new Scanner(System.in); + + public static String readLine() { + return scanner.nextLine().trim(); + } + + public static String readLine(String prompt) { + System.out.print(prompt); + return scanner.nextLine().trim(); + } + + public static int readInt(String prompt) { + while (true) { + try { + System.out.print(prompt); + return Integer.parseInt(scanner.nextLine().trim()); + } catch (NumberFormatException e) { + System.out.println(ZAnsi.brightRed("Некорректное число. Попробуйте ещё раз.")); + } + } + } + + public static int readInt(String prompt, int min, int max) { + while (true) { + int value = readInt(prompt); + if (value >= min && value <= max) { + return value; + } + System.out.println(ZAnsi.brightRed("Значение должно быть от " + min + " до " + max)); + } + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ProgressBar.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ProgressBar.java new file mode 100644 index 0000000..3f35a24 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ProgressBar.java @@ -0,0 +1,70 @@ +package me.sashegdev.zernmc.launcher.utils; + +import java.text.DecimalFormat; + +public class ProgressBar { + + private static final int BAR_LENGTH = 40; + private static final DecimalFormat DF = new DecimalFormat("#.##"); + + /** + * Прогресс по количеству файлов (для библиотек и общего прогресса) + */ + public static void show(String label, long current, long total, String unit) { + if (total <= 0) { + System.out.print("\r" + ZAnsi.cyan(label) + " ..."); + return; + } + double progress = (double) current / total; + int filled = (int) (progress * BAR_LENGTH); + String bar = "█".repeat(filled) + "░".repeat(BAR_LENGTH - filled); + int percent = (int) (progress * 100); + + String text = String.format("%s [%s] %3d%% (%d/%d %s)", + ZAnsi.cyan(label), bar, percent, current, total, unit); + + System.out.print("\r" + text); + System.out.flush(); + } + + /** + * Прогресс по байтам для одного файла (реальный прогресс) + */ + public static void showDownload(String label, long downloaded, long totalBytes) { + if (totalBytes <= 0) { + System.out.print("\r" + ZAnsi.cyan(label) + " ..."); + return; + } + + double progress = (double) downloaded / totalBytes; + int filled = (int) (progress * BAR_LENGTH); + String bar = "█".repeat(filled) + "░".repeat(BAR_LENGTH - filled); + String percent = DF.format(progress * 100); + + String text = String.format("%s [%s] %6s%% %s / %s", + ZAnsi.cyan(label), + bar, + percent, + formatBytes(downloaded), + formatBytes(totalBytes)); + + System.out.print("\r" + text); + System.out.flush(); + } + + public static void finish(String message) { + System.out.println("\r" + ZAnsi.brightGreen(message + " завершено ✓")); + System.out.flush(); + } + + public static void clearLine() { + System.out.print("\r" + " ".repeat(110) + "\r"); + System.out.flush(); + } + + public static String formatBytes(long bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return DF.format(bytes / 1024.0) + " KB"; + return DF.format(bytes / (1024.0 * 1024)) + " MB"; + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Version.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Version.java new file mode 100644 index 0000000..ffb4534 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Version.java @@ -0,0 +1,37 @@ +package me.sashegdev.zernmc.launcher.utils; + +public class Version { + + public static String getCurrentVersion() { + String version = Version.class.getPackage().getImplementationVersion(); + return (version != null && !version.isBlank()) ? version : "1.0.0"; + } + + /** + * Универсальное сравнение версий + * Возвращает true, если serverVersion новее currentVersion + */ + public static boolean isNewer(String current, String server) { + if (current == null || server == null) return false; + + // Убираем -SNAPSHOT для сравнения + current = current.replace("-SNAPSHOT", "").trim(); + server = server.replace("-SNAPSHOT", "").trim(); + + if (current.equals(server)) return false; + + String[] currentParts = current.split("\\."); + String[] serverParts = server.split("\\."); + + int maxLength = Math.max(currentParts.length, serverParts.length); + + for (int i = 0; i < maxLength; i++) { + int c = i < currentParts.length ? Integer.parseInt(currentParts[i]) : 0; + int s = i < serverParts.length ? Integer.parseInt(serverParts[i]) : 0; + + if (s > c) return true; + if (s < c) return false; + } + return false; + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZAnsi.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZAnsi.java new file mode 100644 index 0000000..86e7b9c --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZAnsi.java @@ -0,0 +1,80 @@ +package me.sashegdev.zernmc.launcher.utils; + +import org.fusesource.jansi.Ansi; +import org.fusesource.jansi.AnsiConsole; + +public class ZAnsi { + + //поддержка ANSI епта + public static void install() { + AnsiConsole.systemInstall(); + } + + public static void uninstall() { + AnsiConsole.systemUninstall(); + } + + // === Основные цвета === + public static String green(String text) { + return Ansi.ansi().fg(Ansi.Color.GREEN).a(text).reset().toString(); + } + + public static String brightGreen(String text) { + return Ansi.ansi().fgBright(Ansi.Color.GREEN).a(text).reset().toString(); + } + + public static String cyan(String text) { + return Ansi.ansi().fg(Ansi.Color.CYAN).a(text).reset().toString(); + } + + public static String yellow(String text) { + return Ansi.ansi().fg(Ansi.Color.YELLOW).a(text).reset().toString(); + } + + public static String brightYellow(String text) { + return Ansi.ansi().fgBright(Ansi.Color.YELLOW).a(text).reset().toString(); + } + + public static String red(String text) { + return Ansi.ansi().fg(Ansi.Color.RED).a(text).reset().toString(); + } + + public static String brightRed(String text) { + return Ansi.ansi().fgBright(Ansi.Color.RED).a(text).reset().toString(); + } + + public static String blue(String text) { + return Ansi.ansi().fg(Ansi.Color.BLUE).a(text).reset().toString(); + } + + public static String white(String text) { + return Ansi.ansi().fg(Ansi.Color.WHITE).a(text).reset().toString(); + } + + public static String brightWhite(String text) { + return Ansi.ansi().fgBright(Ansi.Color.WHITE).a(text).reset().toString(); + } + + // Стили + public static String bold(String text) { + return Ansi.ansi().bold().a(text).reset().toString(); + } + + public static String reset() { + return Ansi.ansi().reset().toString(); + } + + // Комбинированные удобные методы + public static String header(String text) { + return Ansi.ansi().fgBright(Ansi.Color.CYAN).bold().a(text).reset().toString(); + } + + public static String selected(String text) { + return Ansi.ansi() + .bgBright(Ansi.Color.WHITE) + .fgBlack() + .a(" > " + text + " ") + .reset() + .toString(); + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java new file mode 100644 index 0000000..cb46b57 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/ZHttpClient.java @@ -0,0 +1,125 @@ +package me.sashegdev.zernmc.launcher.utils; + +import java.io.IOException; +import java.net.URI; +//import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +public class ZHttpClient { + + private static final java.net.http.HttpClient client = java.net.http.HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + + private static final String BASE_URL = "http://87.120.187.36:1582"; + + public static String get(String endpoint) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + endpoint)) + .timeout(Duration.ofSeconds(15)) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode()); + } + + return response.body(); + } + + public static String getBaseUrl() { + return BASE_URL; + } + + public static String getLauncherVersion() throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/launcher/version")) + .timeout(Duration.ofSeconds(10)) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode()); + } + + return response.body(); + } + public static String getLauncherVersionInfo() throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/launcher/version")) + .timeout(Duration.ofSeconds(10)) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode()); + } + + return response.body(); + } + /** + * Получить список всех доступных версий Fabric Loader + */ + public static List getFabricLoaderVersions() throws IOException, InterruptedException { + String url = "https://meta.fabricmc.net/v2/versions/loader"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode()); + } + + // Парсим JSON массив + org.json.JSONArray array = new org.json.JSONArray(response.body()); + List versions = new ArrayList<>(); + + for (int i = 0; i < array.length(); i++) { + org.json.JSONObject obj = array.getJSONObject(i); + if (obj.has("version")) { + versions.add(obj.getString("version")); + } + } + + versions.sort((a, b) -> { + // Правильная семантическая сортировка версий + String[] partsA = a.split("\\+")[0].split("\\."); + String[] partsB = b.split("\\+")[0].split("\\."); + for (int i = 0; i < Math.min(partsA.length, partsB.length); i++) { + int cmp = Integer.compare(Integer.parseInt(partsB[i]), Integer.parseInt(partsA[i])); + if (cmp != 0) return cmp; + } + return b.compareTo(a); // fallback + }); + + return versions; + } + + public static String downloadString(String url) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode()); + } + return response.body(); + } +} \ No newline at end of file