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