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

запуск 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
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();
if ("да".equalsIgnoreCase(confirm)) {
InstanceManager.getInstance(instance.getName());
System.out.println(ZAnsi.brightGreen("Сборка удалена."));
ConsoleUtils.clearScreen();
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.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;
@@ -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;
}
}
}
@@ -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);
@@ -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;
}
@@ -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 отработал, но версия не найдена."));
@@ -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<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() != 200) {
throw new IOException("Не удалось скачать Forge installer (HTTP " + response.statusCode() + ")");
}
@@ -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());
try {
downloadFile(url, target, ""); // пустой label = ассет
count++;
} catch (Exception e) {
failed++;
}
CompletableFuture<Void> 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<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target));
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 (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) + ")");
}
}
}
@@ -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");
}
}