Починил загрузку ассетов, добавлена оптимизация

запуск Vanilla версий работает
This commit is contained in:
Sashegdev
2026-04-05 14:56:01 +00:00
parent 2babe53e99
commit ac3ce1800f
11 changed files with 298 additions and 144 deletions
+1 -1
View File
@@ -7,4 +7,4 @@ server/packs
server/data server/data
jre jre
.vscode .vscode
launcher/dependency-reduced-pom.xml dependency-reduced-pom.xml
@@ -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.minecraft.model.MinecraftVersion;
import me.sashegdev.zernmc.launcher.ui.ArrowMenu; import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; 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.ZAnsi;
import me.sashegdev.zernmc.launcher.utils.ZHttpClient; import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
@@ -219,16 +220,31 @@ public class LaunchMenu {
} }
private void deleteInstance(Instance instance) throws IOException { private void deleteInstance(Instance instance) throws IOException {
System.out.println(ZAnsi.brightRed("Вы действительно хотите удалить сборку '" + instance.getName() + "'?")); ConsoleUtils.clearScreen();
System.out.print(ZAnsi.white("Введите 'да' для подтверждения: "));
String confirm = new java.util.Scanner(System.in).nextLine().trim();
if ("да".equalsIgnoreCase(confirm)) { List<String> confirmOptions = List.of(
InstanceManager.getInstance(instance.getName()); "Да, удалить сборку",
System.out.println(ZAnsi.brightGreen("Сборка удалена.")); "Нет, отменить"
);
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 { } else {
System.out.println(ZAnsi.yellow("Отменено.")); System.out.println(ZAnsi.yellow("Удаление отменено."));
} }
ConsoleUtils.pause(); ConsoleUtils.pause();
} }
@@ -282,7 +298,7 @@ public class LaunchMenu {
private String askPackName() { private String askPackName() {
System.out.print(ZAnsi.white("\nВведите название новой сборки: ")); System.out.print(ZAnsi.white("\nВведите название новой сборки: "));
String name = new java.util.Scanner(System.in).nextLine().trim(); String name = Input.readLine(); // используем наш Input
if (name.isEmpty()) { if (name.isEmpty()) {
System.out.println(ZAnsi.yellow("Отменено.")); System.out.println(ZAnsi.yellow("Отменено."));
return null; return null;
@@ -2,19 +2,18 @@ package me.sashegdev.zernmc.launcher.minecraft;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
public class Instance { public class Instance {
private final String name; private final String name;
private final Path path; private final Path path;
private String minecraftVersion; private String minecraftVersion;
private String loaderType; // vanilla, fabric, forge private String loaderType; // vanilla, fabric, forge
private String loaderVersion; private String loaderVersion;
private String assetIndex; // ← ЭТО САМОЕ ВАЖНОЕ
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
@@ -45,49 +44,49 @@ public class Instance {
saveMetadata(); saveMetadata();
} }
/** Возвращает ТОТ САМЫЙ assetIndex, который сохранился при установке (например 30) */
public String getAssetIndex() {
return assetIndex != null ? assetIndex : minecraftVersion; // fallback для старых сборок
}
public void setAssetIndex(String assetIndex) {
this.assetIndex = assetIndex;
saveMetadata();
}
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(name); StringBuilder sb = new StringBuilder(name);
if (minecraftVersion != null) { if (minecraftVersion != null) {
sb.append(" [").append(minecraftVersion); sb.append(" [").append(minecraftVersion);
if (!"vanilla".equalsIgnoreCase(getLoaderType())) { if (!"vanilla".equalsIgnoreCase(getLoaderType())) {
sb.append(" + ").append(getLoaderType()); sb.append(" + ").append(getLoaderType());
if (loaderVersion != null) { if (loaderVersion != null) sb.append(" ").append(loaderVersion);
sb.append(" ").append(loaderVersion);
}
} }
sb.append("]"); sb.append("]");
} else { } else {
sb.append(" [?]"); sb.append(" [?]");
} }
return sb.toString(); return sb.toString();
} }
// ====================== Метаданные ====================== // ====================== Метаданные ======================
private void loadMetadata() { private void loadMetadata() {
Path metaFile = path.resolve("instance.json"); Path metaFile = path.resolve("instance.json");
if (!Files.exists(metaFile)) return; if (!Files.exists(metaFile)) return;
try { try {
String json = Files.readString(metaFile); String json = Files.readString(metaFile);
InstanceMeta meta = GSON.fromJson(json, InstanceMeta.class); InstanceMeta meta = GSON.fromJson(json, InstanceMeta.class);
this.minecraftVersion = meta.minecraftVersion; this.minecraftVersion = meta.minecraftVersion;
this.loaderType = meta.loaderType; this.loaderType = meta.loaderType;
this.loaderVersion = meta.loaderVersion; this.loaderVersion = meta.loaderVersion;
} catch (Exception e) { this.assetIndex = meta.assetIndex;
// игнорируем, если файл повреждён } catch (Exception ignored) {}
}
} }
private void saveMetadata() { private void saveMetadata() {
Path metaFile = path.resolve("instance.json"); Path metaFile = path.resolve("instance.json");
InstanceMeta meta = new InstanceMeta(minecraftVersion, loaderType, loaderVersion); InstanceMeta meta = new InstanceMeta(minecraftVersion, loaderType, loaderVersion, assetIndex);
try { try {
Files.writeString(metaFile, GSON.toJson(meta)); Files.writeString(metaFile, GSON.toJson(meta));
} catch (IOException e) { } catch (IOException e) {
@@ -95,16 +94,18 @@ public class Instance {
} }
} }
// Внутренний класс для сериализации
private static class InstanceMeta { private static class InstanceMeta {
String minecraftVersion; String minecraftVersion;
String loaderType; String loaderType;
String loaderVersion; 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.minecraftVersion = minecraftVersion;
this.loaderType = loaderType; this.loaderType = loaderType;
this.loaderVersion = loaderVersion; this.loaderVersion = loaderVersion;
this.assetIndex = assetIndex;
} }
} }
} }
@@ -32,6 +32,35 @@ public class InstanceManager {
return null; 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 { public static boolean createInstanceFolder(String name) throws IOException {
Path path = INSTANCES_DIR.resolve(name); Path path = INSTANCES_DIR.resolve(name);
if (Files.exists(path)) { if (Files.exists(path)) {
@@ -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.ConsoleUtils;
import me.sashegdev.zernmc.launcher.utils.ZAnsi; 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; import java.util.List;
public class MinecraftLib { public class MinecraftLib {
@@ -24,13 +27,16 @@ public class MinecraftLib {
//Установка //Установка
public boolean installMinecraft(String versionId) throws Exception { public boolean installMinecraft(String versionId) throws Exception {
VersionInstaller installer = new VersionInstaller(instance.getPath()); 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.setMinecraftVersion(versionId);
instance.setAssetIndex(assetIndex); // ← сохраняем правильный индекс!
instance.setLoaderType("vanilla"); instance.setLoaderType("vanilla");
return true;
} }
return success; return false;
} }
public boolean installForge(String minecraftVersion, String forgeVersion) throws Exception { public boolean installForge(String minecraftVersion, String forgeVersion) throws Exception {
@@ -86,34 +92,89 @@ public class MinecraftLib {
//Запуск //Запуск
public void launch(LaunchOptions options) throws Exception { public void launch(LaunchOptions options) throws Exception {
System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName())); System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName()));
cleanupOldLoaders();
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance); LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
List<String> command = builder.build(options); List<String> command = builder.build(options);
System.out.println(ZAnsi.cyan("Команда запуска (" + command.size() + " аргументов):")); System.out.println(ZAnsi.cyan("Команда запуска (" + command.size() + " аргументов):"));
for (String arg : command) { command.forEach(arg -> System.out.println(" " + arg));
System.out.println(" " + arg);
}
ProcessBuilder pb = new ProcessBuilder(command); ProcessBuilder pb = new ProcessBuilder(command);
pb.directory(instance.getPath().toFile()); pb.directory(instance.getPath().toFile());
// Важно: перенаправляем вывод Minecraft в консоль лаунчера
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT); pb.redirectError(ProcessBuilder.Redirect.INHERIT);
pb.redirectInput(ProcessBuilder.Redirect.INHERIT); pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
System.out.println(ZAnsi.brightGreen("\nЗапускаем Minecraft...\n")); System.out.println(ZAnsi.brightGreen("\nЗапускаем Minecraft...\n"));
ConsoleUtils.clearScreen(); // очищаем TUI перед запуском игры ConsoleUtils.clearScreen();
Process process = pb.start(); Process process = pb.start();
// Ждём завершения игры
int exitCode = process.waitFor(); int exitCode = process.waitFor();
System.out.println(ZAnsi.yellow("\nMinecraft завершился с кодом: " + exitCode)); 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() { public Instance getInstance() {
return instance; return instance;
} }
@@ -28,17 +28,20 @@ public class FabricInstaller {
System.out.println(ZAnsi.cyan("Установка Fabric " + loaderVersion + " для Minecraft " + minecraftVersion)); System.out.println(ZAnsi.cyan("Установка Fabric " + loaderVersion + " для Minecraft " + minecraftVersion));
Path instancePath = instance.getPath(); Path instancePath = instance.getPath();
cleanOldFabricLoaders(); cleanOldFabricLoaders();
// Шаг 1: Установка vanilla версии (если ещё не установлена) // Шаг 1: Устанавливаем vanilla и получаем assetIndex
VersionInstaller versionInstaller = new VersionInstaller(instancePath); VersionInstaller versionInstaller = new VersionInstaller(instancePath);
boolean mcOk = versionInstaller.install(minecraftVersion); String assetIndex = versionInstaller.install(minecraftVersion); // ← теперь String
if (!mcOk) {
if (assetIndex == null || assetIndex.isEmpty()) {
System.out.println(ZAnsi.brightRed("Не удалось установить Minecraft " + minecraftVersion)); System.out.println(ZAnsi.brightRed("Не удалось установить Minecraft " + minecraftVersion));
return false; return false;
} }
// Сохраняем assetIndex
instance.setAssetIndex(assetIndex);
// Шаг 2: Скачивание и запуск Fabric Installer // Шаг 2: Скачивание и запуск Fabric Installer
String installerVersion = getLatestInstallerVersion(); String installerVersion = getLatestInstallerVersion();
String installerUrl = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/" String installerUrl = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/"
@@ -50,9 +53,7 @@ public class FabricInstaller {
downloadFile(installerUrl, installerJar); downloadFile(installerUrl, installerJar);
ProgressBar.finish("Fabric Installer скачан"); ProgressBar.finish("Fabric Installer скачан");
// Шаг 3: Запуск Fabric Installer
System.out.println(ZAnsi.cyan("Запуск Fabric Installer...")); System.out.println(ZAnsi.cyan("Запуск Fabric Installer..."));
ProcessBuilder pb = new ProcessBuilder( ProcessBuilder pb = new ProcessBuilder(
"java", "-jar", installerJar.toAbsolutePath().toString(), "java", "-jar", installerJar.toAbsolutePath().toString(),
"client", "client",
@@ -62,7 +63,6 @@ public class FabricInstaller {
"-noprofile", "-noprofile",
"-snapshot" "-snapshot"
); );
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT); pb.redirectError(ProcessBuilder.Redirect.INHERIT);
@@ -74,13 +74,18 @@ public class FabricInstaller {
return false; return false;
} }
// Шаг 4: Проверка, что Fabric версия появилась // Проверка результата
String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion; String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion;
Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId); Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId);
if (Files.exists(fabricVersionDir)) { if (Files.exists(fabricVersionDir)) {
System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!")); System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!"));
System.out.println("Версия: " + fabricVersionId); System.out.println("Версия: " + fabricVersionId);
instance.setMinecraftVersion(minecraftVersion);
instance.setLoaderType("fabric");
instance.setLoaderVersion(loaderVersion);
return true; return true;
} else { } else {
System.out.println(ZAnsi.brightRed("Fabric Installer отработал, но версия не найдена.")); System.out.println(ZAnsi.brightRed("Fabric Installer отработал, но версия не найдена."));
@@ -3,7 +3,6 @@ package me.sashegdev.zernmc.launcher.minecraft.installer;
import me.sashegdev.zernmc.launcher.minecraft.Instance; import me.sashegdev.zernmc.launcher.minecraft.Instance;
import me.sashegdev.zernmc.launcher.utils.ProgressBar; import me.sashegdev.zernmc.launcher.utils.ProgressBar;
import me.sashegdev.zernmc.launcher.utils.ZAnsi; import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
@@ -27,17 +26,21 @@ public class ForgeInstaller {
public boolean install(String mcVersion, String forgeVersion) throws Exception { public boolean install(String mcVersion, String forgeVersion) throws Exception {
System.out.println(ZAnsi.cyan("Установка Forge " + forgeVersion + " для Minecraft " + mcVersion)); System.out.println(ZAnsi.cyan("Установка Forge " + forgeVersion + " для Minecraft " + mcVersion));
// Шаг 1: Полная установка vanilla // Шаг 1: Устанавливаем vanilla и получаем настоящий assetIndex
System.out.println(ZAnsi.cyan("Установка базовой версии Minecraft " + mcVersion + "...")); System.out.println(ZAnsi.cyan("Установка базовой версии Minecraft " + mcVersion + "..."));
VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath()); VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath());
boolean vanillaSuccess = vanillaInstaller.install(mcVersion);
if (!vanillaSuccess) { String assetIndex = vanillaInstaller.install(mcVersion); // ← теперь возвращает String
if (assetIndex == null || assetIndex.isEmpty()) {
System.out.println(ZAnsi.brightRed("Не удалось установить базовую версию Minecraft")); System.out.println(ZAnsi.brightRed("Не удалось установить базовую версию Minecraft"));
return false; return false;
} }
// Шаг 2: Создаём launcher_profiles.json (критично для Forge) // Сохраняем assetIndex (очень важно!)
instance.setAssetIndex(assetIndex);
// Шаг 2: Создаём launcher_profiles.json
createLauncherProfile(); createLauncherProfile();
// Шаг 3: Скачиваем и запускаем Forge Installer // Шаг 3: Скачиваем и запускаем Forge Installer
@@ -49,17 +52,15 @@ public class ForgeInstaller {
ProgressBar.show("Скачивание Forge Installer", 0, 100, "%"); ProgressBar.show("Скачивание Forge Installer", 0, 100, "%");
downloadFile(installerUrl, installerJar); downloadFile(installerUrl, installerJar);
ProgressBar.finish("Forge Installer скачан (" + ProgressBar.formatBytes(Files.size(installerJar)) + ")"); ProgressBar.finish("Forge Installer скачан");
System.out.println(ZAnsi.cyan("Запуск Forge Installer...")); System.out.println(ZAnsi.cyan("Запуск Forge Installer..."));
ProcessBuilder pb = new ProcessBuilder( ProcessBuilder pb = new ProcessBuilder(
"java", "java",
"-jar", "-jar",
installerJar.toAbsolutePath().toString(), installerJar.toAbsolutePath().toString(),
"--installClient" "--installClient"
); );
pb.directory(instance.getPath().toFile()); pb.directory(instance.getPath().toFile());
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT); pb.redirectError(ProcessBuilder.Redirect.INHERIT);
@@ -81,21 +82,16 @@ public class ForgeInstaller {
return true; return true;
} }
/**
* Создаёт минимальный launcher_profiles.json — Forge без него отказывается работать
*/
private void createLauncherProfile() throws IOException { private void createLauncherProfile() throws IOException {
Path profilePath = instance.getPath().resolve("launcher_profiles.json"); Path profilePath = instance.getPath().resolve("launcher_profiles.json");
if (Files.exists(profilePath)) return; if (Files.exists(profilePath)) return;
String minimalProfile = """ String minimalProfile = """
{ {
"profiles": {}, "profiles": {},
"selectedProfile": "Default" "selectedProfile": "Default"
} }
"""; """;
Files.writeString(profilePath, minimalProfile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); Files.writeString(profilePath, minimalProfile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println(ZAnsi.yellow("Создан launcher_profiles.json")); System.out.println(ZAnsi.yellow("Создан launcher_profiles.json"));
} }
@@ -105,9 +101,7 @@ public class ForgeInstaller {
.uri(URI.create(url)) .uri(URI.create(url))
.GET() .GET()
.build(); .build();
HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target)); HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() != 200) { if (response.statusCode() != 200) {
throw new IOException("Не удалось скачать Forge installer (HTTP " + response.statusCode() + ")"); throw new IOException("Не удалось скачать Forge installer (HTTP " + response.statusCode() + ")");
} }
@@ -17,21 +17,23 @@ import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VersionInstaller { public class VersionInstaller {
private final Path minecraftDir; private final Path minecraftDir;
private final HttpClient httpClient; private final HttpClient httpClient;
private final ExecutorService executor = Executors.newFixedThreadPool(32); // параллельная загрузка
public VersionInstaller(Path minecraftDir) { public VersionInstaller(Path minecraftDir) {
this.minecraftDir = minecraftDir; this.minecraftDir = minecraftDir;
this.httpClient = HttpClient.newBuilder() this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30)) .connectTimeout(Duration.ofSeconds(15))
.build(); .build();
} }
// getAvailableVersions() оставляем как было (с исправлением времени)
public List<MinecraftVersion> getAvailableVersions() throws Exception { public List<MinecraftVersion> getAvailableVersions() throws Exception {
String jsonString = downloadString("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"); String jsonString = downloadString("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json");
JSONObject root = new JSONObject(jsonString); JSONObject root = new JSONObject(jsonString);
@@ -54,9 +56,8 @@ public class VersionInstaller {
return versions; 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 + "...")); System.out.println(ZAnsi.cyan("Полная установка Minecraft " + versionId + "..."));
Path versionDir = minecraftDir.resolve("versions").resolve(versionId); Path versionDir = minecraftDir.resolve("versions").resolve(versionId);
Files.createDirectories(versionDir); Files.createDirectories(versionDir);
@@ -77,13 +78,16 @@ public class VersionInstaller {
downloadLibraries(versionData.getJSONArray("libraries")); downloadLibraries(versionData.getJSONArray("libraries"));
// Ассеты // Ассеты
String assetIndex = versionData.getString("assets"); // ← ВОТ ЭТО ГЛАВНОЕ!
if (versionData.has("assetIndex")) { if (versionData.has("assetIndex")) {
System.out.println(ZAnsi.cyan("Скачивание ассетов...")); System.out.println(ZAnsi.cyan("Скачивание ассетов..."));
downloadAssets(versionData); downloadAssets(versionData);
System.out.println(ZAnsi.brightGreen("Asset index определён как: " + assetIndex));
} }
System.out.println(ZAnsi.brightGreen("\nMinecraft " + versionId + " полностью установлен!")); System.out.println(ZAnsi.brightGreen("\nMinecraft " + versionId + " полностью установлен!"));
return true; return assetIndex; // ← возвращаем настоящий assetIndex (например "30")
} }
private void downloadLibraries(JSONArray libraries) throws Exception { private void downloadLibraries(JSONArray libraries) throws Exception {
@@ -103,11 +107,11 @@ public class VersionInstaller {
try { try {
downloadFile(url, target, "library"); downloadFile(url, target, "library");
} catch (Exception e) { } catch (Exception e) {
// Многие библиотеки могут быть пропущены — это нормально // Пропускаем проблемные библиотеки
} }
} }
count++; count++;
if (count % 20 == 0) ProgressBar.show("Библиотеки", count, total, ""); ProgressBar.show("Библиотеки", count, total, "файлов");
} }
ProgressBar.finish("Библиотеки загружены"); ProgressBar.finish("Библиотеки загружены");
} }
@@ -117,19 +121,29 @@ public class VersionInstaller {
String indexUrl = assetIndexInfo.getString("url"); String indexUrl = assetIndexInfo.getString("url");
String indexId = versionData.getString("assets"); String indexId = versionData.getString("assets");
Path indexPath = minecraftDir.resolve("assets/indexes").resolve(indexId + ".json"); Path indexesDir = minecraftDir.resolve("assets/indexes");
Files.createDirectories(indexPath.getParent()); Files.createDirectories(indexesDir);
Path indexPath = indexesDir.resolve(indexId + ".json");
System.out.println(ZAnsi.cyan("Скачивание asset index (" + indexId + ")..."));
downloadFile(indexUrl, indexPath, "asset index"); downloadFile(indexUrl, indexPath, "asset index");
String assetsJson = new String(Files.readAllBytes(indexPath)); String jsonContent = Files.readString(indexPath);
JSONObject objects = new JSONObject(assetsJson).getJSONObject("objects"); 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 total = objects.length();
int failed = 0; int[] success = {0};
int[] failed = {0};
List<CompletableFuture<Void>> 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; String url = "https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash;
Path target = minecraftDir.resolve("assets/objects") Path target = minecraftDir.resolve("assets/objects")
.resolve(hash.substring(0, 2)) .resolve(hash.substring(0, 2))
@@ -137,23 +151,43 @@ public class VersionInstaller {
Files.createDirectories(target.getParent()); Files.createDirectories(target.getParent());
try { CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
downloadFile(url, target, ""); // пустой label = ассет boolean downloaded = false;
count++; for (int attempt = 1; attempt <= 3; attempt++) {
} catch (Exception e) { try {
failed++; 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 { private String getVersionUrl(String versionId) throws Exception {
for (MinecraftVersion v : getAvailableVersions()) { for (MinecraftVersion v : getAvailableVersions()) {
if (v.getId().equals(versionId)) return v.getUrl(); if (v.getId().equals(versionId)) return v.getUrl();
@@ -174,33 +208,21 @@ public class VersionInstaller {
System.out.println(ZAnsi.cyan("Скачивание " + label + "...")); System.out.println(ZAnsi.cyan("Скачивание " + label + "..."));
} }
try { HttpRequest request = HttpRequest.newBuilder()
HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url))
.uri(URI.create(url)) .GET()
.GET() .build();
.build();
HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target)); HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() != 200) { if (response.statusCode() != 200) {
// Для ассетов 404 — это нормально, просто пропускаем if (label.isEmpty()) return; // для ассетов молча
if (label.isEmpty()) { throw new IOException("HTTP " + response.statusCode() + " при скачивании " + label);
return; // тихий пропуск для ассетов }
}
throw new IOException("HTTP " + response.statusCode());
}
if (!label.isEmpty()) { if (!label.isEmpty()) {
long size = Files.size(target); long size = Files.size(target);
ProgressBar.finish(label + " (" + ProgressBar.formatBytes(size) + ")"); ProgressBar.finish(label + " (" + ProgressBar.formatBytes(size) + ")");
}
} catch (Exception e) {
if (!label.isEmpty()) {
// Для важных файлов (client.jar, библиотеки, index) — ошибка
throw e;
}
// Для ассетов — просто пропускаем молча
} }
} }
} }
@@ -136,15 +136,30 @@ public class LaunchCommandBuilder {
args.add("--assetsDir"); args.add("--assetsDir");
args.add(instance.getPath().resolve("assets").toAbsolutePath().toString()); args.add(instance.getPath().resolve("assets").toAbsolutePath().toString());
if (options.getUsername() != null) { args.add("--assetIndex");
args.add("--username"); args.add(instance.getAssetIndex());
args.add(options.getUsername());
} else {
args.add("--username");
args.add("Player");
}
// Можно добавить --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; return args;
} }
@@ -11,6 +11,8 @@ public class LaunchOptions {
private boolean fullscreen = false; private boolean fullscreen = false;
private String javaPath = "java"; private String javaPath = "java";
private List<String> extraJvmArgs = new ArrayList<>(); private List<String> extraJvmArgs = new ArrayList<>();
private int width = 854;
private int height = 480;
// Геттеры и сеттеры // Геттеры и сеттеры
public String getUsername() { return username; } public String getUsername() { return username; }
@@ -33,4 +35,7 @@ public class LaunchOptions {
public List<String> getExtraJvmArgs() { return extraJvmArgs; } public List<String> getExtraJvmArgs() { return extraJvmArgs; }
public void setExtraJvmArgs(List<String> extraJvmArgs) { this.extraJvmArgs = extraJvmArgs; } public void setExtraJvmArgs(List<String> extraJvmArgs) { this.extraJvmArgs = extraJvmArgs; }
public int getWidth() { return width; }
public int getHeight() { return height; }
} }
@@ -32,7 +32,13 @@ public class Input {
if (value >= min && value <= max) { if (value >= min && value <= max) {
return value; 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");
}
} }