diff --git a/launcher/dependency-reduced-pom.xml b/launcher/dependency-reduced-pom.xml index a448fcb..51a0b9f 100644 --- a/launcher/dependency-reduced-pom.xml +++ b/launcher/dependency-reduced-pom.xml @@ -3,7 +3,7 @@ 4.0.0 me.sashegdev ZernMCLauncher - 1.0.2 + 1.0.3 diff --git a/launcher/pom.xml b/launcher/pom.xml index c55decf..bf3d3a3 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -6,7 +6,7 @@ 4.0.0 me.sashegdev ZernMCLauncher - 1.0.2 + 1.0.4 jar diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java index 3604206..4419d62 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/LaunchMenu.java @@ -3,6 +3,8 @@ 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.PackDownloader; +import me.sashegdev.zernmc.launcher.minecraft.ServerPack; import me.sashegdev.zernmc.launcher.minecraft.installer.VersionInstaller; import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions; import me.sashegdev.zernmc.launcher.minecraft.model.MinecraftVersion; @@ -35,7 +37,7 @@ public class LaunchMenu { int choice = menu.show(); if (choice == -1) break; - if (choice == options.size() - 1) break; // Назад + if (choice == options.size() - 1) break; if (choice == instances.size()) { installNewPack(); @@ -47,72 +49,219 @@ public class LaunchMenu { } } - private void installNewPack() throws IOException { + private void installNewPack() throws Exception { + ConsoleUtils.clearScreen(); + + List options = List.of( + "Установить сборку с сервера ZernMC", + "Установить Vanilla Minecraft", + "Создать сборку вручную (Fabric/Forge)", + "Назад" + ); + + ArrowMenu menu = new ArrowMenu("Установка новой сборки", options); + int choice = menu.show(); + + if (choice == -1 || choice == 3) return; + + switch (choice) { + case 0 -> { + try { + installServerPack(); + } catch (Exception e) { + System.out.println(ZAnsi.brightRed("Ошибка: " + e.getMessage())); + e.printStackTrace(); + ConsoleUtils.pause(); + } + } + case 1 -> createVanillaInstance(); + case 2 -> createCustomInstance(); + } + } + + private void installServerPack() throws Exception { + ConsoleUtils.clearScreen(); + System.out.println(ZAnsi.cyan("Получение списка доступных сборок с сервера...")); + + PackDownloader tempDownloader = new PackDownloader(null); + List availablePacks = tempDownloader.getAvailablePacks(); + + if (availablePacks.isEmpty()) { + System.out.println(ZAnsi.yellow("Нет доступных сборок на сервере.")); + ConsoleUtils.pause(); + return; + } + + // Исправлено: убраны спецсимволы для Windows + List options = availablePacks.stream() + .map(p -> String.format("%s [%s + %s v%d] - %d файлов", + p.getName(), + p.getMinecraftVersion(), + p.getLoaderType(), + p.getVersion(), + p.getFilesCount())) + .collect(Collectors.toList()); + options.add("Назад"); + + ArrowMenu menu = new ArrowMenu("Выберите сборку для установки", options); + int choice = menu.show(); + + if (choice == -1 || choice == options.size() - 1) return; + + ServerPack selected = availablePacks.get(choice); + + // Запрашиваем имя для локальной сборки + ConsoleUtils.clearScreen(); + System.out.println(ZAnsi.header("Установка сборки: " + selected.getName())); + System.out.println(ZAnsi.white(" Minecraft: ") + selected.getMinecraftVersion()); + System.out.println(ZAnsi.white(" Лоадер: ") + selected.getLoaderType() + " " + selected.getLoaderVersion()); + System.out.println(ZAnsi.white(" Версия: v") + selected.getVersion()); + System.out.println(ZAnsi.white(" Файлов: ") + selected.getFilesCount()); + System.out.println(); + + System.out.print(ZAnsi.white("Введите название локальной сборки (Enter = использовать имя пака): ")); + String localName = Input.readLine(); + if (localName.isEmpty()) { + localName = selected.getName(); + } + + // Проверяем, существует ли уже такая сборка + if (InstanceManager.getInstance(localName) != null) { + System.out.println(ZAnsi.brightRed("Сборка с таким именем уже существует!")); + ConsoleUtils.pause(); + return; + } + + // Создаем инстанс + InstanceManager.createInstanceFolder(localName); + Instance newInstance = InstanceManager.getInstance(localName); + + // Устанавливаем сборку + PackDownloader packDownloader = new PackDownloader(newInstance); + boolean success = packDownloader.installOrUpdatePack(selected.getName(), selected); + + if (success) { + System.out.println(ZAnsi.brightGreen("\n[OK] Сборка '" + localName + "' успешно установлена!")); + } else { + System.out.println(ZAnsi.brightRed("\n[FAIL] Не удалось установить сборку.")); + } + + ConsoleUtils.pause(); + } + + private void createVanillaInstance() throws Exception { 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())); + + 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(); + + String packName = askPackName(); + if (packName == null) return; + + if (InstanceManager.getInstance(packName) != null) { + System.out.println(ZAnsi.brightRed("Сборка с таким именем уже существует!")); + ConsoleUtils.pause(); + 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("\n[OK] Vanilla сборка '" + packName + "' успешно создана!")); + } else { + System.out.println(ZAnsi.brightRed("\n[FAIL] Не удалось создать сборку.")); + } + + ConsoleUtils.pause(); + } + private void createCustomInstance() throws Exception { + ConsoleUtils.clearScreen(); + System.out.println(ZAnsi.cyan("Получение списка версий Minecraft...")); + + 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(); + 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; + + if (InstanceManager.getInstance(packName) != null) { + System.out.println(ZAnsi.brightRed("Сборка с таким именем уже существует!")); + ConsoleUtils.pause(); + 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[OK] Сборка '" + packName + "' успешно установлена!")); + } else { + System.out.println(ZAnsi.brightRed("\n[FAIL] Не удалось установить сборку.")); + } + ConsoleUtils.pause(); } @@ -120,7 +269,7 @@ public class LaunchMenu { private List buildLoaderOptions(String mcVersion) { List options = new ArrayList<>(); - + if (isFabricSupported(mcVersion)) { options.add("Fabric"); } @@ -129,25 +278,22 @@ public class LaunchMenu { } 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(); @@ -155,70 +301,120 @@ public class LaunchMenu { System.out.println(ZAnsi.white("Версия: " + instance.getMinecraftVersion())); System.out.println(ZAnsi.white("Лоадер: " + instance.getLoaderType() + (instance.getLoaderVersion() != null ? " " + instance.getLoaderVersion() : ""))); - + + if (instance.isServerPack()) { + System.out.println(ZAnsi.green("Серверная сборка: v" + instance.getServerVersion())); + } + List options = new ArrayList<>(); options.add("Запустить сборку"); + if (instance.isServerPack()) { + options.add("Проверить обновления"); + } options.add("Изменить версию лоадера"); options.add("Удалить сборку"); options.add("Назад"); - + ArrowMenu menu = new ArrowMenu("Действия", options); int choice = menu.show(); - - if (choice == -1 || choice == 3) return; // Esc или Назад - + + if (choice == -1 || choice == options.size() - 1) return; + switch (choice) { case 0 -> launchExistingInstance(instance); - case 1 -> changeLoaderVersion(instance); - case 2 -> deleteInstance(instance); + case 1 -> { + if (instance.isServerPack()) { + checkAndUpdateServerPack(instance); + } else { + changeLoaderVersion(instance); + } + } + case 2 -> { + if (instance.isServerPack()) { + changeLoaderVersion(instance); + } else { + deleteInstance(instance); + } + } + case 3 -> deleteInstance(instance); } } } - + + private void checkAndUpdateServerPack(Instance instance) throws Exception { + ConsoleUtils.clearScreen(); + System.out.println(ZAnsi.cyan("Проверка обновлений для " + instance.getName())); + + PackDownloader downloader = new PackDownloader(instance); + boolean hasUpdate = downloader.checkForUpdates(instance.getServerPackName()); + + if (!hasUpdate) { + System.out.println(ZAnsi.green("Сборка актуальна (v" + instance.getServerVersion() + ")")); + ConsoleUtils.pause(); + return; + } + + System.out.println(ZAnsi.brightYellow("Доступно обновление!")); + if (Input.confirm("Обновить сборку")) { + boolean success = downloader.updatePack(instance.getServerPackName()); + if (success) { + System.out.println(ZAnsi.brightGreen("Сборка успешно обновлена!")); + } else { + System.out.println(ZAnsi.brightRed("Не удалось обновить сборку.")); + } + } else { + System.out.println(ZAnsi.yellow("Обновление отменено.")); + } + + ConsoleUtils.pause(); + } + 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 + "...")); - + + 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("Версия лоадера успешно изменена!")); + } else { + System.out.println(ZAnsi.brightRed("Не удалось изменить версию лоадера.")); } } catch (Exception e) { System.out.println(ZAnsi.brightRed("Ошибка при смене лоадера: " + e.getMessage())); } - + ConsoleUtils.pause(); } - + private void deleteInstance(Instance instance) throws IOException { ConsoleUtils.clearScreen(); @@ -226,15 +422,15 @@ public class LaunchMenu { "Да, удалить сборку", "Нет, отменить" ); - + ArrowMenu confirmMenu = new ArrowMenu( "Вы действительно хотите удалить сборку '" + instance.getName() + "'?", confirmOptions ); - + int choice = confirmMenu.show(); - - if (choice == 0) { // "Да, удалить" + + if (choice == 0) { boolean deleted = InstanceManager.deleteInstance(instance.getName()); if (deleted) { System.out.println(ZAnsi.brightGreen("Сборка '" + instance.getName() + "' успешно удалена.")); @@ -244,105 +440,89 @@ public class LaunchMenu { } 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 + .limit(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-" + .map(v -> v.substring(mcVersion.length() + 1)) .collect(Collectors.toList()); - + if (compatibleVersions.isEmpty()) { System.out.println(ZAnsi.yellow("Не найдено совместимых версий Forge для " + mcVersion)); ConsoleUtils.pause(); return null; } - + List options = compatibleVersions.stream() + .limit(30) .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 = Input.readLine(); // используем наш Input + String name = Input.readLine(); 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())); + e.printStackTrace(); } - + 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, если его нет + String xml = ZHttpClient.downloadString(metadataUrl); - // Парсим простым способом (без XML парсера) List versions = new ArrayList<>(); int index = 0; @@ -350,15 +530,14 @@ public class LaunchMenu { 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/UpdateMenu.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/UpdateMenu.java index 5e4f802..41f8b20 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/UpdateMenu.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/menu/UpdateMenu.java @@ -1,10 +1,18 @@ 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.PackDownloader; import me.sashegdev.zernmc.launcher.ui.ArrowMenu; import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; +import me.sashegdev.zernmc.launcher.utils.Input; +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 UpdateMenu { @@ -23,14 +31,122 @@ public class UpdateMenu { ConsoleUtils.clearScreen(); if (choice == 0) { - System.out.println("Проверка обновлений сборки..."); - System.out.println("Дифф обновлений пока в заглушке (сборки ещё не загружены)"); - System.out.println(" Эндпоинт: POST /pack/{pack_name}/diff"); + try { + checkPackUpdates(); + } catch (Exception e) { + System.out.println(ZAnsi.brightRed("Ошибка: " + e.getMessage())); + e.printStackTrace(); + ConsoleUtils.pause(); + } } else { - System.out.println("Проверка обновлений лаунчера..."); - System.out.println("Версия лаунчера актуальна (заглушка)"); + checkLauncherUpdates(); } + } + private void checkPackUpdates() throws Exception { + System.out.println(ZAnsi.cyan("Проверка обновлений сборок...")); + + List instances = InstanceManager.getAllInstances(); + List serverInstances = instances.stream() + .filter(Instance::isServerPack) + .collect(Collectors.toList()); + + if (serverInstances.isEmpty()) { + System.out.println(ZAnsi.yellow("Нет сборок, установленных с сервера.")); + ConsoleUtils.pause(); + return; + } + + System.out.println(ZAnsi.cyan("\nПроверка обновлений для серверных сборок:\n")); + + boolean hasUpdates = false; + List updatableInstances = new ArrayList<>(); + + for (Instance instance : serverInstances) { + PackDownloader downloader = new PackDownloader(instance); + + try { + boolean hasUpdate = downloader.checkForUpdates(instance.getServerPackName()); + if (hasUpdate) { + System.out.println(ZAnsi.yellow(instance.getName() + " - Есть обновление!")); + updatableInstances.add(instance); + hasUpdates = true; + } else { + System.out.println(ZAnsi.green(instance.getName() + " - Актуальна")); + } + } catch (Exception e) { + System.out.println(ZAnsi.red(instance.getName() + " - Ошибка проверки: " + e.getMessage())); + } + } + + if (!hasUpdates) { + System.out.println(ZAnsi.green("\nВсе сборки актуальны!")); + 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() + "...")); + PackDownloader downloader = new PackDownloader(instance); + + try { + boolean success = downloader.updatePack(instance.getServerPackName()); + if (success) { + System.out.println(ZAnsi.brightGreen(instance.getName() + " обновлен")); + } else { + System.out.println(ZAnsi.brightRed(instance.getName() + " не удалось обновить")); + } + } catch (Exception e) { + System.out.println(ZAnsi.brightRed(instance.getName() + ": " + e.getMessage())); + } + } else { + System.out.println(ZAnsi.yellow(" Пропущено: " + instance.getName())); + } + } + ConsoleUtils.pause(); } + + private void checkLauncherUpdates() { + System.out.println(ZAnsi.cyan("Проверка обновлений лаунчера...")); + + 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); + + if (me.sashegdev.zernmc.launcher.utils.Version.isNewer(currentVersion, serverVersion)) { + System.out.println(ZAnsi.brightYellow("\nДоступна новая версия!")); + if (Input.confirm("Обновить лаунчер?")) { + // Обновление будет при следующем запуске + System.out.println(ZAnsi.green("Лаунчер будет обновлен при следующем запуске.")); + } + } else { + System.out.println(ZAnsi.brightGreen("Лаунчер актуален.")); + } + } catch (Exception e) { + System.out.println(ZAnsi.yellow("Не удалось проверить обновления лаунчера.")); + System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage()); + } + + ConsoleUtils.pause(); + } + + private String extractVersion(String json) { + try { + int start = json.indexOf("\"version\""); + if (start == -1) return "unknown"; + start = json.indexOf("\"", start + 9) + 1; + int end = json.indexOf("\"", start); + return json.substring(start, end); + } catch (Exception e) { + return "unknown"; + } + } } \ 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 deleted file mode 100644 index 8b009ff..0000000 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Installer.java +++ /dev/null @@ -1,19 +0,0 @@ -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 index e5d8fa2..9068d40 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/Instance.java @@ -8,7 +8,7 @@ import java.nio.file.Path; -// ЭТОТ КЛАСС РАБОТАЕТ НЕ ТРОГАТЬ ТОТ КТО БУДЕТ ЧИТАТЬ +// ЭТОТ КЛАСС РАБОТАЕТ НЕ ТРОГАТЬ ТОТ КТО БУДЕТ ЧИТАТЬ (на момент 1.0.2) public class Instance { private final String name; private final Path path; @@ -17,6 +17,9 @@ public class Instance { private String loaderType; // vanilla, fabric, forge private String loaderVersion; private String assetIndex; + private boolean isServerPack; // флаг, что это сборка с сервера + private int serverVersion; // версия сборки на сервере + private String serverPackName; // имя пака на сервере private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); @@ -57,6 +60,34 @@ public class Instance { saveMetadata(); } + + public boolean isServerPack() { + return isServerPack; + } + + public void setServerPack(boolean serverPack) { + this.isServerPack = serverPack; + saveMetadata(); + } + + public int getServerVersion() { + return serverVersion; + } + + public void setServerVersion(int serverVersion) { + this.serverVersion = serverVersion; + saveMetadata(); + } + + public String getServerPackName() { + return serverPackName; + } + + public void setServerPackName(String serverPackName) { + this.serverPackName = serverPackName; + saveMetadata(); + } + @Override public String toString() { StringBuilder sb = new StringBuilder(name); @@ -67,6 +98,9 @@ public class Instance { if (loaderVersion != null) sb.append(" ").append(loaderVersion); } sb.append("]"); + if (isServerPack) { + sb.append("v").append(serverVersion); + } } else { sb.append(" [?]"); } @@ -84,12 +118,18 @@ public class Instance { this.loaderType = meta.loaderType; this.loaderVersion = meta.loaderVersion; this.assetIndex = meta.assetIndex; + this.isServerPack = meta.isServerPack; + this.serverVersion = meta.serverVersion; + this.serverPackName = meta.serverPackName; } catch (Exception ignored) {} } private void saveMetadata() { Path metaFile = path.resolve("instance.json"); - InstanceMeta meta = new InstanceMeta(minecraftVersion, loaderType, loaderVersion, assetIndex); + InstanceMeta meta = new InstanceMeta( + minecraftVersion, loaderType, loaderVersion, assetIndex, + isServerPack, serverVersion, serverPackName + ); try { Files.writeString(metaFile, GSON.toJson(meta)); } catch (IOException e) { @@ -102,13 +142,22 @@ public class Instance { String loaderType; String loaderVersion; String assetIndex; + boolean isServerPack = false; + int serverVersion = 0; + String serverPackName; + public InstanceMeta(String minecraftVersion, String loaderType, - String loaderVersion, String assetIndex) { + String loaderVersion, String assetIndex, + boolean isServerPack, int serverVersion, + String serverPackName) { this.minecraftVersion = minecraftVersion; this.loaderType = loaderType; this.loaderVersion = loaderVersion; this.assetIndex = assetIndex; + this.isServerPack = isServerPack; + this.serverVersion = serverVersion; + this.serverPackName = serverPackName; } } } \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java new file mode 100644 index 0000000..85d42b5 --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/PackDownloader.java @@ -0,0 +1,544 @@ +package me.sashegdev.zernmc.launcher.minecraft; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import me.sashegdev.zernmc.launcher.utils.ProgressBar; +import me.sashegdev.zernmc.launcher.utils.ZAnsi; +import me.sashegdev.zernmc.launcher.utils.ZHttpClient; + +import java.io.*; +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.security.MessageDigest; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class PackDownloader { + + private final Instance instance; + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + private final HttpClient httpClient = HttpClient.newHttpClient(); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + + public PackDownloader(Instance instance) { + this.instance = instance; + } + + /** + * Получить список доступных паков с сервера + */ + public List getAvailablePacks() throws Exception { + String response = ZHttpClient.get("/packs"); + + // Для отладки - выведем ответ сервера + System.out.println(ZAnsi.cyan("Ответ сервера: " + response)); + + JsonObject root = JsonParser.parseString(response).getAsJsonObject(); + + // Проверяем, есть ли поле "packs" + if (!root.has("packs")) { + System.out.println(ZAnsi.yellow("Сервер вернул неожиданный формат ответа")); + return new ArrayList<>(); + } + + JsonArray packsArray = root.getAsJsonArray("packs"); + List result = new ArrayList<>(); + + for (JsonElement elem : packsArray) { + JsonObject pack = elem.getAsJsonObject(); + + // Пропускаем паки с ошибками + if (pack.has("error")) { + System.out.println(ZAnsi.yellow("Пак имеет ошибку: " + pack.get("error").getAsString())); + continue; + } + + // Пропускаем паки со статусом not_scanned + if (pack.has("status") && "not_scanned".equals(pack.get("status").getAsString())) { + System.out.println(ZAnsi.yellow("Пак " + pack.get("name").getAsString() + " не отсканирован на сервере")); + continue; + } + + try { + // Пробуем получить name или pack_name (разные форматы) + String name = null; + if (pack.has("name")) { + name = pack.get("name").getAsString(); + } else if (pack.has("pack_name")) { + name = pack.get("pack_name").getAsString(); + } else { + continue; // Пропускаем если нет имени + } + + int version = pack.has("version") ? pack.get("version").getAsInt() : 0; + + // Получаем остальные поля (могут отсутствовать) + String minecraftVersion = pack.has("minecraft_version") ? pack.get("minecraft_version").getAsString() : "unknown"; + String loaderType = pack.has("loader_type") ? pack.get("loader_type").getAsString() : "vanilla"; + String loaderVersion = pack.has("loader_version") && !pack.get("loader_version").isJsonNull() ? pack.get("loader_version").getAsString() : ""; + int filesCount = pack.has("files_count") ? pack.get("files_count").getAsInt() : 0; + + // Парсим дату, если есть + LocalDateTime updatedAt = null; + if (pack.has("updated_at") && !pack.get("updated_at").isJsonNull()) { + try { + updatedAt = parseDateTime(pack.get("updated_at").getAsString()); + } catch (Exception e) { + // Игнорируем ошибки парсинга даты + } + } + + result.add(new ServerPack(name, version, minecraftVersion, + loaderType, loaderVersion, updatedAt, filesCount)); + } catch (Exception e) { + System.err.println(ZAnsi.yellow("Ошибка парсинга пака: " + e.getMessage())); + } + } + + return result; + } + + /** + * Получить манифест пака + */ + public PackManifest getPackManifest(String packName) throws Exception { + String response = ZHttpClient.get("/pack/" + packName); + return gson.fromJson(response, PackManifest.class); + } + + /** + * Установить или обновить сборку с сервера + */ + public boolean installOrUpdatePack(String packName, ServerPack serverPack) throws Exception { + System.out.println(ZAnsi.cyan("Установка сборки " + packName + " с сервера...")); + + // 1. Получаем манифест + PackManifest manifest = getPackManifest(packName); + + // 2. Сначала устанавливаем Minecraft + Loader через MinecraftLib + MinecraftLib lib = new MinecraftLib(instance); + + System.out.println(ZAnsi.cyan("Установка Minecraft " + manifest.getMinecraftVersion() + "...")); + + boolean needsMinecraftInstall = instance.getMinecraftVersion() == null || + !instance.getMinecraftVersion().equals(manifest.getMinecraftVersion()); + + if (needsMinecraftInstall) { + if ("fabric".equalsIgnoreCase(manifest.getLoaderType())) { + boolean success = lib.installFabric(manifest.getMinecraftVersion(), manifest.getLoaderVersion()); + if (!success) { + System.err.println(ZAnsi.brightRed("Не удалось установить Fabric")); + return false; + } + } else if ("forge".equalsIgnoreCase(manifest.getLoaderType())) { + boolean success = lib.installForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion()); + if (!success) { + System.err.println(ZAnsi.brightRed("Не удалось установить Forge")); + return false; + } + } else { + boolean success = lib.installMinecraft(manifest.getMinecraftVersion()); + if (!success) { + System.err.println(ZAnsi.brightRed("Не удалось установить Vanilla Minecraft")); + return false; + } + } + } else { + System.out.println(ZAnsi.green("Minecraft уже установлен, пропускаем...")); + } + + // 3. Сканируем локальные файлы ТОЛЬКО если есть файлы для скачивания + Map localFiles = scanLocalFiles(); + + // Если в сборке нет файлов (только vanilla/loader), пропускаем diff + if (manifest.files == null || manifest.files.isEmpty()) { + System.out.println(ZAnsi.green("Сборка не содержит дополнительных файлов")); + + // Обновляем метаданные инстанса + instance.setServerPack(true); + instance.setServerPackName(packName); + instance.setServerVersion(manifest.getVersion()); + instance.setMinecraftVersion(manifest.getMinecraftVersion()); + instance.setLoaderType(manifest.getLoaderType()); + instance.setLoaderVersion(manifest.getLoaderVersion()); + instance.setAssetIndex(manifest.getAssetIndex()); + + System.out.println(ZAnsi.brightGreen("Сборка успешно установлена!")); + return true; + } + + // 4. Отправляем diff запрос + System.out.println(ZAnsi.cyan("Проверка файлов сборки...")); + DiffResponse diff = getDiff(packName, localFiles); + + // 5. Применяем изменения + boolean success = applyDiff(diff, packName); + + if (success) { + // 6. Обновляем метаданные инстанса + instance.setServerPack(true); + instance.setServerPackName(packName); + instance.setServerVersion(manifest.getVersion()); + instance.setMinecraftVersion(manifest.getMinecraftVersion()); + instance.setLoaderType(manifest.getLoaderType()); + instance.setLoaderVersion(manifest.getLoaderVersion()); + instance.setAssetIndex(manifest.getAssetIndex()); + + System.out.println(ZAnsi.brightGreen("Сборка успешно установлена!")); + } + + return success; + } + + /** + * Проверить наличие обновлений для серверной сборки + */ + public boolean checkForUpdates(String packName) throws Exception { + if (!instance.isServerPack()) return false; + + PackManifest manifest = getPackManifest(packName); + int serverVersion = manifest.getVersion(); + int localVersion = instance.getServerVersion(); + + return serverVersion > localVersion; + } + + /** + * Обновить существующую серверную сборку + */ + public boolean updatePack(String packName) throws Exception { + System.out.println(ZAnsi.cyan("Проверка обновлений для " + instance.getName() + "...")); + + PackManifest manifest = getPackManifest(packName); + int serverVersion = manifest.getVersion(); + + if (serverVersion <= instance.getServerVersion()) { + System.out.println(ZAnsi.green("Сборка уже актуальна (v" + instance.getServerVersion() + ")")); + return true; + } + + System.out.println(ZAnsi.yellow("Доступно обновление: v" + instance.getServerVersion() + " → v" + serverVersion)); + + // Сканируем локальные файлы + Map localFiles = scanLocalFiles(); + + // Получаем diff + DiffResponse diff = getDiff(packName, localFiles); + + // Применяем изменения + boolean success = applyDiff(diff, packName); + + if (success) { + instance.setServerVersion(serverVersion); + System.out.println(ZAnsi.brightGreen("Сборка обновлена до v" + serverVersion)); + } + + return success; + } + + /** + * Сканирование локальных файлов и вычисление хешей + */ + private Map scanLocalFiles() throws IOException { + Map files = new HashMap<>(); + Path instancePath = instance.getPath(); + + // Игнорируемые директории + Set ignoredDirs = Set.of( + "resourcepacks", "shaderpacks", "saves", "logs", + "crash-reports", "screenshots", "journeymap", "config", + "natives", "assets", "libraries", "versions", "cache" + ); + + if (!Files.exists(instancePath)) { + return files; + } + + Files.walk(instancePath) + .filter(Files::isRegularFile) + .forEach(file -> { + Path relative = instancePath.relativize(file); + String path = relative.toString().replace("\\", "/"); + + // Проверяем, не в игнорируемой ли директории + for (String ignored : ignoredDirs) { + if (path.startsWith(ignored + "/") || path.startsWith(ignored + "\\")) { + return; + } + } + + try { + String hash = calculateHash(file); + files.put(path, hash); + } catch (Exception e) { + // Пропускаем файлы, которые не можем прочитать + } + }); + + return files; + } + + /** + * Отправить diff запрос на сервер + */ + private DiffResponse getDiff(String packName, Map localFiles) throws Exception { + String json = gson.toJson(localFiles); + + System.out.println(ZAnsi.cyan("Отправка diff запроса для " + packName)); + System.out.println(ZAnsi.cyan("JSON размер: " + json.length() + " байт")); + System.out.println(ZAnsi.cyan("JSON тело: " + json)); + + String baseUrl = ZHttpClient.getBaseUrl(); + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.substring(0, baseUrl.length() - 1); + } + String url = baseUrl + "/pack/" + packName + "/diff"; + + System.out.println(ZAnsi.cyan("URL: " + url)); + + // ПРОБЛЕМА: стандартный HttpClient может отправлять chunked encoding + // РЕШЕНИЕ: используем HttpURLConnection вместо HttpClient + + java.net.HttpURLConnection connection = null; + try { + java.net.URL urlObj = new java.net.URL(url); + connection = (java.net.HttpURLConnection) urlObj.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("Content-Length", String.valueOf(json.getBytes("UTF-8").length)); + connection.setDoOutput(true); + connection.setConnectTimeout(30000); + connection.setReadTimeout(30000); + + // Отправляем JSON + try (java.io.OutputStream os = connection.getOutputStream()) { + byte[] input = json.getBytes("UTF-8"); + os.write(input, 0, input.length); + os.flush(); + } + + int responseCode = connection.getResponseCode(); + System.out.println(ZAnsi.cyan("Diff ответ: HTTP " + responseCode)); + + // Читаем ответ + 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"))) { + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + } + + String responseBody = response.toString(); + System.out.println(ZAnsi.cyan("Тело ответа: " + responseBody)); + + if (responseCode != 200) { + throw new IOException("HTTP " + responseCode + ": " + responseBody); + } + + return gson.fromJson(responseBody, DiffResponse.class); + + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + /** + * Применить diff (скачать новые файлы, удалить старые) + */ + 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() + " файлов"); + + // Создаем директории если нужно + try { + Files.createDirectories(instance.getPath()); + } catch (IOException e) { + System.err.println(ZAnsi.red("Ошибка создания директорий: " + e.getMessage())); + return false; + } + + // Удаляем файлы + for (String filePath : diff.getToDelete()) { + Path fullPath = instance.getPath().resolve(filePath); + try { + if (Files.deleteIfExists(fullPath)) { + System.out.println(ZAnsi.yellow(" Удален: " + filePath)); + } + } catch (IOException e) { + System.err.println(ZAnsi.red(" Ошибка удаления " + filePath + ": " + e.getMessage())); + } + } + + // Скачиваем файлы + AtomicInteger downloaded = new AtomicInteger(0); + int total = diff.getToDownload().size(); + + for (FileInfo file : diff.getToDownload()) { + String path = file.getPath(); + Path fullPath = instance.getPath().resolve(path); + + try { + // Создаем директории + Files.createDirectories(fullPath.getParent()); + + // Скачиваем файл + downloadFile(file, fullPath); + + // Проверяем хеш + String actualHash = calculateHash(fullPath); + if (!actualHash.equals(file.getHash())) { + throw new IOException("Хеш не совпадает! Ожидался: " + file.getHash() + + ", получен: " + actualHash); + } + + downloaded.incrementAndGet(); + if (total > 0) { + ProgressBar.show("Скачивание", downloaded.get(), total, "файлов"); + } + + } catch (Exception e) { + System.err.println("\n" + ZAnsi.red(" Ошибка скачивания " + path + ": " + e.getMessage())); + return false; + } + } + + if (total > 0) { + ProgressBar.finish("Скачивание"); + } + + return true; + } + + /** + * Скачать один файл с сервера + */ + private void downloadFile(FileInfo file, Path destination) throws Exception { + String url = ZHttpClient.getBaseUrl() + file.getUrl(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(java.net.URI.create(url)) + .GET() + .build(); + + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofInputStream()); + + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode()); + } + + // Скачиваем с прогрессом + try (InputStream in = response.body(); + FileOutputStream out = new FileOutputStream(destination.toFile())) { + + byte[] buffer = new byte[8192]; + int bytesRead; + long totalRead = 0; + long fileSize = file.getSize(); + + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + totalRead += bytesRead; + + if (fileSize > 0 && totalRead % 8192 == 0) { + ProgressBar.showDownload(" " + file.getPath(), totalRead, fileSize); + } + } + } + + ProgressBar.clearLine(); + } + + /** + * Вычисление SHA256 хеша файла + */ + private String calculateHash(Path file) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + + try (InputStream in = Files.newInputStream(file)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + digest.update(buffer, 0, bytesRead); + } + } + + byte[] hashBytes = digest.digest(); + StringBuilder sb = new StringBuilder(); + for (byte b : hashBytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + /** + * Парсинг даты из строки + */ + private LocalDateTime parseDateTime(String dateTimeStr) { + try { + return LocalDateTime.parse(dateTimeStr, DATE_FORMATTER); + } catch (Exception e) { + return null; + } + } + + // ====================== Вложенные классы ====================== + + public static class PackManifest { + private String pack_name; + private int version; + private String minecraft_version; + private String loader_type; + private String loader_version; + private String asset_index; + private Map files; + + public String getPackName() { return pack_name; } + public int getVersion() { return version; } + public String getMinecraftVersion() { return minecraft_version; } + public String getLoaderType() { return loader_type; } + public String getLoaderVersion() { return loader_version; } + public String getAssetIndex() { return asset_index != null ? asset_index : minecraft_version; } + public Map getFiles() { return files; } + public boolean isEmpty() { return files == null || files.isEmpty(); } + } + + public static class DiffResponse { + private int version; + private List to_download; + private List to_delete; + private List to_update; + + public int getVersion() { return version; } + public List getToDownload() { return to_download != null ? to_download : new ArrayList<>(); } + public List getToDelete() { return to_delete != null ? to_delete : new ArrayList<>(); } + public List getToUpdate() { return to_update != null ? to_update : new ArrayList<>(); } + } + + public static class FileInfo { + private String path; + private String url; + private long size; + private String hash; + + public String getPath() { return path; } + public String getUrl() { return url; } + public long getSize() { return size; } + public String getHash() { return hash; } + } +} \ No newline at end of file diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/ServerPack.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/ServerPack.java new file mode 100644 index 0000000..9dfd9ee --- /dev/null +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/ServerPack.java @@ -0,0 +1,46 @@ +package me.sashegdev.zernmc.launcher.minecraft; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class ServerPack { + private final String name; + private final int version; + private final String minecraftVersion; + private final String loaderType; + private final String loaderVersion; + private final LocalDateTime updatedAt; + private final int filesCount; + + public ServerPack(String name, int version, String minecraftVersion, + String loaderType, String loaderVersion, + LocalDateTime updatedAt, int filesCount) { + this.name = name; + this.version = version; + this.minecraftVersion = minecraftVersion; + this.loaderType = loaderType; + this.loaderVersion = loaderVersion; + this.updatedAt = updatedAt; + this.filesCount = filesCount; + } + + public String getName() { return name; } + public int getVersion() { return version; } + public String getMinecraftVersion() { return minecraftVersion; } + public String getLoaderType() { return loaderType; } + public String getLoaderVersion() { return loaderVersion; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + public int getFilesCount() { return filesCount; } + + @Override + public String toString() { + if (updatedAt != null) { + return String.format("%s [%s + %s v%d] - %d файлов (обновлен: %s)", + name, minecraftVersion, loaderType, version, filesCount, + updatedAt.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); + } else { + return String.format("%s [%s + %s v%d] - %d файлов", + name, minecraftVersion, loaderType, version, filesCount); + } + } +} \ 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 index d4c1945..92103d1 100644 --- 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 @@ -32,37 +32,56 @@ public class FabricInstaller { // Шаг 1: Устанавливаем vanilla и получаем assetIndex VersionInstaller versionInstaller = new VersionInstaller(instancePath); - String assetIndex = versionInstaller.install(minecraftVersion); // ← теперь String - - if (assetIndex == null || assetIndex.isEmpty()) { - System.out.println(ZAnsi.brightRed("Не удалось установить Minecraft " + minecraftVersion)); - return false; + String assetIndex = versionInstaller.install(minecraftVersion); + + // ДОПОЛНИТЕЛЬНАЯ ПРОВЕРКА: если versionInstaller.install() вернул неправильный индекс + // (например, "1.20.1" вместо "5"), то получаем правильный индекс напрямую + if (assetIndex == null || assetIndex.isEmpty() || assetIndex.equals(minecraftVersion)) { + System.out.println(ZAnsi.yellow("Asset index из установки выглядит подозрительно: " + assetIndex)); + System.out.println(ZAnsi.cyan("Получаем правильный asset index для " + minecraftVersion + "...")); + + // Получаем правильный asset index из манифеста версии + String correctAssetIndex = versionInstaller.getAssetIndexForVersion(minecraftVersion); + if (correctAssetIndex != null && !correctAssetIndex.isEmpty()) { + assetIndex = correctAssetIndex; + System.out.println(ZAnsi.green("Правильный asset index: " + assetIndex)); + } else { + System.out.println(ZAnsi.brightRed("Не удалось получить asset index для версии " + minecraftVersion)); + return false; + } } - // Сохраняем assetIndex + // Сохраняем правильный assetIndex instance.setAssetIndex(assetIndex); + System.out.println(ZAnsi.green("Asset index сохранён: " + assetIndex)); - // Шаг 2: Скачивание и запуск Fabric Installer + // Шаг 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 скачан"); + if (!Files.exists(installerJar)) { + ProgressBar.show("Скачивание Fabric Installer", 0, 100, "%"); + downloadFile(installerUrl, installerJar); + ProgressBar.finish("Fabric Installer скачан"); + } else { + System.out.println(ZAnsi.green("Fabric Installer уже скачан, пропускаем...")); + } System.out.println(ZAnsi.cyan("Запуск Fabric Installer...")); + + // Используем ProcessBuilder с правильными аргументами ProcessBuilder pb = new ProcessBuilder( "java", "-jar", installerJar.toAbsolutePath().toString(), "client", "-dir", instancePath.toAbsolutePath().toString(), "-mcversion", minecraftVersion, "-loader", loaderVersion, - "-noprofile", - "-snapshot" + "-noprofile" ); + pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); pb.redirectError(ProcessBuilder.Redirect.INHERIT); @@ -74,25 +93,81 @@ public class FabricInstaller { return false; } - // Проверка результата + // Проверка результата - Fabric создаёт папку versions/fabric-loader-{loaderVersion}-{minecraftVersion} String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion; Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId); + + // Альтернативная проверка (иногда Fabric использует другой формат) + if (!Files.exists(fabricVersionDir)) { + fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion; + fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId); + } if (Files.exists(fabricVersionDir)) { System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!")); - System.out.println("Версия: " + fabricVersionId); + System.out.println(ZAnsi.white("Версия: ") + fabricVersionId); + System.out.println(ZAnsi.white("Asset index: ") + assetIndex); + // Сохраняем метаданные в Instance instance.setMinecraftVersion(minecraftVersion); instance.setLoaderType("fabric"); instance.setLoaderVersion(loaderVersion); + // assetIndex уже сохранён выше, но сохраняем ещё раз для надёжности + instance.setAssetIndex(assetIndex); + + // Копируем или создаём ссылку на правильный asset index в версии Fabric + ensureAssetIndexInFabricVersion(fabricVersionDir, assetIndex, minecraftVersion); return true; } else { System.out.println(ZAnsi.brightRed("Fabric Installer отработал, но версия не найдена.")); + System.out.println(ZAnsi.yellow("Искали: " + fabricVersionDir)); + + // Выводим содержимое папки versions для отладки + Path versionsDir = instancePath.resolve("versions"); + if (Files.exists(versionsDir)) { + System.out.println(ZAnsi.cyan("Доступные версии в папке versions:")); + try (var stream = Files.list(versionsDir)) { + stream.forEach(p -> System.out.println(" - " + p.getFileName())); + } + } return false; } } + /** + * Убеждаемся, что в JSON файле версии Fabric есть правильный asset index + */ + private void ensureAssetIndexInFabricVersion(Path fabricVersionDir, String assetIndex, String minecraftVersion) throws IOException { + Path versionJson = fabricVersionDir.resolve(fabricVersionDir.getFileName() + ".json"); + + if (!Files.exists(versionJson)) { + System.out.println(ZAnsi.yellow("JSON файл версии не найден: " + versionJson)); + return; + } + + String content = Files.readString(versionJson); + + // Проверяем, есть ли в JSON правильный asset index + if (!content.contains("\"assets\":\"" + assetIndex + "\"")) { + System.out.println(ZAnsi.yellow("Исправляем asset index в JSON файле версии...")); + + // Заменяем assets на правильное значение + // Ищем "assets": "что-то" и заменяем + content = content.replaceAll("\"assets\":\\s*\"[^\"]*\"", "\"assets\": \"" + assetIndex + "\""); + + // Также проверяем assetIndex в downloads + if (content.contains("\"assetIndex\"")) { + content = content.replaceAll("\"assetIndex\":\\s*\"[^\"]*\"", "\"assetIndex\": \"" + assetIndex + "\""); + } + + Files.writeString(versionJson, content); + System.out.println(ZAnsi.green("Asset index исправлен на: " + assetIndex)); + } else { + System.out.println(ZAnsi.green("Asset index в JSON версии уже правильный: " + assetIndex)); + } + } + private void cleanOldFabricLoaders() throws IOException { Path librariesDir = instance.getPath().resolve("libraries/net/fabricmc/fabric-loader"); if (!Files.exists(librariesDir)) return; @@ -101,12 +176,11 @@ public class FabricInstaller { try (var stream = Files.walk(librariesDir)) { stream.filter(Files::isDirectory) - .filter(dir -> dir.getFileName().toString().startsWith("0.")) + .filter(dir -> dir.getFileName().toString().matches("\\d+\\.\\d+\\.\\d+.*")) .forEach(dir -> { try { - // Удаляем папку версии Files.walk(dir) - .sorted((a,b) -> b.compareTo(a)) // удаляем файлы перед папками + .sorted((a,b) -> b.compareTo(a)) .forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException ignored) {} @@ -126,14 +200,31 @@ public class FabricInstaller { } private String downloadString(String url) throws Exception { - HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(30)) + .GET() + .build(); + HttpResponse resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - if (resp.statusCode() != 200) throw new IOException("HTTP " + resp.statusCode()); + if (resp.statusCode() != 200) { + throw new IOException("HTTP " + resp.statusCode() + " при скачивании " + url); + } 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)); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .timeout(Duration.ofSeconds(60)) + .GET() + .build(); + + HttpResponse response = httpClient.send(request, + HttpResponse.BodyHandlers.ofFile(target)); + + if (response.statusCode() != 200) { + throw new IOException("HTTP " + response.statusCode() + " при скачивании " + url); + } } } \ 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 index dd4b383..73c5aec 100644 --- 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 @@ -56,7 +56,7 @@ public class VersionInstaller { return versions; } - public String install(String versionId) throws Exception { // ← поменял boolean на String + public String install(String versionId) throws Exception { System.out.println(ZAnsi.cyan("Полная установка Minecraft " + versionId + "...")); Path versionDir = minecraftDir.resolve("versions").resolve(versionId); Files.createDirectories(versionDir); @@ -77,17 +77,20 @@ public class VersionInstaller { System.out.println(ZAnsi.cyan("Скачивание библиотек...")); downloadLibraries(versionData.getJSONArray("libraries")); - // Ассеты - String assetIndex = versionData.getString("assets"); // ← ВОТ ЭТО ГЛАВНОЕ! + // Ассеты - ЭТО ВАЖНО + String assetIndex = versionData.getString("assets"); // ← Например "5" для 1.20.1 + System.out.println(ZAnsi.cyan("Asset index из версии: " + assetIndex)); if (versionData.has("assetIndex")) { System.out.println(ZAnsi.cyan("Скачивание ассетов...")); downloadAssets(versionData); System.out.println(ZAnsi.brightGreen("Asset index определён как: " + assetIndex)); + } else { + System.out.println(ZAnsi.yellow("Нет assetIndex в версии, использую fallback: " + assetIndex)); } System.out.println(ZAnsi.brightGreen("\nMinecraft " + versionId + " полностью установлен!")); - return assetIndex; // ← возвращаем настоящий assetIndex (например "30") + return assetIndex; // ← Возвращаем правильный индекс (например "5") } private void downloadLibraries(JSONArray libraries) throws Exception { @@ -188,6 +191,16 @@ public class VersionInstaller { } } + public String getAssetIndexForVersion(String versionId) throws Exception { + String versionUrl = getVersionUrl(versionId); + if (versionUrl == null) throw new Exception("Версия " + versionId + " не найдена"); + + String versionJson = downloadString(versionUrl); + JSONObject versionData = new JSONObject(versionJson); + + return versionData.getString("assets"); + } + private String getVersionUrl(String versionId) throws Exception { for (MinecraftVersion v : getAvailableVersions()) { if (v.getId().equals(versionId)) return v.getUrl(); diff --git a/server/log_manager.py b/server/log_manager.py index d94cbf4..40370e2 100644 --- a/server/log_manager.py +++ b/server/log_manager.py @@ -102,8 +102,9 @@ def setup_logging(): structlog.processors.CallsiteParameter.MODULE, structlog.processors.CallsiteParameter.FUNC_NAME, } - ), + ), _add_location, + structlog.stdlib.ProcessorFormatter.wrap_for_formatter, ], logger_factory=structlog.stdlib.LoggerFactory(), wrapper_class=structlog.stdlib.BoundLogger, diff --git a/server/main.py b/server/main.py index 6a896f8..5a6107a 100644 --- a/server/main.py +++ b/server/main.py @@ -1,5 +1,3 @@ -import os - from fastapi import FastAPI, HTTPException, Request from fastapi.responses import FileResponse, JSONResponse from contextlib import asynccontextmanager @@ -7,34 +5,32 @@ from pathlib import Path import json import structlog from cachetools import TTLCache -import asyncio import logging from datetime import datetime from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol # Import local modules -from pack_manager import DATA_DIR, get_fabric_versions, get_forge_versions, scan_pack, get_cached_manifest, PACKS_DIR +from pack_manager import DATA_DIR, scan_pack, get_cached_manifest, PACKS_DIR from models import PackMeta -from logging_config import setup_logging from middleware import LoggingMiddleware from cli import parse_args, run_test_mode, run_production_mode, run_development_mode -from log_manager import init_logging, get_logger - -from models import MinecraftVersion -from pack_manager import get_minecraft_versions, download_minecraft_version +from log_manager import init_logging logger = structlog.get_logger(__name__) # Cache for manifests - expires after 5 minutes manifest_cache = TTLCache(maxsize=100, ttl=300) +BUILDS_DIR = Path("builds") + + @asynccontextmanager async def lifespan(app: FastAPI): args = parse_args() # Initialize logging init_logging() - logger = logging.getLogger(__name__) + #logger = logging.getLogger(__name__) # Determine environment if args.test: @@ -46,6 +42,11 @@ async def lifespan(app: FastAPI): logger.info(f"Starting ZernMC Launcher Server (environment: {env})") + # Create directories if they don't exist + BUILDS_DIR.mkdir(exist_ok=True) + PACKS_DIR.mkdir(exist_ok=True) + DATA_DIR.mkdir(exist_ok=True) + if args.test: await run_test_mode() yield @@ -69,6 +70,7 @@ async def lifespan(app: FastAPI): yield logger.info("Server shutting down...") + # Create app with lifespan app = FastAPI(title="ZernMC Launcher Server", lifespan=lifespan) @@ -85,17 +87,35 @@ def patched_data_received(self, data): except Exception as e: client = self.transport.get_extra_info('peername') logger = logging.getLogger(__name__) - logger.warning(f"Invalid HTTP request from {client}: {str(e)[:200]}") - # Log raw data if possible + + # Показываем первые 200 байт запроса в HEX для диагностики + hex_preview = data[:100].hex() if len(data) > 0 else "empty" + + logger.error(f"Invalid HTTP request from {client}") + logger.error(f"Error: {str(e)}") + logger.error(f"First 100 bytes (hex): {hex_preview}") + try: raw_data = data[:500].decode('utf-8', errors='replace') - logger.debug(f"Raw request data: {raw_data}") + logger.error(f"Raw request data: {repr(raw_data)}") except: pass - raise + + # Не перевыбрасываем исключение, а возвращаем 400 ответ + # Это важно! Иначе клиент не получит ответ + try: + response = b"HTTP/1.1 400 Bad Request\r\nContent-Type: text/plain\r\nContent-Length: 21\r\n\r\nInvalid HTTP request" + self.transport.write(response) + self.transport.close() + except: + pass + return HttpToolsProtocol.data_received = patched_data_received + +# ====================== ОСНОВНЫЕ ЭНДПОИНТЫ ====================== + @app.get("/") async def root(): """Root endpoint""" @@ -108,12 +128,15 @@ async def root(): "redoc": "/redoc" } + @app.get("/health") async def health(): """Health check endpoint""" return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()} +# ====================== ЭНДПОИНТЫ ДЛЯ ПАКОВ ====================== + @app.get("/packs") async def list_packs(): """List all available packs""" @@ -125,13 +148,21 @@ async def list_packs(): meta_path = DATA_DIR / f"{pack_dir.name}.meta" if meta_path.exists(): try: - async with open(meta_path, 'r', encoding='utf-8') as f: + with open(meta_path, 'r', encoding='utf-8') as f: meta = json.load(f) + # Исправлено: конвертируем updated_at в строку если это datetime + updated_at = meta.get("updated_at") + if updated_at and isinstance(updated_at, datetime): + updated_at = updated_at.isoformat() + packs.append({ "name": pack_dir.name, "version": meta.get("version", 1), "files_count": len(meta.get("files", {})), - "updated_at": meta.get("updated_at") + "updated_at": updated_at, + "minecraft_version": meta.get("minecraft_version", "unknown"), + "loader_type": meta.get("loader_type", "vanilla"), + "loader_version": meta.get("loader_version") }) except Exception as e: logger.error(f"Failed to load pack meta for {pack_dir.name}: {e}") @@ -147,22 +178,28 @@ async def list_packs(): return {"packs": packs} -# ------------------- DIFF ENDPOINT ------------------- @app.post("/pack/{pack_name}/diff") -async def get_pack_diff(pack_name: str, client_files: dict[str, str], request: Request): +async def get_pack_diff(pack_name: str, request: Request): """ Client sends: { "mods/jei.jar": "sha256_hash", ... } Server returns diff information """ client_ip = request.client.host if request.client else "unknown" + + # Читаем тело запроса + try: + body = await request.json() + except Exception as e: + logger.error(f"Failed to parse JSON body: {e}") + raise HTTPException(400, "Invalid JSON body") + logger.info("Received diff request", pack=pack_name, - client_files_count=len(client_files), + client_files_count=len(body), client_ip=client_ip) try: - # Use cached manifest if available meta = get_cached_manifest(pack_name) if not meta: meta = await scan_pack(pack_name) @@ -171,7 +208,7 @@ async def get_pack_diff(pack_name: str, client_files: dict[str, str], request: R raise HTTPException(404, "Pack not found") except Exception as e: logger.error("Error loading pack meta", pack=pack_name, error=str(e), exc_info=True) - raise HTTPException(500, "Internal server error") + raise HTTPException(500, f"Internal server error: {str(e)}") to_download = [] to_delete = [] @@ -179,9 +216,8 @@ async def get_pack_diff(pack_name: str, client_files: dict[str, str], request: R server_files = meta.files - # Calculate what needs to be downloaded or updated for path, entry in server_files.items(): - client_hash = client_files.get(path) + client_hash = body.get(path) if client_hash is None or client_hash != entry.hash: url = f"/pack/{pack_name}/file/{path}" to_download.append({ @@ -193,8 +229,7 @@ async def get_pack_diff(pack_name: str, client_files: dict[str, str], request: R if client_hash is not None: to_update.append(path) - # Calculate what needs to be deleted - for path in client_files: + for path in body: if path not in server_files: to_delete.append(path) @@ -213,76 +248,34 @@ async def get_pack_diff(pack_name: str, client_files: dict[str, str], request: R "to_update": to_update } -@app.get("/minecraft/versions") -async def list_minecraft_versions(): - """List available Minecraft versions""" - try: - versions = await get_minecraft_versions() - return {"versions": [v.model_dump() for v in versions]} - except Exception as e: - logger.error(f"Failed to fetch Minecraft versions: {e}") - raise HTTPException(500, "Failed to fetch versions") - -@app.get("/minecraft/version/{version}") -async def get_version_details(version: str): - """Get details for specific Minecraft version""" - # This would fetch version JSON from Mojang - return {"version": version, "status": "available"} - -@app.post("/minecraft/download/{version}") -async def download_version(version: str, request: Request): - """Download Minecraft version""" - client_ip = request.client.host if request.client else "unknown" - logger.info(f"Download request for Minecraft {version}", client_ip=client_ip) - - version_path = Path("versions") / version - version_path.mkdir(parents=True, exist_ok=True) - - success = await download_minecraft_version(version, version_path) - if success: - return {"status": "success", "version": version} - raise HTTPException(404, "Version not found") - -@app.get("/modloaders/{loader_type}") -async def get_modloaders(loader_type: str, minecraft_version: str = None): - """Get available mod loaders""" - if loader_type == "fabric": - versions = await get_fabric_versions(minecraft_version) if minecraft_version else [] - return {"loader": "fabric", "versions": versions} - elif loader_type == "forge": - versions = await get_forge_versions(minecraft_version) if minecraft_version else [] - return {"loader": "forge", "versions": versions} - elif loader_type == "vanilla": - return {"loader": "vanilla", "versions": ["vanilla"]} - raise HTTPException(400, "Invalid loader type") @app.get("/pack/{pack_name}") async def get_pack_manifest(pack_name: str, request: Request): """Get pack manifest with caching""" client_ip = request.client.host if request.client else "unknown" - # Check cache first cached_meta = get_cached_manifest(pack_name) if cached_meta: logger.debug("Manifest served from cache", pack=pack_name, version=cached_meta.version, client_ip=client_ip) + + # Исправлено: конвертируем datetime в строку при сериализации return JSONResponse( - content=cached_meta.model_dump(), + content=cached_meta.model_dump(mode='json'), headers={"X-Pack-Version": str(cached_meta.version), "X-Cached": "true"} ) - # Load from disk if not in cache meta_path = Path("data") / f"{pack_name}.meta" if not meta_path.exists(): logger.warning("Manifest requested but pack not found", pack=pack_name, client_ip=client_ip) raise HTTPException(404, "Pack not found") - async with open(meta_path, 'r', encoding='utf-8') as f: + with open(meta_path, 'r', encoding='utf-8') as f: meta_dict = json.load(f) + # Исправлено: используем model_validate для создания объекта meta = PackMeta.model_validate(meta_dict) - # Update cache manifest_cache[pack_name] = meta logger.debug("Manifest served from disk", @@ -290,16 +283,23 @@ async def get_pack_manifest(pack_name: str, request: Request): version=meta.version, client_ip=client_ip) + # Исправлено: конвертируем datetime в строку при сериализации return JSONResponse( - content=meta_dict, + content=meta.model_dump(mode='json'), headers={"X-Pack-Version": str(meta.version), "X-Cached": "false"} ) + @app.get("/pack/{pack_name}/file/{file_path:path}") async def get_pack_file(pack_name: str, file_path: str, request: Request): + """Get a file from a pack""" full_path = PACKS_DIR / pack_name / file_path client_ip = request.client.host if request.client else None + # Security: prevent path traversal + if ".." in file_path: + raise HTTPException(403, "Invalid file path") + if not full_path.exists() or not full_path.is_file(): logger.warning("File not found", pack=pack_name, @@ -316,51 +316,177 @@ async def get_pack_file(pack_name: str, file_path: str, request: Request): return FileResponse(full_path) +# ====================== ЭНДПОИНТЫ ДЛЯ ЛАУНЧЕРА ====================== + +def get_current_launcher_version() -> str: + """Get current launcher version from build.version file""" + version_file = BUILDS_DIR / "build.version" + if version_file.exists(): + return version_file.read_text().strip() + return "1.0.0" + + +def get_available_zips() -> list: + """Get list of available zip archives""" + if not BUILDS_DIR.exists(): + return [] + + zips = [] + for zip_file in BUILDS_DIR.glob("ZernMCLauncher-*.zip"): + version = zip_file.stem.replace("ZernMCLauncher-", "") + stat = zip_file.stat() + zips.append({ + "version": version, + "filename": zip_file.name, + "size": stat.st_size, + "modified": datetime.fromtimestamp(stat.st_mtime).isoformat() + }) + + zips.sort(key=lambda x: x["version"], reverse=True) + return zips + + @app.get("/launcher/version") async def get_launcher_version(): - """Возвращает информацию о текущей версии лаунчера""" - version_file = Path("builds/build.version") + """Return launcher version information""" + version = get_current_launcher_version() + zips = get_available_zips() - version = "1.0.0" - if version_file.exists(): - version = version_file.read_text().strip() - - return { + response = { "version": version, - "download_jar": "/launcher/download?type=jar", - "download_exe": "/launcher/download?type=exe", "updated_at": datetime.utcnow().isoformat() } + + jar_path = BUILDS_DIR / "ZernMCLauncher.jar" + if jar_path.exists(): + response["download_jar"] = "/launcher/download/jar" + response["jar_size"] = jar_path.stat().st_size + + exe_path = BUILDS_DIR / "ZernMCLauncher.exe" + if exe_path.exists(): + response["download_exe"] = "/launcher/download/exe" + response["exe_size"] = exe_path.stat().st_size + + if zips: + response["download_zip"] = f"/launcher/download/zip/{zips[0]['filename']}" + response["zip_version"] = zips[0]["version"] + response["zip_size"] = zips[0]["size"] + response["all_zips"] = zips + + return response -@app.get("/launcher/download") -async def download_launcher(type: str = "exe"): - """Отдаёт файл лаунчера""" - if type == "exe": - file_path = Path("builds/ZernMCLauncher.exe") - filename = "ZernMCLauncher.exe" - else: - file_path = Path("builds/ZernMCLauncher.jar") - filename = "ZernMCLauncher.jar" - +@app.get("/launcher/download/jar") +async def download_launcher_jar(): + """Download launcher JAR file""" + file_path = BUILDS_DIR / "ZernMCLauncher.jar" + if not file_path.exists(): - raise HTTPException(404, "Launcher file not found") + raise HTTPException(404, "JAR file not found") + + return FileResponse( + path=file_path, + filename="ZernMCLauncher.jar", + media_type="application/java-archive" + ) + +@app.get("/launcher/download/exe") +async def download_launcher_exe(): + """Download launcher EXE file (Windows)""" + file_path = BUILDS_DIR / "ZernMCLauncher.exe" + + if not file_path.exists(): + raise HTTPException(404, "EXE file not found") + + return FileResponse( + path=file_path, + filename="ZernMCLauncher.exe", + media_type="application/vnd.microsoft.portable-executable" + ) + + +@app.get("/launcher/download/zip/{filename}") +async def download_launcher_zip(filename: str): + """Download specific launcher ZIP archive""" + if ".." in filename or not filename.startswith("ZernMCLauncher-") or not filename.endswith(".zip"): + raise HTTPException(400, "Invalid filename") + + file_path = BUILDS_DIR / filename + + if not file_path.exists(): + raise HTTPException(404, "ZIP file not found") + return FileResponse( path=file_path, filename=filename, - media_type="application/octet-stream" + media_type="application/zip" ) + +@app.get("/launcher/download/latest") +async def download_latest_launcher(): + """Download the latest launcher (prefer ZIP if available, fallback to JAR)""" + zips = get_available_zips() + + if zips: + latest_zip = zips[0]["filename"] + return await download_launcher_zip(latest_zip) + + jar_path = BUILDS_DIR / "ZernMCLauncher.jar" + if jar_path.exists(): + return await download_launcher_jar() + + raise HTTPException(404, "No launcher files available") + + +@app.get("/launcher/info") +async def get_launcher_full_info(): + """Full launcher information with all available files""" + version = get_current_launcher_version() + zips = get_available_zips() + + info = { + "current_version": version, + "updated_at": datetime.utcnow().isoformat(), + "files": { + "jar": None, + "exe": None, + "zips": zips + }, + "recommended": "zip" if zips else ("exe" if (BUILDS_DIR / "ZernMCLauncher.exe").exists() else "jar") + } + + jar_path = BUILDS_DIR / "ZernMCLauncher.jar" + if jar_path.exists(): + info["files"]["jar"] = { + "size": jar_path.stat().st_size, + "download_url": "/launcher/download/jar" + } + + exe_path = BUILDS_DIR / "ZernMCLauncher.exe" + if exe_path.exists(): + info["files"]["exe"] = { + "size": exe_path.stat().st_size, + "download_url": "/launcher/download/exe" + } + + if zips: + info["files"]["latest_zip"] = zips[0] + info["files"]["download_latest"] = "/launcher/download/latest" + + return info + + +# ====================== ЗАПУСК ====================== + if __name__ == "__main__": args = parse_args() if args.test: - # Test mode runs within lifespan import asyncio asyncio.run(run_test_mode()) elif args.dev: run_development_mode(args.host, args.port, args.reload) else: - # Default to production run_production_mode(args.host, args.port, args.workers) \ No newline at end of file diff --git a/server/pack_manager.py b/server/pack_manager.py index 8bf00d4..4eae54c 100644 --- a/server/pack_manager.py +++ b/server/pack_manager.py @@ -3,25 +3,17 @@ import os from datetime import datetime from pathlib import Path import json -import aiofiles from typing import Optional, Dict import structlog from models import PackMeta, FileEntry -import aiohttp -from typing import List, Optional - logger = structlog.get_logger(__name__) PACKS_DIR = Path("packs") DATA_DIR = Path("data") DATA_DIR.mkdir(exist_ok=True) -MINECRAFT_VERSION_MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest.json" -FABRIC_META_URL = "https://meta.fabricmc.net/v2/versions" -FORGE_META_URL = "https://files.minecraftforge.net/net/minecraftforge/forge/" - # Cache for loaded manifests _manifest_cache: Dict[str, PackMeta] = {} @@ -32,102 +24,63 @@ def get_cached_manifest(pack_name: str) -> Optional[PackMeta]: def get_meta_path(pack_name: str) -> Path: return DATA_DIR / f"{pack_name}.meta" -async def calculate_sha256(file_path: Path) -> str: +def calculate_sha256_sync(file_path: Path) -> str: + """Calculate SHA256 hash of a file (synchronous version)""" hash_sha = hashlib.sha256() - async with aiofiles.open(file_path, 'rb') as f: - while chunk := await f.read(8192): + with open(file_path, 'rb') as f: + while chunk := f.read(8192): hash_sha.update(chunk) return hash_sha.hexdigest() -async def get_minecraft_versions(): - """Fetch available Minecraft versions from Mojang""" - from models import MinecraftVersion - async with aiohttp.ClientSession() as session: - async with session.get(MINECRAFT_VERSION_MANIFEST_URL) as response: - data = await response.json() - versions = [] - for v in data.get("versions", []): - versions.append(MinecraftVersion( - version=v["id"], - type=v["type"], - release_time=datetime.fromisoformat(v["releaseTime"].replace('Z', '+00:00')), - url=v["url"] - )) - return versions - -async def get_fabric_versions(minecraft_version: str) -> List[str]: - """Get available Fabric versions for specific Minecraft version""" - async with aiohttp.ClientSession() as session: - async with session.get(f"{FABRIC_META_URL}/loader") as response: - data = await response.json() - fabric_versions = [] - for loader in data: - if loader.get("stable", True): - fabric_versions.append(loader["version"]) - return fabric_versions - -async def get_forge_versions(minecraft_version: str) -> List[str]: - """Get available Forge versions for specific Minecraft version""" - async with aiohttp.ClientSession() as session: - async with session.get(FORGE_META_URL) as response: - # Forge API is more complex, simplified for now - return [] - -async def download_minecraft_version(version: str, target_path: Path) -> bool: - """Download Minecraft version JSON and client jar""" - async with aiohttp.ClientSession() as session: - async with session.get(f"https://piston-meta.mojang.com/mc/game/{version}/{version}.json") as response: - if response.status == 200: - version_data = await response.json() - async with aiofiles.open(target_path / f"{version}.json", 'w') as f: - await f.write(json.dumps(version_data, indent=2)) - - downloads = version_data.get("downloads", {}) - client_info = downloads.get("client", {}) - if client_info: - client_url = client_info.get("url") - async with session.get(client_url) as client_response: - if client_response.status == 200: - async with aiofiles.open(target_path / f"{version}.jar", 'wb') as f: - async for chunk in client_response.content.iter_chunked(8192): - await f.write(chunk) - return True - return False - +async def calculate_sha256(file_path: Path) -> str: + """Calculate SHA256 hash of a file (async wrapper)""" + # Используем синхронную версию для простоты + return calculate_sha256_sync(file_path) async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta: """Scan pack directory and update manifest if needed""" pack_path = PACKS_DIR / pack_name + if not pack_path.exists() or not pack_path.is_dir(): raise FileNotFoundError(f"Pack {pack_name} not found") meta_path = get_meta_path(pack_name) current_meta: Optional[PackMeta] = None + # Check cache first if not force_rescan and pack_name in _manifest_cache: return _manifest_cache[pack_name] + # Load existing meta if available (синхронно) if meta_path.exists(): - async with aiofiles.open(meta_path, 'r', encoding='utf-8') as f: - data = json.loads(await f.read()) - try: + try: + with open(meta_path, 'r', encoding='utf-8') as f: + data = json.load(f) current_meta = PackMeta.model_validate(data) - except Exception as e: - logger.warning(f"Failed to validate existing meta for pack {pack_name}", error=str(e)) - current_meta = None + except Exception as e: + logger.warning(f"Failed to load existing meta for pack {pack_name}: {e}") + current_meta = None new_files: Dict[str, FileEntry] = {} changed = False + # Get ignored directories + ignored_dirs = current_meta.ignored_dirs if current_meta else [ + "resourcepacks", "shaderpacks", "saves", "logs", + "crash-reports", "screenshots", "journeymap", "config" + ] + + # Walk through pack directory for root, dirs, files in os.walk(pack_path): - ignored = current_meta.ignored_dirs if current_meta else [] - dirs[:] = [d for d in dirs if d not in ignored] + # Filter ignored directories + dirs[:] = [d for d in dirs if d not in ignored_dirs] for file in files: file_path = Path(root) / file rel_path = file_path.relative_to(pack_path).as_posix() - if any(ignored_dir in rel_path.split('/') for ignored_dir in ignored): + # Skip if in ignored directory + if any(ignored_dir in rel_path.split('/') for ignored_dir in ignored_dirs): continue stat = file_path.stat() @@ -143,48 +96,58 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta: new_files[rel_path] = entry + # Check if file changed if current_meta and (rel_path not in current_meta.files or current_meta.files[rel_path].hash != file_hash): changed = True + # Check if we need to update if not current_meta or changed or len(new_files) != len(current_meta.files if current_meta else 0): version = (current_meta.version + 1) if current_meta else 1 - pack_config_path = pack_path / "instance.json" + # Load instance.json for pack metadata minecraft_version = "1.20.4" loader_type = "vanilla" loader_version = None + pack_config_path = pack_path / "instance.json" if pack_config_path.exists(): try: - async with aiofiles.open(pack_config_path, 'r', encoding='utf-8') as f: - config = json.loads(await f.read()) + # Синхронное чтение конфига + with open(pack_config_path, 'r', encoding='utf-8') as f: + config = json.load(f) minecraft_version = config.get("minecraftVersion", minecraft_version) loader_type = config.get("loaderType", loader_type) loader_version = config.get("loaderVersion") except Exception as e: - logger.warning(f"Failed to load instance.json for {pack_name}", error=str(e)) + logger.warning(f"Failed to load instance.json for {pack_name}: {e}") + # Create new manifest new_meta = PackMeta( pack_name=pack_name, version=version, files=new_files, updated_at=datetime.utcnow(), - ignored_dirs=current_meta.ignored_dirs if current_meta else [], + ignored_dirs=ignored_dirs, minecraft_version=minecraft_version, loader_type=loader_type, loader_version=loader_version ) - async with aiofiles.open(meta_path, 'w', encoding='utf-8') as f: - await f.write(new_meta.model_dump_json(indent=2)) + # Save to disk (синхронно) + with open(meta_path, 'w', encoding='utf-8') as f: + f.write(new_meta.model_dump_json(indent=2)) + # Update cache _manifest_cache[pack_name] = new_meta logger.info(f"Pack updated: {pack_name} v{version}, {len(new_files)} files") return new_meta + # No changes, use existing if current_meta: _manifest_cache[pack_name] = current_meta + return current_meta - return current_meta \ No newline at end of file + # Should not happen + raise Exception(f"Failed to scan pack {pack_name}") \ No newline at end of file