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