Both | БЛЯЯЯ ЗАГРУЗКА ПАКОВ С СЕРВЕРА СЮДААА

This commit is contained in:
Sashegdev
2026-04-06 00:32:36 +00:00
parent 4edbe7e910
commit 0b4af1353d
13 changed files with 1482 additions and 373 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>me.sashegdev</groupId> <groupId>me.sashegdev</groupId>
<artifactId>ZernMCLauncher</artifactId> <artifactId>ZernMCLauncher</artifactId>
<version>1.0.2</version> <version>1.0.3</version>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
+1 -1
View File
@@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>me.sashegdev</groupId> <groupId>me.sashegdev</groupId>
<artifactId>ZernMCLauncher</artifactId> <artifactId>ZernMCLauncher</artifactId>
<version>1.0.2</version> <version>1.0.4</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
@@ -3,6 +3,8 @@ package me.sashegdev.zernmc.launcher.menu;
import me.sashegdev.zernmc.launcher.minecraft.Instance; import me.sashegdev.zernmc.launcher.minecraft.Instance;
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager; import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
import me.sashegdev.zernmc.launcher.minecraft.MinecraftLib; 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.installer.VersionInstaller;
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions; import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
import me.sashegdev.zernmc.launcher.minecraft.model.MinecraftVersion; import me.sashegdev.zernmc.launcher.minecraft.model.MinecraftVersion;
@@ -35,7 +37,7 @@ public class LaunchMenu {
int choice = menu.show(); int choice = menu.show();
if (choice == -1) break; if (choice == -1) break;
if (choice == options.size() - 1) break; // Назад if (choice == options.size() - 1) break;
if (choice == instances.size()) { if (choice == instances.size()) {
installNewPack(); installNewPack();
@@ -47,11 +49,154 @@ public class LaunchMenu {
} }
} }
private void installNewPack() throws IOException { private void installNewPack() throws Exception {
ConsoleUtils.clearScreen();
List<String> 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<ServerPack> availablePacks = tempDownloader.getAvailablePacks();
if (availablePacks.isEmpty()) {
System.out.println(ZAnsi.yellow("Нет доступных сборок на сервере."));
ConsoleUtils.pause();
return;
}
// Исправлено: убраны спецсимволы для Windows
List<String> 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..."));
VersionInstaller versionInstaller = new VersionInstaller(null);
List<MinecraftVersion> allVersions = versionInstaller.getAvailableVersions();
List<String> 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(); ConsoleUtils.clearScreen();
System.out.println(ZAnsi.cyan("Получение списка версий Minecraft...")); System.out.println(ZAnsi.cyan("Получение списка версий Minecraft..."));
try {
VersionInstaller versionInstaller = new VersionInstaller(null); VersionInstaller versionInstaller = new VersionInstaller(null);
List<MinecraftVersion> allVersions = versionInstaller.getAvailableVersions(); List<MinecraftVersion> allVersions = versionInstaller.getAvailableVersions();
@@ -78,7 +223,7 @@ public class LaunchMenu {
String selectedLoader = loaderOptions.get(loaderChoice); String selectedLoader = loaderOptions.get(loaderChoice);
if (selectedLoader.contains("Vanilla")) { if (selectedLoader.contains("Vanilla")) {
createVanillaInstance(mcVersion); createVanillaInstance();
return; return;
} }
@@ -96,6 +241,12 @@ public class LaunchMenu {
String packName = askPackName(); String packName = askPackName();
if (packName == null) return; if (packName == null) return;
if (InstanceManager.getInstance(packName) != null) {
System.out.println(ZAnsi.brightRed("Сборка с таким именем уже существует!"));
ConsoleUtils.pause();
return;
}
InstanceManager.createInstanceFolder(packName); InstanceManager.createInstanceFolder(packName);
Instance newInstance = InstanceManager.getInstance(packName); Instance newInstance = InstanceManager.getInstance(packName);
@@ -106,11 +257,9 @@ public class LaunchMenu {
: lib.installForge(mcVersion, loaderVersion); : lib.installForge(mcVersion, loaderVersion);
if (success) { if (success) {
System.out.println(ZAnsi.brightGreen("\nСборка '" + packName + "' успешно установлена!")); System.out.println(ZAnsi.brightGreen("\n[OK] Сборка '" + packName + "' успешно установлена!"));
} } else {
System.out.println(ZAnsi.brightRed("\n[FAIL] Не удалось установить сборку."));
} catch (Exception e) {
System.out.println(ZAnsi.brightRed("Ошибка: " + e.getMessage()));
} }
ConsoleUtils.pause(); ConsoleUtils.pause();
@@ -134,13 +283,10 @@ public class LaunchMenu {
} }
private boolean isFabricSupported(String version) { private boolean isFabricSupported(String version) {
// Fabric стабильно работает с 1.14+
return version.matches("^1\\.(1[4-9]|[2-9]\\d).*"); return version.matches("^1\\.(1[4-9]|[2-9]\\d).*");
} }
private boolean isForgeSupported(String version) { private boolean isForgeSupported(String version) {
// Forge поддерживает примерно до 1.21.4 на текущий момент
// Для версий 1.22+ и экспериментальных — отключаем
if (version.matches("^1\\.2[2-9].*") || version.matches("^\\d{2}.*")) { if (version.matches("^1\\.2[2-9].*") || version.matches("^\\d{2}.*")) {
return false; return false;
} }
@@ -156,8 +302,15 @@ public class LaunchMenu {
System.out.println(ZAnsi.white("Лоадер: " + instance.getLoaderType() + System.out.println(ZAnsi.white("Лоадер: " + instance.getLoaderType() +
(instance.getLoaderVersion() != null ? " " + instance.getLoaderVersion() : ""))); (instance.getLoaderVersion() != null ? " " + instance.getLoaderVersion() : "")));
if (instance.isServerPack()) {
System.out.println(ZAnsi.green("Серверная сборка: v" + instance.getServerVersion()));
}
List<String> options = new ArrayList<>(); List<String> options = new ArrayList<>();
options.add("Запустить сборку"); options.add("Запустить сборку");
if (instance.isServerPack()) {
options.add("Проверить обновления");
}
options.add("Изменить версию лоадера"); options.add("Изменить версию лоадера");
options.add("Удалить сборку"); options.add("Удалить сборку");
options.add("Назад"); options.add("Назад");
@@ -165,14 +318,55 @@ public class LaunchMenu {
ArrowMenu menu = new ArrowMenu("Действия", options); ArrowMenu menu = new ArrowMenu("Действия", options);
int choice = menu.show(); int choice = menu.show();
if (choice == -1 || choice == 3) return; // Esc или Назад if (choice == -1 || choice == options.size() - 1) return;
switch (choice) { switch (choice) {
case 0 -> launchExistingInstance(instance); case 0 -> launchExistingInstance(instance);
case 1 -> changeLoaderVersion(instance); case 1 -> {
case 2 -> deleteInstance(instance); 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 { private void changeLoaderVersion(Instance instance) throws Exception {
@@ -197,7 +391,7 @@ public class LaunchMenu {
if (newLoaderVersion == null) return; if (newLoaderVersion == null) return;
System.out.println(ZAnsi.cyan("Переустановка лоадера " + currentLoader + " " + newLoaderVersion + "...")); System.out.println(ZAnsi.cyan("Переустановка лоадера " + currentLoader + " -> " + newLoaderVersion + "..."));
MinecraftLib lib = new MinecraftLib(instance); MinecraftLib lib = new MinecraftLib(instance);
boolean success; boolean success;
@@ -211,6 +405,8 @@ public class LaunchMenu {
if (success) { if (success) {
System.out.println(ZAnsi.brightGreen("Версия лоадера успешно изменена!")); System.out.println(ZAnsi.brightGreen("Версия лоадера успешно изменена!"));
} else {
System.out.println(ZAnsi.brightRed("Не удалось изменить версию лоадера."));
} }
} catch (Exception e) { } catch (Exception e) {
System.out.println(ZAnsi.brightRed("Ошибка при смене лоадера: " + e.getMessage())); System.out.println(ZAnsi.brightRed("Ошибка при смене лоадера: " + e.getMessage()));
@@ -234,7 +430,7 @@ public class LaunchMenu {
int choice = confirmMenu.show(); int choice = confirmMenu.show();
if (choice == 0) { // "Да, удалить" if (choice == 0) {
boolean deleted = InstanceManager.deleteInstance(instance.getName()); boolean deleted = InstanceManager.deleteInstance(instance.getName());
if (deleted) { if (deleted) {
System.out.println(ZAnsi.brightGreen("Сборка '" + instance.getName() + "' успешно удалена.")); System.out.println(ZAnsi.brightGreen("Сборка '" + instance.getName() + "' успешно удалена."));
@@ -253,7 +449,7 @@ public class LaunchMenu {
List<String> versions = ZHttpClient.getFabricLoaderVersions(); List<String> versions = ZHttpClient.getFabricLoaderVersions();
List<String> options = versions.stream() List<String> options = versions.stream()
.limit(30) // увеличил до 30 .limit(30)
.map(v -> "Fabric Loader " + v) .map(v -> "Fabric Loader " + v)
.collect(Collectors.toList()); .collect(Collectors.toList());
options.add("Назад"); options.add("Назад");
@@ -268,13 +464,11 @@ public class LaunchMenu {
private String askForgeVersion(String mcVersion) throws Exception { private String askForgeVersion(String mcVersion) throws Exception {
System.out.println(ZAnsi.cyan("Получение списка версий Forge для " + mcVersion + "...")); System.out.println(ZAnsi.cyan("Получение списка версий Forge для " + mcVersion + "..."));
// Получаем все версии Forge из Maven
List<String> allForgeVersions = getAllForgeVersions(); List<String> allForgeVersions = getAllForgeVersions();
// Фильтруем только те, которые подходят под нашу версию Minecraft
List<String> compatibleVersions = allForgeVersions.stream() List<String> compatibleVersions = allForgeVersions.stream()
.filter(v -> v.startsWith(mcVersion + "-")) .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()); .collect(Collectors.toList());
if (compatibleVersions.isEmpty()) { if (compatibleVersions.isEmpty()) {
@@ -284,6 +478,7 @@ public class LaunchMenu {
} }
List<String> options = compatibleVersions.stream() List<String> options = compatibleVersions.stream()
.limit(30)
.map(v -> "Forge " + v) .map(v -> "Forge " + v)
.collect(Collectors.toList()); .collect(Collectors.toList());
options.add("Назад"); options.add("Назад");
@@ -298,7 +493,7 @@ public class LaunchMenu {
private String askPackName() { private String askPackName() {
System.out.print(ZAnsi.white("\nВведите название новой сборки: ")); System.out.print(ZAnsi.white("\nВведите название новой сборки: "));
String name = Input.readLine(); // используем наш Input String name = Input.readLine();
if (name.isEmpty()) { if (name.isEmpty()) {
System.out.println(ZAnsi.yellow("Отменено.")); System.out.println(ZAnsi.yellow("Отменено."));
return null; return null;
@@ -306,21 +501,6 @@ public class LaunchMenu {
return name; 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) { private void launchExistingInstance(Instance instance) {
ConsoleUtils.clearScreen(); ConsoleUtils.clearScreen();
System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName())); System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName()));
@@ -332,6 +512,7 @@ public class LaunchMenu {
lib.launch(options); lib.launch(options);
} catch (Exception e) { } catch (Exception e) {
System.out.println(ZAnsi.brightRed("Ошибка при запуске: " + e.getMessage())); System.out.println(ZAnsi.brightRed("Ошибка при запуске: " + e.getMessage()));
e.printStackTrace();
} }
ConsoleUtils.pause(); ConsoleUtils.pause();
@@ -340,9 +521,8 @@ public class LaunchMenu {
private List<String> getAllForgeVersions() throws Exception { private List<String> getAllForgeVersions() throws Exception {
String metadataUrl = "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml"; 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<String> versions = new ArrayList<>(); List<String> versions = new ArrayList<>();
int index = 0; int index = 0;
@@ -356,7 +536,6 @@ public class LaunchMenu {
index = end; index = end;
} }
// Сортируем по убыванию (новые сверху)
versions.sort((a, b) -> b.compareTo(a)); versions.sort((a, b) -> b.compareTo(a));
return versions; return versions;
@@ -1,10 +1,18 @@
package me.sashegdev.zernmc.launcher.menu; 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.ui.ArrowMenu;
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; import me.sashegdev.zernmc.launcher.utils.ConsoleUtils;
import me.sashegdev.zernmc.launcher.utils.Input;
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
public class UpdateMenu { public class UpdateMenu {
@@ -23,14 +31,122 @@ public class UpdateMenu {
ConsoleUtils.clearScreen(); ConsoleUtils.clearScreen();
if (choice == 0) { if (choice == 0) {
System.out.println("Проверка обновлений сборки..."); try {
System.out.println("Дифф обновлений пока в заглушке (сборки ещё не загружены)"); checkPackUpdates();
System.out.println(" Эндпоинт: POST /pack/{pack_name}/diff"); } catch (Exception e) {
System.out.println(ZAnsi.brightRed("Ошибка: " + e.getMessage()));
e.printStackTrace();
ConsoleUtils.pause();
}
} else { } else {
System.out.println("Проверка обновлений лаунчера..."); checkLauncherUpdates();
System.out.println("Версия лаунчера актуальна (заглушка)"); }
}
private void checkPackUpdates() throws Exception {
System.out.println(ZAnsi.cyan("Проверка обновлений сборок..."));
List<Instance> instances = InstanceManager.getAllInstances();
List<Instance> 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<Instance> 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(); 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";
}
}
} }
@@ -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;
}
}
@@ -8,7 +8,7 @@ import java.nio.file.Path;
// ЭТОТ КЛАСС РАБОТАЕТ НЕ ТРОГАТЬ ТОТ КТО БУДЕТ ЧИТАТЬ // ЭТОТ КЛАСС РАБОТАЕТ НЕ ТРОГАТЬ ТОТ КТО БУДЕТ ЧИТАТЬ (на момент 1.0.2)
public class Instance { public class Instance {
private final String name; private final String name;
private final Path path; private final Path path;
@@ -17,6 +17,9 @@ public class Instance {
private String loaderType; // vanilla, fabric, forge private String loaderType; // vanilla, fabric, forge
private String loaderVersion; private String loaderVersion;
private String assetIndex; private String assetIndex;
private boolean isServerPack; // флаг, что это сборка с сервера
private int serverVersion; // версия сборки на сервере
private String serverPackName; // имя пака на сервере
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
@@ -57,6 +60,34 @@ public class Instance {
saveMetadata(); 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 @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(name); StringBuilder sb = new StringBuilder(name);
@@ -67,6 +98,9 @@ public class Instance {
if (loaderVersion != null) sb.append(" ").append(loaderVersion); if (loaderVersion != null) sb.append(" ").append(loaderVersion);
} }
sb.append("]"); sb.append("]");
if (isServerPack) {
sb.append("v").append(serverVersion);
}
} else { } else {
sb.append(" [?]"); sb.append(" [?]");
} }
@@ -84,12 +118,18 @@ public class Instance {
this.loaderType = meta.loaderType; this.loaderType = meta.loaderType;
this.loaderVersion = meta.loaderVersion; this.loaderVersion = meta.loaderVersion;
this.assetIndex = meta.assetIndex; this.assetIndex = meta.assetIndex;
this.isServerPack = meta.isServerPack;
this.serverVersion = meta.serverVersion;
this.serverPackName = meta.serverPackName;
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
private void saveMetadata() { private void saveMetadata() {
Path metaFile = path.resolve("instance.json"); Path metaFile = path.resolve("instance.json");
InstanceMeta meta = new InstanceMeta(minecraftVersion, loaderType, loaderVersion, assetIndex); InstanceMeta meta = new InstanceMeta(
minecraftVersion, loaderType, loaderVersion, assetIndex,
isServerPack, serverVersion, serverPackName
);
try { try {
Files.writeString(metaFile, GSON.toJson(meta)); Files.writeString(metaFile, GSON.toJson(meta));
} catch (IOException e) { } catch (IOException e) {
@@ -102,13 +142,22 @@ public class Instance {
String loaderType; String loaderType;
String loaderVersion; String loaderVersion;
String assetIndex; String assetIndex;
boolean isServerPack = false;
int serverVersion = 0;
String serverPackName;
public InstanceMeta(String minecraftVersion, String loaderType, public InstanceMeta(String minecraftVersion, String loaderType,
String loaderVersion, String assetIndex) { String loaderVersion, String assetIndex,
boolean isServerPack, int serverVersion,
String serverPackName) {
this.minecraftVersion = minecraftVersion; this.minecraftVersion = minecraftVersion;
this.loaderType = loaderType; this.loaderType = loaderType;
this.loaderVersion = loaderVersion; this.loaderVersion = loaderVersion;
this.assetIndex = assetIndex; this.assetIndex = assetIndex;
this.isServerPack = isServerPack;
this.serverVersion = serverVersion;
this.serverPackName = serverPackName;
} }
} }
} }
@@ -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<ServerPack> 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<ServerPack> 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<String, String> 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<String, String> 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<String, String> scanLocalFiles() throws IOException {
Map<String, String> files = new HashMap<>();
Path instancePath = instance.getPath();
// Игнорируемые директории
Set<String> 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<String, String> 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<InputStream> 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<String, Object> 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<String, Object> getFiles() { return files; }
public boolean isEmpty() { return files == null || files.isEmpty(); }
}
public static class DiffResponse {
private int version;
private List<FileInfo> to_download;
private List<String> to_delete;
private List<String> to_update;
public int getVersion() { return version; }
public List<FileInfo> getToDownload() { return to_download != null ? to_download : new ArrayList<>(); }
public List<String> getToDelete() { return to_delete != null ? to_delete : new ArrayList<>(); }
public List<String> 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; }
}
}
@@ -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);
}
}
}
@@ -32,37 +32,56 @@ public class FabricInstaller {
// Шаг 1: Устанавливаем vanilla и получаем assetIndex // Шаг 1: Устанавливаем vanilla и получаем assetIndex
VersionInstaller versionInstaller = new VersionInstaller(instancePath); VersionInstaller versionInstaller = new VersionInstaller(instancePath);
String assetIndex = versionInstaller.install(minecraftVersion); // теперь String String assetIndex = versionInstaller.install(minecraftVersion);
if (assetIndex == null || assetIndex.isEmpty()) { // ДОПОЛНИТЕЛЬНАЯ ПРОВЕРКА: если versionInstaller.install() вернул неправильный индекс
System.out.println(ZAnsi.brightRed("Не удалось установить Minecraft " + minecraftVersion)); // (например, "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; return false;
} }
}
// Сохраняем assetIndex // Сохраняем правильный assetIndex
instance.setAssetIndex(assetIndex); instance.setAssetIndex(assetIndex);
System.out.println(ZAnsi.green("Asset index сохранён: " + assetIndex));
// Шаг 2: Скачивание и запуск Fabric Installer // Шаг 2: Скачивание Fabric Installer
String installerVersion = getLatestInstallerVersion(); String installerVersion = getLatestInstallerVersion();
String installerUrl = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/" String installerUrl = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/"
+ installerVersion + "/fabric-installer-" + installerVersion + ".jar"; + installerVersion + "/fabric-installer-" + installerVersion + ".jar";
Path installerJar = instancePath.resolve("fabric-installer.jar"); Path installerJar = instancePath.resolve("fabric-installer.jar");
if (!Files.exists(installerJar)) {
ProgressBar.show("Скачивание Fabric Installer", 0, 100, "%"); ProgressBar.show("Скачивание Fabric Installer", 0, 100, "%");
downloadFile(installerUrl, installerJar); downloadFile(installerUrl, installerJar);
ProgressBar.finish("Fabric Installer скачан"); ProgressBar.finish("Fabric Installer скачан");
} else {
System.out.println(ZAnsi.green("Fabric Installer уже скачан, пропускаем..."));
}
System.out.println(ZAnsi.cyan("Запуск Fabric Installer...")); System.out.println(ZAnsi.cyan("Запуск Fabric Installer..."));
// Используем ProcessBuilder с правильными аргументами
ProcessBuilder pb = new ProcessBuilder( ProcessBuilder pb = new ProcessBuilder(
"java", "-jar", installerJar.toAbsolutePath().toString(), "java", "-jar", installerJar.toAbsolutePath().toString(),
"client", "client",
"-dir", instancePath.toAbsolutePath().toString(), "-dir", instancePath.toAbsolutePath().toString(),
"-mcversion", minecraftVersion, "-mcversion", minecraftVersion,
"-loader", loaderVersion, "-loader", loaderVersion,
"-noprofile", "-noprofile"
"-snapshot"
); );
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT); pb.redirectError(ProcessBuilder.Redirect.INHERIT);
@@ -74,25 +93,81 @@ public class FabricInstaller {
return false; return false;
} }
// Проверка результата // Проверка результата - Fabric создаёт папку versions/fabric-loader-{loaderVersion}-{minecraftVersion}
String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion; String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion;
Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId); Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId);
// Альтернативная проверка (иногда Fabric использует другой формат)
if (!Files.exists(fabricVersionDir)) {
fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion;
fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId);
}
if (Files.exists(fabricVersionDir)) { if (Files.exists(fabricVersionDir)) {
System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!")); System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!"));
System.out.println("Версия: " + fabricVersionId); System.out.println(ZAnsi.white("Версия: ") + fabricVersionId);
System.out.println(ZAnsi.white("Asset index: ") + assetIndex);
// Сохраняем метаданные в Instance
instance.setMinecraftVersion(minecraftVersion); instance.setMinecraftVersion(minecraftVersion);
instance.setLoaderType("fabric"); instance.setLoaderType("fabric");
instance.setLoaderVersion(loaderVersion); instance.setLoaderVersion(loaderVersion);
// assetIndex уже сохранён выше, но сохраняем ещё раз для надёжности
instance.setAssetIndex(assetIndex);
// Копируем или создаём ссылку на правильный asset index в версии Fabric
ensureAssetIndexInFabricVersion(fabricVersionDir, assetIndex, minecraftVersion);
return true; return true;
} else { } else {
System.out.println(ZAnsi.brightRed("Fabric Installer отработал, но версия не найдена.")); 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; 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 { private void cleanOldFabricLoaders() throws IOException {
Path librariesDir = instance.getPath().resolve("libraries/net/fabricmc/fabric-loader"); Path librariesDir = instance.getPath().resolve("libraries/net/fabricmc/fabric-loader");
if (!Files.exists(librariesDir)) return; if (!Files.exists(librariesDir)) return;
@@ -101,12 +176,11 @@ public class FabricInstaller {
try (var stream = Files.walk(librariesDir)) { try (var stream = Files.walk(librariesDir)) {
stream.filter(Files::isDirectory) stream.filter(Files::isDirectory)
.filter(dir -> dir.getFileName().toString().startsWith("0.")) .filter(dir -> dir.getFileName().toString().matches("\\d+\\.\\d+\\.\\d+.*"))
.forEach(dir -> { .forEach(dir -> {
try { try {
// Удаляем папку версии
Files.walk(dir) Files.walk(dir)
.sorted((a,b) -> b.compareTo(a)) // удаляем файлы перед папками .sorted((a,b) -> b.compareTo(a))
.forEach(p -> { .forEach(p -> {
try { Files.deleteIfExists(p); } try { Files.deleteIfExists(p); }
catch (IOException ignored) {} catch (IOException ignored) {}
@@ -126,14 +200,31 @@ public class FabricInstaller {
} }
private String downloadString(String url) throws Exception { 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<String> resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> 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(); return resp.body();
} }
private void downloadFile(String url, Path target) throws Exception { private void downloadFile(String url, Path target) throws Exception {
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)).GET().build(); HttpRequest request = HttpRequest.newBuilder()
httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target)); .uri(URI.create(url))
.timeout(Duration.ofSeconds(60))
.GET()
.build();
HttpResponse<Path> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofFile(target));
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode() + " при скачивании " + url);
}
} }
} }
@@ -56,7 +56,7 @@ public class VersionInstaller {
return versions; 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 + "...")); System.out.println(ZAnsi.cyan("Полная установка Minecraft " + versionId + "..."));
Path versionDir = minecraftDir.resolve("versions").resolve(versionId); Path versionDir = minecraftDir.resolve("versions").resolve(versionId);
Files.createDirectories(versionDir); Files.createDirectories(versionDir);
@@ -77,17 +77,20 @@ public class VersionInstaller {
System.out.println(ZAnsi.cyan("Скачивание библиотек...")); System.out.println(ZAnsi.cyan("Скачивание библиотек..."));
downloadLibraries(versionData.getJSONArray("libraries")); 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")) { if (versionData.has("assetIndex")) {
System.out.println(ZAnsi.cyan("Скачивание ассетов...")); System.out.println(ZAnsi.cyan("Скачивание ассетов..."));
downloadAssets(versionData); downloadAssets(versionData);
System.out.println(ZAnsi.brightGreen("Asset index определён как: " + assetIndex)); System.out.println(ZAnsi.brightGreen("Asset index определён как: " + assetIndex));
} else {
System.out.println(ZAnsi.yellow("Нет assetIndex в версии, использую fallback: " + assetIndex));
} }
System.out.println(ZAnsi.brightGreen("\nMinecraft " + versionId + " полностью установлен!")); System.out.println(ZAnsi.brightGreen("\nMinecraft " + versionId + " полностью установлен!"));
return assetIndex; // возвращаем настоящий assetIndex (например "30") return assetIndex; // Возвращаем правильный индекс (например "5")
} }
private void downloadLibraries(JSONArray libraries) throws Exception { 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 { private String getVersionUrl(String versionId) throws Exception {
for (MinecraftVersion v : getAvailableVersions()) { for (MinecraftVersion v : getAvailableVersions()) {
if (v.getId().equals(versionId)) return v.getUrl(); if (v.getId().equals(versionId)) return v.getUrl();
+1
View File
@@ -104,6 +104,7 @@ def setup_logging():
} }
), ),
_add_location, _add_location,
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
], ],
logger_factory=structlog.stdlib.LoggerFactory(), logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger, wrapper_class=structlog.stdlib.BoundLogger,
+221 -95
View File
@@ -1,5 +1,3 @@
import os
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse, JSONResponse from fastapi.responses import FileResponse, JSONResponse
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
@@ -7,34 +5,32 @@ from pathlib import Path
import json import json
import structlog import structlog
from cachetools import TTLCache from cachetools import TTLCache
import asyncio
import logging import logging
from datetime import datetime from datetime import datetime
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
# Import local modules # 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 models import PackMeta
from logging_config import setup_logging
from middleware import LoggingMiddleware from middleware import LoggingMiddleware
from cli import parse_args, run_test_mode, run_production_mode, run_development_mode from cli import parse_args, run_test_mode, run_production_mode, run_development_mode
from log_manager import init_logging, get_logger from log_manager import init_logging
from models import MinecraftVersion
from pack_manager import get_minecraft_versions, download_minecraft_version
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
# Cache for manifests - expires after 5 minutes # Cache for manifests - expires after 5 minutes
manifest_cache = TTLCache(maxsize=100, ttl=300) manifest_cache = TTLCache(maxsize=100, ttl=300)
BUILDS_DIR = Path("builds")
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
args = parse_args() args = parse_args()
# Initialize logging # Initialize logging
init_logging() init_logging()
logger = logging.getLogger(__name__) #logger = logging.getLogger(__name__)
# Determine environment # Determine environment
if args.test: if args.test:
@@ -46,6 +42,11 @@ async def lifespan(app: FastAPI):
logger.info(f"Starting ZernMC Launcher Server (environment: {env})") 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: if args.test:
await run_test_mode() await run_test_mode()
yield yield
@@ -69,6 +70,7 @@ async def lifespan(app: FastAPI):
yield yield
logger.info("Server shutting down...") logger.info("Server shutting down...")
# Create app with lifespan # Create app with lifespan
app = FastAPI(title="ZernMC Launcher Server", lifespan=lifespan) app = FastAPI(title="ZernMC Launcher Server", lifespan=lifespan)
@@ -85,17 +87,35 @@ def patched_data_received(self, data):
except Exception as e: except Exception as e:
client = self.transport.get_extra_info('peername') client = self.transport.get_extra_info('peername')
logger = logging.getLogger(__name__) 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: try:
raw_data = data[:500].decode('utf-8', errors='replace') 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: except:
pass 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 HttpToolsProtocol.data_received = patched_data_received
# ====================== ОСНОВНЫЕ ЭНДПОИНТЫ ======================
@app.get("/") @app.get("/")
async def root(): async def root():
"""Root endpoint""" """Root endpoint"""
@@ -108,12 +128,15 @@ async def root():
"redoc": "/redoc" "redoc": "/redoc"
} }
@app.get("/health") @app.get("/health")
async def health(): async def health():
"""Health check endpoint""" """Health check endpoint"""
return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()} return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
# ====================== ЭНДПОИНТЫ ДЛЯ ПАКОВ ======================
@app.get("/packs") @app.get("/packs")
async def list_packs(): async def list_packs():
"""List all available packs""" """List all available packs"""
@@ -125,13 +148,21 @@ async def list_packs():
meta_path = DATA_DIR / f"{pack_dir.name}.meta" meta_path = DATA_DIR / f"{pack_dir.name}.meta"
if meta_path.exists(): if meta_path.exists():
try: 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) 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({ packs.append({
"name": pack_dir.name, "name": pack_dir.name,
"version": meta.get("version", 1), "version": meta.get("version", 1),
"files_count": len(meta.get("files", {})), "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: except Exception as e:
logger.error(f"Failed to load pack meta for {pack_dir.name}: {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} return {"packs": packs}
# ------------------- DIFF ENDPOINT -------------------
@app.post("/pack/{pack_name}/diff") @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", ... } Client sends: { "mods/jei.jar": "sha256_hash", ... }
Server returns diff information Server returns diff information
""" """
client_ip = request.client.host if request.client else "unknown" 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", logger.info("Received diff request",
pack=pack_name, pack=pack_name,
client_files_count=len(client_files), client_files_count=len(body),
client_ip=client_ip) client_ip=client_ip)
try: try:
# Use cached manifest if available
meta = get_cached_manifest(pack_name) meta = get_cached_manifest(pack_name)
if not meta: if not meta:
meta = await scan_pack(pack_name) 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") raise HTTPException(404, "Pack not found")
except Exception as e: except Exception as e:
logger.error("Error loading pack meta", pack=pack_name, error=str(e), exc_info=True) 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_download = []
to_delete = [] 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 server_files = meta.files
# Calculate what needs to be downloaded or updated
for path, entry in server_files.items(): 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: if client_hash is None or client_hash != entry.hash:
url = f"/pack/{pack_name}/file/{path}" url = f"/pack/{pack_name}/file/{path}"
to_download.append({ 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: if client_hash is not None:
to_update.append(path) to_update.append(path)
# Calculate what needs to be deleted for path in body:
for path in client_files:
if path not in server_files: if path not in server_files:
to_delete.append(path) 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 "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}") @app.get("/pack/{pack_name}")
async def get_pack_manifest(pack_name: str, request: Request): async def get_pack_manifest(pack_name: str, request: Request):
"""Get pack manifest with caching""" """Get pack manifest with caching"""
client_ip = request.client.host if request.client else "unknown" client_ip = request.client.host if request.client else "unknown"
# Check cache first
cached_meta = get_cached_manifest(pack_name) cached_meta = get_cached_manifest(pack_name)
if cached_meta: if cached_meta:
logger.debug("Manifest served from cache", logger.debug("Manifest served from cache",
pack=pack_name, pack=pack_name,
version=cached_meta.version, version=cached_meta.version,
client_ip=client_ip) client_ip=client_ip)
# Исправлено: конвертируем datetime в строку при сериализации
return JSONResponse( 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"} 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" meta_path = Path("data") / f"{pack_name}.meta"
if not meta_path.exists(): if not meta_path.exists():
logger.warning("Manifest requested but pack not found", pack=pack_name, client_ip=client_ip) logger.warning("Manifest requested but pack not found", pack=pack_name, client_ip=client_ip)
raise HTTPException(404, "Pack not found") 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) meta_dict = json.load(f)
# Исправлено: используем model_validate для создания объекта
meta = PackMeta.model_validate(meta_dict) meta = PackMeta.model_validate(meta_dict)
# Update cache
manifest_cache[pack_name] = meta manifest_cache[pack_name] = meta
logger.debug("Manifest served from disk", logger.debug("Manifest served from disk",
@@ -290,16 +283,23 @@ async def get_pack_manifest(pack_name: str, request: Request):
version=meta.version, version=meta.version,
client_ip=client_ip) client_ip=client_ip)
# Исправлено: конвертируем datetime в строку при сериализации
return JSONResponse( return JSONResponse(
content=meta_dict, content=meta.model_dump(mode='json'),
headers={"X-Pack-Version": str(meta.version), "X-Cached": "false"} headers={"X-Pack-Version": str(meta.version), "X-Cached": "false"}
) )
@app.get("/pack/{pack_name}/file/{file_path:path}") @app.get("/pack/{pack_name}/file/{file_path:path}")
async def get_pack_file(pack_name: str, file_path: str, request: Request): 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 full_path = PACKS_DIR / pack_name / file_path
client_ip = request.client.host if request.client else None 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(): if not full_path.exists() or not full_path.is_file():
logger.warning("File not found", logger.warning("File not found",
pack=pack_name, pack=pack_name,
@@ -316,51 +316,177 @@ async def get_pack_file(pack_name: str, file_path: str, request: Request):
return FileResponse(full_path) 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") @app.get("/launcher/version")
async def get_launcher_version(): async def get_launcher_version():
"""Возвращает информацию о текущей версии лаунчера""" """Return launcher version information"""
version_file = Path("builds/build.version") version = get_current_launcher_version()
zips = get_available_zips()
version = "1.0.0" response = {
if version_file.exists():
version = version_file.read_text().strip()
return {
"version": version, "version": version,
"download_jar": "/launcher/download?type=jar",
"download_exe": "/launcher/download?type=exe",
"updated_at": datetime.utcnow().isoformat() "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
@app.get("/launcher/download") exe_path = BUILDS_DIR / "ZernMCLauncher.exe"
async def download_launcher(type: str = "exe"): if exe_path.exists():
"""Отдаёт файл лаунчера""" response["download_exe"] = "/launcher/download/exe"
if type == "exe": response["exe_size"] = exe_path.stat().st_size
file_path = Path("builds/ZernMCLauncher.exe")
filename = "ZernMCLauncher.exe" if zips:
else: response["download_zip"] = f"/launcher/download/zip/{zips[0]['filename']}"
file_path = Path("builds/ZernMCLauncher.jar") response["zip_version"] = zips[0]["version"]
filename = "ZernMCLauncher.jar" response["zip_size"] = zips[0]["size"]
response["all_zips"] = zips
return response
@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(): 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( return FileResponse(
path=file_path, path=file_path,
filename=filename, 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__": if __name__ == "__main__":
args = parse_args() args = parse_args()
if args.test: if args.test:
# Test mode runs within lifespan
import asyncio import asyncio
asyncio.run(run_test_mode()) asyncio.run(run_test_mode())
elif args.dev: elif args.dev:
run_development_mode(args.host, args.port, args.reload) run_development_mode(args.host, args.port, args.reload)
else: else:
# Default to production
run_production_mode(args.host, args.port, args.workers) run_production_mode(args.host, args.port, args.workers)
+43 -80
View File
@@ -3,25 +3,17 @@ import os
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
import json import json
import aiofiles
from typing import Optional, Dict from typing import Optional, Dict
import structlog import structlog
from models import PackMeta, FileEntry from models import PackMeta, FileEntry
import aiohttp
from typing import List, Optional
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
PACKS_DIR = Path("packs") PACKS_DIR = Path("packs")
DATA_DIR = Path("data") DATA_DIR = Path("data")
DATA_DIR.mkdir(exist_ok=True) 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 # Cache for loaded manifests
_manifest_cache: Dict[str, PackMeta] = {} _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: def get_meta_path(pack_name: str) -> Path:
return DATA_DIR / f"{pack_name}.meta" 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() hash_sha = hashlib.sha256()
async with aiofiles.open(file_path, 'rb') as f: with open(file_path, 'rb') as f:
while chunk := await f.read(8192): while chunk := f.read(8192):
hash_sha.update(chunk) hash_sha.update(chunk)
return hash_sha.hexdigest() return hash_sha.hexdigest()
async def get_minecraft_versions(): async def calculate_sha256(file_path: Path) -> str:
"""Fetch available Minecraft versions from Mojang""" """Calculate SHA256 hash of a file (async wrapper)"""
from models import MinecraftVersion # Используем синхронную версию для простоты
async with aiohttp.ClientSession() as session: return calculate_sha256_sync(file_path)
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 scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta: async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
"""Scan pack directory and update manifest if needed""" """Scan pack directory and update manifest if needed"""
pack_path = PACKS_DIR / pack_name pack_path = PACKS_DIR / pack_name
if not pack_path.exists() or not pack_path.is_dir(): if not pack_path.exists() or not pack_path.is_dir():
raise FileNotFoundError(f"Pack {pack_name} not found") raise FileNotFoundError(f"Pack {pack_name} not found")
meta_path = get_meta_path(pack_name) meta_path = get_meta_path(pack_name)
current_meta: Optional[PackMeta] = None current_meta: Optional[PackMeta] = None
# Check cache first
if not force_rescan and pack_name in _manifest_cache: if not force_rescan and pack_name in _manifest_cache:
return _manifest_cache[pack_name] return _manifest_cache[pack_name]
# Load existing meta if available (синхронно)
if meta_path.exists(): 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) current_meta = PackMeta.model_validate(data)
except Exception as e: except Exception as e:
logger.warning(f"Failed to validate existing meta for pack {pack_name}", error=str(e)) logger.warning(f"Failed to load existing meta for pack {pack_name}: {e}")
current_meta = None current_meta = None
new_files: Dict[str, FileEntry] = {} new_files: Dict[str, FileEntry] = {}
changed = False 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): for root, dirs, files in os.walk(pack_path):
ignored = current_meta.ignored_dirs if current_meta else [] # Filter ignored directories
dirs[:] = [d for d in dirs if d not in ignored] dirs[:] = [d for d in dirs if d not in ignored_dirs]
for file in files: for file in files:
file_path = Path(root) / file file_path = Path(root) / file
rel_path = file_path.relative_to(pack_path).as_posix() 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 continue
stat = file_path.stat() 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 new_files[rel_path] = entry
# Check if file changed
if current_meta and (rel_path not in current_meta.files or if current_meta and (rel_path not in current_meta.files or
current_meta.files[rel_path].hash != file_hash): current_meta.files[rel_path].hash != file_hash):
changed = True 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): 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 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" minecraft_version = "1.20.4"
loader_type = "vanilla" loader_type = "vanilla"
loader_version = None loader_version = None
pack_config_path = pack_path / "instance.json"
if pack_config_path.exists(): if pack_config_path.exists():
try: 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) minecraft_version = config.get("minecraftVersion", minecraft_version)
loader_type = config.get("loaderType", loader_type) loader_type = config.get("loaderType", loader_type)
loader_version = config.get("loaderVersion") loader_version = config.get("loaderVersion")
except Exception as e: 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( new_meta = PackMeta(
pack_name=pack_name, pack_name=pack_name,
version=version, version=version,
files=new_files, files=new_files,
updated_at=datetime.utcnow(), updated_at=datetime.utcnow(),
ignored_dirs=current_meta.ignored_dirs if current_meta else [], ignored_dirs=ignored_dirs,
minecraft_version=minecraft_version, minecraft_version=minecraft_version,
loader_type=loader_type, loader_type=loader_type,
loader_version=loader_version loader_version=loader_version
) )
async with aiofiles.open(meta_path, 'w', encoding='utf-8') as f: # Save to disk (синхронно)
await f.write(new_meta.model_dump_json(indent=2)) 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 _manifest_cache[pack_name] = new_meta
logger.info(f"Pack updated: {pack_name} v{version}, {len(new_files)} files") logger.info(f"Pack updated: {pack_name} v{version}, {len(new_files)} files")
return new_meta return new_meta
# No changes, use existing
if current_meta: if current_meta:
_manifest_cache[pack_name] = current_meta _manifest_cache[pack_name] = current_meta
return current_meta return current_meta
# Should not happen
raise Exception(f"Failed to scan pack {pack_name}")