Both | БЛЯЯЯ ЗАГРУЗКА ПАКОВ С СЕРВЕРА СЮДААА
This commit is contained in:
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+107
-16
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+17
-4
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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}")
|
||||||
Reference in New Issue
Block a user