Починил загрузку ассетов, добавлена оптимизация
запуск Vanilla версий работает
This commit is contained in:
+1
-1
@@ -7,4 +7,4 @@ server/packs
|
||||
server/data
|
||||
jre
|
||||
.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.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();
|
||||
ConsoleUtils.clearScreen();
|
||||
|
||||
if ("да".equalsIgnoreCase(confirm)) {
|
||||
InstanceManager.getInstance(instance.getName());
|
||||
System.out.println(ZAnsi.brightGreen("Сборка удалена."));
|
||||
List<String> 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.yellow("Отменено."));
|
||||
System.out.println(ZAnsi.brightRed("Не удалось удалить сборку."));
|
||||
}
|
||||
} else {
|
||||
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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -45,49 +44,49 @@ public class Instance {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,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);
|
||||
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.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<String> 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;
|
||||
}
|
||||
|
||||
+13
-8
@@ -28,17 +28,20 @@ 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/"
|
||||
@@ -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 отработал, но версия не найдена."));
|
||||
|
||||
+9
-15
@@ -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,17 +26,21 @@ 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);
|
||||
|
||||
if (!vanillaSuccess) {
|
||||
String assetIndex = vanillaInstaller.install(mcVersion); // ← теперь возвращает String
|
||||
|
||||
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
|
||||
@@ -49,17 +52,15 @@ public class ForgeInstaller {
|
||||
|
||||
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,12 +82,8 @@ 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 = """
|
||||
@@ -95,7 +92,6 @@ public class ForgeInstaller {
|
||||
"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<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target));
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
throw new IOException("Не удалось скачать Forge installer (HTTP " + response.statusCode() + ")");
|
||||
}
|
||||
|
||||
+62
-40
@@ -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<MinecraftVersion> 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<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;
|
||||
Path target = minecraftDir.resolve("assets/objects")
|
||||
.resolve(hash.substring(0, 2))
|
||||
@@ -137,23 +151,43 @@ public class VersionInstaller {
|
||||
|
||||
Files.createDirectories(target.getParent());
|
||||
|
||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
|
||||
boolean downloaded = false;
|
||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
downloadFile(url, target, ""); // пустой label = ассет
|
||||
count++;
|
||||
downloadFile(url, target, "");
|
||||
synchronized (this) {
|
||||
success[0]++;
|
||||
ProgressBar.show("Ассеты", success[0], total, "файлов");
|
||||
}
|
||||
downloaded = true;
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
failed++;
|
||||
if (attempt == 3) {
|
||||
synchronized (this) {
|
||||
failed[0]++;
|
||||
}
|
||||
System.err.println("Не удалось скачать " + hash);
|
||||
} else {
|
||||
try { Thread.sleep(500 * attempt); } catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, executor);
|
||||
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
ProgressBar.show("Ассеты", count, objects.length(), "файлов");
|
||||
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("Игра запустится, но некоторые текстуры/звуки могут отсутствовать."));
|
||||
}
|
||||
|
||||
ProgressBar.finish("Ассеты загружены (" + count + " успешно, " + failed + " пропущено)");
|
||||
}
|
||||
|
||||
|
||||
|
||||
// === Вспомогательные методы ===
|
||||
|
||||
private String getVersionUrl(String versionId) throws Exception {
|
||||
for (MinecraftVersion v : getAvailableVersions()) {
|
||||
if (v.getId().equals(versionId)) return v.getUrl();
|
||||
@@ -174,7 +208,6 @@ public class VersionInstaller {
|
||||
System.out.println(ZAnsi.cyan("Скачивание " + label + "..."));
|
||||
}
|
||||
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.GET()
|
||||
@@ -183,24 +216,13 @@ public class VersionInstaller {
|
||||
HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target));
|
||||
|
||||
if (response.statusCode() != 200) {
|
||||
// Для ассетов 404 — это нормально, просто пропускаем
|
||||
if (label.isEmpty()) {
|
||||
return; // тихий пропуск для ассетов
|
||||
}
|
||||
throw new IOException("HTTP " + response.statusCode());
|
||||
if (label.isEmpty()) 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;
|
||||
}
|
||||
// Для ассетов — просто пропускаем молча
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-8
@@ -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;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ public class LaunchOptions {
|
||||
private boolean fullscreen = false;
|
||||
private String javaPath = "java";
|
||||
private List<String> 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<String> getExtraJvmArgs() { return 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) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user