diff --git a/.gitignore b/.gitignore index a9c6d17..bf1b5e8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ server/packs server/data jre .vscode -launcher/dependency-reduced-pom.xml +dependency-reduced-pom.xml 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 2deec3b..3604206 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 @@ -8,6 +8,7 @@ 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.Input; import me.sashegdev.zernmc.launcher.utils.ZAnsi; import me.sashegdev.zernmc.launcher.utils.ZHttpClient; @@ -219,16 +220,31 @@ public class LaunchMenu { } 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("Сборка удалена.")); + ConsoleUtils.clearScreen(); + + List confirmOptions = List.of( + "Да, удалить сборку", + "Нет, отменить" + ); + + ArrowMenu confirmMenu = new ArrowMenu( + "Вы действительно хотите удалить сборку '" + instance.getName() + "'?", + confirmOptions + ); + + int choice = confirmMenu.show(); + + if (choice == 0) { // "Да, удалить" + boolean deleted = InstanceManager.deleteInstance(instance.getName()); + if (deleted) { + System.out.println(ZAnsi.brightGreen("Сборка '" + instance.getName() + "' успешно удалена.")); + } else { + System.out.println(ZAnsi.brightRed("Не удалось удалить сборку.")); + } } else { - System.out.println(ZAnsi.yellow("Отменено.")); + System.out.println(ZAnsi.yellow("Удаление отменено.")); } + ConsoleUtils.pause(); } @@ -282,7 +298,7 @@ public class LaunchMenu { private String askPackName() { System.out.print(ZAnsi.white("\nВведите название новой сборки: ")); - String name = new java.util.Scanner(System.in).nextLine().trim(); + String name = Input.readLine(); // используем наш Input if (name.isEmpty()) { System.out.println(ZAnsi.yellow("Отменено.")); return null; 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 a2de872..eebe015 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 @@ -2,19 +2,18 @@ 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 String assetIndex; // ← ЭТО САМОЕ ВАЖНОЕ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); @@ -28,66 +27,66 @@ public class Instance { public Path getPath() { return path; } public String getMinecraftVersion() { return minecraftVersion; } - public void setMinecraftVersion(String minecraftVersion) { - this.minecraftVersion = 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; + public void setLoaderType(String loaderType) { + this.loaderType = loaderType; saveMetadata(); } public String getLoaderVersion() { return loaderVersion; } - public void setLoaderVersion(String loaderVersion) { - this.loaderVersion = loaderVersion; + public void setLoaderVersion(String loaderVersion) { + this.loaderVersion = loaderVersion; + saveMetadata(); + } + + /** Возвращает ТОТ САМЫЙ assetIndex, который сохранился при установке (например 30) */ + public String getAssetIndex() { + return assetIndex != null ? assetIndex : minecraftVersion; // fallback для старых сборок + } + + public void setAssetIndex(String assetIndex) { + this.assetIndex = assetIndex; 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); - } + 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) { - // игнорируем, если файл повреждён - } + this.assetIndex = meta.assetIndex; + } catch (Exception ignored) {} } private void saveMetadata() { Path metaFile = path.resolve("instance.json"); - InstanceMeta meta = new InstanceMeta(minecraftVersion, loaderType, loaderVersion); - + InstanceMeta meta = new InstanceMeta(minecraftVersion, loaderType, loaderVersion, assetIndex); try { Files.writeString(metaFile, GSON.toJson(meta)); } catch (IOException e) { @@ -95,16 +94,18 @@ public class Instance { } } - // Внутренний класс для сериализации private static class InstanceMeta { String minecraftVersion; String loaderType; String loaderVersion; + String assetIndex; - public InstanceMeta(String minecraftVersion, String loaderType, String loaderVersion) { + public InstanceMeta(String minecraftVersion, String loaderType, + String loaderVersion, String assetIndex) { this.minecraftVersion = minecraftVersion; this.loaderType = loaderType; this.loaderVersion = loaderVersion; + this.assetIndex = assetIndex; } } } \ 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 index 8baa8a4..9d16fbe 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/InstanceManager.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/InstanceManager.java @@ -31,6 +31,35 @@ public class InstanceManager { } return null; } + + public static boolean deleteInstance(String instanceName) { + if (instanceName == null || instanceName.isBlank()) { + return false; + } + + Path instancePath = INSTANCES_DIR.resolve(instanceName); + + if (!Files.exists(instancePath)) { + return false; + } + + try { + // Рекурсивно удаляем всю папку сборки + Files.walk(instancePath) + .sorted((a, b) -> b.compareTo(a)) // удаляем снизу вверх + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + System.err.println("Не удалось удалить: " + path); + } + }); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } public static boolean createInstanceFolder(String name) throws IOException { Path path = INSTANCES_DIR.resolve(name); diff --git a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java index 861a702..1ee8ea1 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java @@ -8,6 +8,9 @@ import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions; import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; import me.sashegdev.zernmc.launcher.utils.ZAnsi; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.List; public class MinecraftLib { @@ -24,13 +27,16 @@ public class MinecraftLib { //Установка public boolean installMinecraft(String versionId) throws Exception { VersionInstaller installer = new VersionInstaller(instance.getPath()); - boolean success = installer.install(versionId); - - if (success) { + + String assetIndex = installer.install(versionId); // ← теперь возвращается String + + if (assetIndex != null && !assetIndex.isEmpty()) { instance.setMinecraftVersion(versionId); + instance.setAssetIndex(assetIndex); // ← сохраняем правильный индекс! instance.setLoaderType("vanilla"); + return true; } - return success; + return false; } public boolean installForge(String minecraftVersion, String forgeVersion) throws Exception { @@ -86,34 +92,89 @@ public class MinecraftLib { //Запуск public void launch(LaunchOptions options) throws Exception { System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName())); - + cleanupOldLoaders(); + 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); - } - + command.forEach(arg -> 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 перед запуском игры - + ConsoleUtils.clearScreen(); + Process process = pb.start(); - - // Ждём завершения игры int exitCode = process.waitFor(); - + System.out.println(ZAnsi.yellow("\nMinecraft завершился с кодом: " + exitCode)); } + private void safeDeleteDirectory(Path dir) { + try { + Files.walk(dir) + .sorted((a, b) -> b.compareTo(a)) + .forEach(p -> { + try { Files.deleteIfExists(p); } + catch (IOException ignored) {} + }); + } catch (IOException ignored) {} + } + + private void deleteOldVersionDirs(Path versionsDir, String keepVersion) throws IOException { + if (!Files.exists(versionsDir)) return; + + try (var stream = Files.walk(versionsDir)) { + stream.filter(Files::isDirectory) + .filter(dir -> dir.getFileName().toString().contains("fabric-loader") || + dir.getFileName().toString().contains("forge")) + .filter(dir -> !dir.getFileName().toString().contains(keepVersion)) + .forEach(this::safeDeleteDirectory); + } + } + + private void deleteAllExcept(Path baseDir, String keepVersion) throws IOException { + if (!Files.exists(baseDir)) return; + + try (var stream = Files.walk(baseDir)) { + stream.filter(Files::isDirectory) + .filter(dir -> { + String name = dir.getFileName().toString(); + return name.contains(".") && !name.contains(keepVersion); + }) + .forEach(this::safeDeleteDirectory); + } + } + + private void cleanupOldLoaders() throws IOException { + String loaderType = instance.getLoaderType().toLowerCase(); + String currentLoaderVer = instance.getLoaderVersion(); + + if (currentLoaderVer == null) return; + + System.out.println(ZAnsi.yellow("Выполняем очистку старых версий лоадера...")); + + // Удаляем все старые fabric-loader / forge + Path libraries = instance.getPath().resolve("libraries"); + + if ("fabric".equals(loaderType)) { + deleteAllExcept(libraries.resolve("net/fabricmc/fabric-loader"), currentLoaderVer); + } else if ("forge".equals(loaderType)) { + deleteAllExcept(libraries.resolve("net/minecraftforge/forge"), currentLoaderVer); + } + + // Также чистим versions/ от старых fabric/forge версий + Path versionsDir = instance.getPath().resolve("versions"); + deleteOldVersionDirs(versionsDir, currentLoaderVer); + } + + + public Instance getInstance() { return instance; } 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 4da0be8..d4c1945 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 @@ -28,20 +28,23 @@ public class FabricInstaller { System.out.println(ZAnsi.cyan("Установка Fabric " + loaderVersion + " для Minecraft " + minecraftVersion)); Path instancePath = instance.getPath(); - cleanOldFabricLoaders(); - // Шаг 1: Установка vanilla версии (если ещё не установлена) + // Шаг 1: Устанавливаем vanilla и получаем assetIndex VersionInstaller versionInstaller = new VersionInstaller(instancePath); - boolean mcOk = versionInstaller.install(minecraftVersion); - if (!mcOk) { + String assetIndex = versionInstaller.install(minecraftVersion); // ← теперь String + + if (assetIndex == null || assetIndex.isEmpty()) { System.out.println(ZAnsi.brightRed("Не удалось установить Minecraft " + minecraftVersion)); return false; } + // Сохраняем assetIndex + instance.setAssetIndex(assetIndex); + // Шаг 2: Скачивание и запуск Fabric Installer String installerVersion = getLatestInstallerVersion(); - String installerUrl = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/" + String installerUrl = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/" + installerVersion + "/fabric-installer-" + installerVersion + ".jar"; Path installerJar = instancePath.resolve("fabric-installer.jar"); @@ -50,9 +53,7 @@ public class FabricInstaller { 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", @@ -62,7 +63,6 @@ public class FabricInstaller { "-noprofile", "-snapshot" ); - pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); pb.redirectError(ProcessBuilder.Redirect.INHERIT); @@ -74,13 +74,18 @@ public class FabricInstaller { 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); + + instance.setMinecraftVersion(minecraftVersion); + instance.setLoaderType("fabric"); + instance.setLoaderVersion(loaderVersion); + return true; } else { System.out.println(ZAnsi.brightRed("Fabric Installer отработал, но версия не найдена.")); 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 index 16c3dfd..412b652 100644 --- 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 @@ -3,7 +3,6 @@ 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; @@ -27,39 +26,41 @@ public class ForgeInstaller { public boolean install(String mcVersion, String forgeVersion) throws Exception { System.out.println(ZAnsi.cyan("Установка Forge " + forgeVersion + " для Minecraft " + mcVersion)); - // Шаг 1: Полная установка vanilla + // Шаг 1: Устанавливаем vanilla и получаем настоящий assetIndex System.out.println(ZAnsi.cyan("Установка базовой версии Minecraft " + mcVersion + "...")); VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath()); - boolean vanillaSuccess = vanillaInstaller.install(mcVersion); + + String assetIndex = vanillaInstaller.install(mcVersion); // ← теперь возвращает String - if (!vanillaSuccess) { + if (assetIndex == null || assetIndex.isEmpty()) { System.out.println(ZAnsi.brightRed("Не удалось установить базовую версию Minecraft")); return false; } - // Шаг 2: Создаём launcher_profiles.json (критично для Forge) + // Сохраняем assetIndex (очень важно!) + instance.setAssetIndex(assetIndex); + + // Шаг 2: Создаём launcher_profiles.json createLauncherProfile(); // Шаг 3: Скачиваем и запускаем Forge Installer - String installerUrl = "https://maven.minecraftforge.net/net/minecraftforge/forge/" - + mcVersion + "-" + forgeVersion + 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)) + ")"); + ProgressBar.finish("Forge Installer скачан"); System.out.println(ZAnsi.cyan("Запуск Forge Installer...")); - ProcessBuilder pb = new ProcessBuilder( "java", "-jar", installerJar.toAbsolutePath().toString(), "--installClient" ); - pb.directory(instance.getPath().toFile()); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); pb.redirectError(ProcessBuilder.Redirect.INHERIT); @@ -81,21 +82,16 @@ public class ForgeInstaller { 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" - } - """; - + { + "profiles": {}, + "selectedProfile": "Default" + } + """; Files.writeString(profilePath, minimalProfile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); System.out.println(ZAnsi.yellow("Создан launcher_profiles.json")); } @@ -105,9 +101,7 @@ public class ForgeInstaller { .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() + ")"); } 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 73c3d65..73ce349 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 @@ -17,21 +17,23 @@ import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class VersionInstaller { private final Path minecraftDir; private final HttpClient httpClient; + private final ExecutorService executor = Executors.newFixedThreadPool(32); // параллельная загрузка public VersionInstaller(Path minecraftDir) { this.minecraftDir = minecraftDir; this.httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(30)) + .connectTimeout(Duration.ofSeconds(15)) .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); @@ -54,9 +56,8 @@ public class VersionInstaller { return versions; } - public boolean install(String versionId) throws Exception { + public String install(String versionId) throws Exception { // ← поменял boolean на String System.out.println(ZAnsi.cyan("Полная установка Minecraft " + versionId + "...")); - Path versionDir = minecraftDir.resolve("versions").resolve(versionId); Files.createDirectories(versionDir); @@ -77,13 +78,16 @@ public class VersionInstaller { downloadLibraries(versionData.getJSONArray("libraries")); // Ассеты + String assetIndex = versionData.getString("assets"); // ← ВОТ ЭТО ГЛАВНОЕ! + if (versionData.has("assetIndex")) { System.out.println(ZAnsi.cyan("Скачивание ассетов...")); downloadAssets(versionData); + System.out.println(ZAnsi.brightGreen("Asset index определён как: " + assetIndex)); } System.out.println(ZAnsi.brightGreen("\nMinecraft " + versionId + " полностью установлен!")); - return true; + return assetIndex; // ← возвращаем настоящий assetIndex (например "30") } private void downloadLibraries(JSONArray libraries) throws Exception { @@ -103,11 +107,11 @@ public class VersionInstaller { try { downloadFile(url, target, "library"); } catch (Exception e) { - // Многие библиотеки могут быть пропущены — это нормально + // Пропускаем проблемные библиотеки } } count++; - if (count % 20 == 0) ProgressBar.show("Библиотеки", count, total, ""); + ProgressBar.show("Библиотеки", count, total, "файлов"); } ProgressBar.finish("Библиотеки загружены"); } @@ -117,19 +121,29 @@ public class VersionInstaller { String indexUrl = assetIndexInfo.getString("url"); String indexId = versionData.getString("assets"); - Path indexPath = minecraftDir.resolve("assets/indexes").resolve(indexId + ".json"); - Files.createDirectories(indexPath.getParent()); + Path indexesDir = minecraftDir.resolve("assets/indexes"); + Files.createDirectories(indexesDir); + Path indexPath = indexesDir.resolve(indexId + ".json"); + + System.out.println(ZAnsi.cyan("Скачивание asset index (" + indexId + ")...")); downloadFile(indexUrl, indexPath, "asset index"); - String assetsJson = new String(Files.readAllBytes(indexPath)); - JSONObject objects = new JSONObject(assetsJson).getJSONObject("objects"); + String jsonContent = Files.readString(indexPath); + JSONObject root = new JSONObject(jsonContent); + JSONObject objects = root.getJSONObject("objects"); - System.out.println(ZAnsi.cyan("Скачивание " + objects.length() + " объектов ассетов...")); + System.out.println(ZAnsi.cyan("Скачивание " + objects.length() + " объектов ассетов (index: " + indexId + ")...")); - int count = 0; - int failed = 0; + int total = objects.length(); + int[] success = {0}; + int[] failed = {0}; + + List> futures = new ArrayList<>(); + + for (String key : objects.keySet()) { + JSONObject asset = objects.getJSONObject(key); + String hash = asset.getString("hash"); // ← вот это правильный хеш! - 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)) @@ -137,23 +151,43 @@ public class VersionInstaller { Files.createDirectories(target.getParent()); - try { - downloadFile(url, target, ""); // пустой label = ассет - count++; - } catch (Exception e) { - failed++; - } + CompletableFuture future = CompletableFuture.runAsync(() -> { + boolean downloaded = false; + for (int attempt = 1; attempt <= 3; attempt++) { + try { + downloadFile(url, target, ""); + synchronized (this) { + success[0]++; + ProgressBar.show("Ассеты", success[0], total, "файлов"); + } + downloaded = true; + break; + } catch (Exception e) { + if (attempt == 3) { + synchronized (this) { + failed[0]++; + } + System.err.println("Не удалось скачать " + hash); + } else { + try { Thread.sleep(500 * attempt); } catch (InterruptedException ignored) {} + } + } + } + }, executor); - ProgressBar.show("Ассеты", count, objects.length(), "файлов"); + futures.add(future); } - ProgressBar.finish("Ассеты загружены (" + count + " успешно, " + failed + " пропущено)"); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + + ProgressBar.finish("Ассеты загружены (" + success[0] + " успешно, " + failed[0] + " пропущено)"); + + if (failed[0] > 0) { + System.out.println(ZAnsi.yellow("Предупреждение: " + failed[0] + " файлов ассетов не удалось скачать.")); + System.out.println(ZAnsi.yellow("Игра запустится, но некоторые текстуры/звуки могут отсутствовать.")); + } } - - - // === Вспомогательные методы === - private String getVersionUrl(String versionId) throws Exception { for (MinecraftVersion v : getAvailableVersions()) { if (v.getId().equals(versionId)) return v.getUrl(); @@ -174,33 +208,21 @@ public class VersionInstaller { System.out.println(ZAnsi.cyan("Скачивание " + label + "...")); } - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .GET() - .build(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target)); + 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 (response.statusCode() != 200) { + if (label.isEmpty()) return; // для ассетов молча + throw new IOException("HTTP " + response.statusCode() + " при скачивании " + label); + } - 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; - } - // Для ассетов — просто пропускаем молча + if (!label.isEmpty()) { + long size = Files.size(target); + ProgressBar.finish(label + " (" + ProgressBar.formatBytes(size) + ")"); } } } \ 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 index 912aa93..3532566 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java @@ -136,15 +136,30 @@ public class LaunchCommandBuilder { 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"); - } + args.add("--assetIndex"); + args.add(instance.getAssetIndex()); - // Можно добавить --width, --height, --server и т.д. позже + args.add("--username"); + args.add(options.getUsername() != null ? options.getUsername() : "Player"); + + args.add("--accessToken"); + args.add("0"); // потом токен от блядкого сервера + + args.add("--uuid"); + args.add("00000000-0000-0000-0000-000000000000"); // тоже потом от блядкого сервера + + args.add("--userType"); + args.add("legacy"); + + // Дополнительные параметры + if (options.getWidth() > 0) { + args.add("--width"); + args.add(String.valueOf(options.getWidth())); + } + if (options.getHeight() > 0) { + args.add("--height"); + args.add(String.valueOf(options.getHeight())); + } return args; } 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 index 13677a3..ea150b6 100644 --- 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 @@ -11,6 +11,8 @@ public class LaunchOptions { private boolean fullscreen = false; private String javaPath = "java"; private List extraJvmArgs = new ArrayList<>(); + private int width = 854; + private int height = 480; // Геттеры и сеттеры public String getUsername() { return username; } @@ -33,4 +35,7 @@ public class LaunchOptions { public List getExtraJvmArgs() { return extraJvmArgs; } public void setExtraJvmArgs(List extraJvmArgs) { this.extraJvmArgs = extraJvmArgs; } + + public int getWidth() { return width; } + public int getHeight() { return height; } } \ 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 index 4ac8c30..ecfb442 100644 --- a/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Input.java +++ b/launcher/src/main/java/me/sashegdev/zernmc/launcher/utils/Input.java @@ -32,7 +32,13 @@ public class Input { if (value >= min && value <= max) { return value; } - System.out.println(ZAnsi.brightRed("Значение должно быть от " + min + " до " + max)); + System.out.println(ZAnsi.brightRed("Значение должно быть от " + min + " до " + max + ".")); } } + + public static boolean confirm(String prompt) { + System.out.print(prompt + " (да/нет): "); + String answer = scanner.nextLine().trim().toLowerCase(); + return answer.equals("да") || answer.equals("y") || answer.equals("yes"); + } } \ No newline at end of file