Refactor: Multi-module Maven project structure
- Restructured to multi-module Maven project (bootstrap + launcher) - Removed duplicate code (launcher/launcher/ with JCEF) - Added JavaFX modules to lib/javafx in ZIP - Added JRE 21 to lib/jre21 in ZIP - Fixed Bootstrap with UTF-8 encoding and JavaFX module-path - Fixed JAR naming (zernmclauncher.jar) - Added Windows build configuration (ZernMC-win-*.zip) - Fixed version parsing for -any, -alpha, -beta suffixes
This commit is contained in:
+2
-1
@@ -2,6 +2,8 @@ logs/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
./.venv/
|
./.venv/
|
||||||
launcher/target
|
launcher/target
|
||||||
|
bootstrap/target
|
||||||
|
src/target
|
||||||
server/builds
|
server/builds
|
||||||
server/packs
|
server/packs
|
||||||
server/data
|
server/data
|
||||||
@@ -10,4 +12,3 @@ jre
|
|||||||
dependency-reduced-pom.xml
|
dependency-reduced-pom.xml
|
||||||
OpenJDK21U-jre_x64_windows_hotspot_21.0.6_7.zip
|
OpenJDK21U-jre_x64_windows_hotspot_21.0.6_7.zip
|
||||||
telegram-bot/
|
telegram-bot/
|
||||||
.env
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,22 +0,0 @@
|
|||||||
public class Test {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
String line = "{\"version\":\"1.0.8\",\"updated_at\":\"2026-05-06T04:38:07\"}";
|
|
||||||
System.out.println("Line: " + line);
|
|
||||||
|
|
||||||
int start = line.indexOf("\"version\":\"");
|
|
||||||
System.out.println("start (version): " + start);
|
|
||||||
|
|
||||||
if (start >= 0) {
|
|
||||||
start += 10;
|
|
||||||
System.out.println("start + 10: " + start);
|
|
||||||
|
|
||||||
int end = line.indexOf("\"", start + 1);
|
|
||||||
System.out.println("end: " + end);
|
|
||||||
|
|
||||||
if (end > start) {
|
|
||||||
String result = line.substring(start, end);
|
|
||||||
System.out.println("Result: [" + result + "]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,31 +0,0 @@
|
|||||||
public class Test2 {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
String[] tests = {
|
|
||||||
"{\"version\":\"1.0.8\",\"updated_at\":\"2026-05-06T04:38:07\"}",
|
|
||||||
"{\"version\":\"1.0.2\"}",
|
|
||||||
"invalid json"
|
|
||||||
};
|
|
||||||
|
|
||||||
for (String line : tests) {
|
|
||||||
String result = getVersion(line);
|
|
||||||
System.out.println("Input: " + line);
|
|
||||||
System.out.println("Output: [" + result + "]");
|
|
||||||
System.out.println("Length: " + result.length());
|
|
||||||
System.out.println();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static String getVersion(String line) {
|
|
||||||
if (line != null && line.contains("version")) {
|
|
||||||
int start = line.indexOf("\"version\":\"");
|
|
||||||
if (start >= 0) {
|
|
||||||
start += 10;
|
|
||||||
int end = line.indexOf("\"", start + 1);
|
|
||||||
if (end > start) {
|
|
||||||
return line.substring(start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-51
@@ -2,7 +2,6 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
@@ -13,24 +12,24 @@
|
|||||||
|
|
||||||
<artifactId>zernmc-bootstrap</artifactId>
|
<artifactId>zernmc-bootstrap</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>ZernMC Bootstrap</name>
|
<name>ZernMC Bootstrap</name>
|
||||||
<description>ZernMC Launcher - Bootstrap (auto-updater)</description>
|
<description>Bootstrap module - handles updates and Java launching</description>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- Minimal dependencies for Bootstrap -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>com.google.code.gson</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>gson</artifactId>
|
||||||
<version>5.10.0</version>
|
</dependency>
|
||||||
<scope>test</scope>
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
@@ -42,56 +41,16 @@
|
|||||||
<goal>shade</goal>
|
<goal>shade</goal>
|
||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<outputFile>../../server/builds/zernmc/zernmc.jar</outputFile>
|
<outputFile>../../server/builds/zernmc-bootstrap.jar</outputFile>
|
||||||
<transformers>
|
<transformers>
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
<mainClass>me.sashegdev.zernmc.launcher.Bootstrap</mainClass>
|
<mainClass>me.sashegdev.zernmc.launcher.Bootstrap</mainClass>
|
||||||
<manifestEntries>
|
|
||||||
<Implementation-Version>1.0.8</Implementation-Version>
|
|
||||||
<Implementation-Title>ZernMC Bootstrap</Implementation-Title>
|
|
||||||
<Implementation-Vendor>ZernMC</Implementation-Vendor>
|
|
||||||
</manifestEntries>
|
|
||||||
</transformer>
|
</transformer>
|
||||||
</transformers>
|
</transformers>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
|
||||||
<groupId>com.akathist.maven.plugins.launch4j</groupId>
|
|
||||||
<artifactId>launch4j-maven-plugin</artifactId>
|
|
||||||
<version>2.5.0</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>l4j</id>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>launch4j</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<outfile>../../server/builds/zernmc/zernmc.exe</outfile>
|
|
||||||
<jar>../../server/builds/zernmc/zernmc.jar</jar>
|
|
||||||
<headerType>gui</headerType>
|
|
||||||
<dontWrapJar>true</dontWrapJar>
|
|
||||||
<jre>
|
|
||||||
<path>lib/jre21-custom</path>
|
|
||||||
<minVersion>21</minVersion>
|
|
||||||
</jre>
|
|
||||||
<versionInfo>
|
|
||||||
<fileVersion>1.0.8.0</fileVersion>
|
|
||||||
<txtFileVersion>1.0.8</txtFileVersion>
|
|
||||||
<fileDescription>ZernMC Launcher Bootstrap</fileDescription>
|
|
||||||
<productVersion>1.0.8.0</productVersion>
|
|
||||||
<txtProductVersion>1.0.8</txtProductVersion>
|
|
||||||
<productName>ZernMC Launcher</productName>
|
|
||||||
<companyName>ZernMC</companyName>
|
|
||||||
<internalName>zernmc</internalName>
|
|
||||||
<originalFilename>zernmc.exe</originalFilename>
|
|
||||||
</versionInfo>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
@@ -6,13 +6,14 @@ import java.net.URL;
|
|||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.regex.*;
|
import java.util.List;
|
||||||
|
|
||||||
public class Bootstrap {
|
public class Bootstrap {
|
||||||
private static final String VERSION_FILE = "build.version";
|
private static final String VERSION_FILE = "build.version";
|
||||||
private static final String JAR_NAME = "bin/ZernMCLauncher.jar";
|
private static final String JAR_NAME = "zernmclauncher.jar";
|
||||||
private static final String BASE_URL = "http://87.120.187.36:1582/launcher/download?type=jar";
|
private static final String BASE_URL = "http://87.120.187.36:1582";
|
||||||
|
|
||||||
private static Path baseDir;
|
private static Path baseDir;
|
||||||
private static Path logDir;
|
private static Path logDir;
|
||||||
@@ -24,25 +25,31 @@ public class Bootstrap {
|
|||||||
|
|
||||||
log("=== ZernMC Launcher ===");
|
log("=== ZernMC Launcher ===");
|
||||||
|
|
||||||
if (args.length > 0 && args[0].equals("--launcher")) {
|
// Определяем режим запуска
|
||||||
launchUI();
|
List<String> argList = Arrays.asList(args);
|
||||||
return;
|
boolean cliMode = argList.contains("--cli");
|
||||||
}
|
boolean jfxMode = !cliMode; // по умолчанию JFX
|
||||||
|
|
||||||
|
// Проверка и обновление лаунчера
|
||||||
String currentVersion = readCurrentVersion();
|
String currentVersion = readCurrentVersion();
|
||||||
String serverVersion = getServerVersion();
|
String serverVersion = getServerVersion();
|
||||||
|
|
||||||
log("Локальная: " + currentVersion);
|
log("Локальная версия: " + currentVersion);
|
||||||
log("Сервер: " + serverVersion);
|
log("Версия на сервере: " + serverVersion);
|
||||||
|
|
||||||
if (isNewer(serverVersion, currentVersion)) {
|
if (isNewer(serverVersion, currentVersion)) {
|
||||||
log("Доступно обновление!");
|
log("Доступно обновление!");
|
||||||
downloadUpdate();
|
downloadUpdate(serverVersion);
|
||||||
} else {
|
} else {
|
||||||
log("Актуально");
|
log("Версия актуальна");
|
||||||
}
|
}
|
||||||
|
|
||||||
launchGame();
|
// Запуск в выбранном режиме
|
||||||
|
if (jfxMode) {
|
||||||
|
launchJFX();
|
||||||
|
} else {
|
||||||
|
launchCLI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void log(String msg) {
|
private static void log(String msg) {
|
||||||
@@ -71,14 +78,7 @@ public class Bootstrap {
|
|||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
|
||||||
String line = br.readLine();
|
String line = br.readLine();
|
||||||
if (line != null && line.contains("version")) {
|
if (line != null && line.contains("version")) {
|
||||||
int start = line.indexOf("\"version\":\"");
|
return line.replaceAll(".*\"version\"\\s*:\\s*\"([^\"]+)\".*", "$1");
|
||||||
if (start >= 0) {
|
|
||||||
start += 11;
|
|
||||||
int end = line.indexOf("\"", start);
|
|
||||||
if (end > start) {
|
|
||||||
return line.substring(start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,13 +101,15 @@ public class Bootstrap {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void downloadUpdate() throws Exception {
|
private static void downloadUpdate(String newVersion) throws Exception {
|
||||||
URL url = new URL(BASE_URL);
|
URL url = new URL(BASE_URL + "/launcher/download/jar");
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
conn.setRequestMethod("GET");
|
conn.setRequestMethod("GET");
|
||||||
|
|
||||||
if (conn.getResponseCode() == 200) {
|
if (conn.getResponseCode() == 200) {
|
||||||
Path tmp = baseDir.resolve(JAR_NAME + ".new");
|
Path jarFile = baseDir.resolve(JAR_NAME);
|
||||||
|
Path tmp = jarFile.resolveSibling("zernmc-launcher-new.jar");
|
||||||
|
|
||||||
try (InputStream in = conn.getInputStream();
|
try (InputStream in = conn.getInputStream();
|
||||||
OutputStream out = new FileOutputStream(tmp.toFile())) {
|
OutputStream out = new FileOutputStream(tmp.toFile())) {
|
||||||
byte[] buf = new byte[8192];
|
byte[] buf = new byte[8192];
|
||||||
@@ -121,84 +123,124 @@ public class Bootstrap {
|
|||||||
}
|
}
|
||||||
log("Скачано");
|
log("Скачано");
|
||||||
|
|
||||||
Path jarFile = baseDir.resolve(JAR_NAME);
|
Path backup = jarFile.resolveSibling(JAR_NAME + ".old");
|
||||||
Path backup = baseDir.resolve(JAR_NAME + ".old");
|
|
||||||
|
|
||||||
if (Files.exists(jarFile)) Files.move(jarFile, backup);
|
if (Files.exists(jarFile)) Files.move(jarFile, backup, StandardCopyOption.REPLACE_EXISTING);
|
||||||
Files.move(tmp, jarFile);
|
Files.move(tmp, jarFile, StandardCopyOption.REPLACE_EXISTING);
|
||||||
if (Files.exists(backup)) Files.delete(backup);
|
if (Files.exists(backup)) Files.delete(backup);
|
||||||
|
|
||||||
String newVersion = getServerVersion();
|
|
||||||
Files.writeString(baseDir.resolve(VERSION_FILE), newVersion);
|
Files.writeString(baseDir.resolve(VERSION_FILE), newVersion);
|
||||||
log("Обновлено до v" + newVersion);
|
log("Обновлено до v" + newVersion);
|
||||||
|
} else {
|
||||||
|
throw new IOException("Сервер вернул код: " + conn.getResponseCode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void launchUI() throws Exception {
|
private static void launchJFX() throws Exception {
|
||||||
// Запускаем JAR файл с аргументом --cli
|
Path javaBin = findJava();
|
||||||
Path javaBin = baseDir.resolve("lib").resolve("jre21-custom").resolve("bin").resolve("java");
|
|
||||||
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
|
||||||
javaBin = javaBin.resolveSibling("java.exe");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Files.exists(javaBin)) {
|
|
||||||
javaBin = Paths.get(System.getProperty("java.home"), "bin", "java");
|
|
||||||
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
|
||||||
javaBin = javaBin.resolveSibling("java.exe");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Path jarPath = baseDir.resolve(JAR_NAME);
|
Path jarPath = baseDir.resolve(JAR_NAME);
|
||||||
|
|
||||||
ProcessBuilder pb = new ProcessBuilder(
|
log("Запуск JFX режима...");
|
||||||
javaBin.toAbsolutePath().toString(),
|
|
||||||
"-jar",
|
|
||||||
jarPath.toAbsolutePath().toString(),
|
|
||||||
"--cli"
|
|
||||||
);
|
|
||||||
pb.directory(baseDir.toFile());
|
|
||||||
pb.inheritIO();
|
|
||||||
Process p = pb.start();
|
|
||||||
int code = p.waitFor();
|
|
||||||
System.exit(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void launchGame() throws Exception {
|
|
||||||
Path javaBin = baseDir.resolve("lib").resolve("jre21-custom").resolve("bin").resolve("java");
|
|
||||||
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
|
||||||
javaBin = javaBin.resolveSibling("java.exe");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Files.exists(javaBin)) {
|
|
||||||
javaBin = baseDir.resolve("bin").resolve("java");
|
|
||||||
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
|
||||||
javaBin = javaBin.resolveSibling("java.exe");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Files.exists(javaBin)) {
|
|
||||||
javaBin = Paths.get(System.getProperty("java.home"), "bin", "java");
|
|
||||||
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
|
||||||
javaBin = javaBin.resolveSibling("java.exe");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Java: " + javaBin);
|
log("Java: " + javaBin);
|
||||||
log("Запуск...");
|
log("JAR: " + jarPath);
|
||||||
|
|
||||||
Path jarPath = baseDir.resolve(JAR_NAME);
|
// JVM аргументы для UTF-8 и JavaFX
|
||||||
|
List<String> jvmArgs = List.of(
|
||||||
ProcessBuilder pb = new ProcessBuilder(
|
"-Dfile.encoding=UTF-8",
|
||||||
javaBin.toAbsolutePath().toString(),
|
"-Dsun.stdout.encoding=UTF-8",
|
||||||
"-jar",
|
"-Dsun.stderr.encoding=UTF-8"
|
||||||
jarPath.toAbsolutePath().toString(),
|
|
||||||
"--launcher"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Путь к JavaFX модулям
|
||||||
|
Path javafxPath = baseDir.resolve("lib").resolve("javafx");
|
||||||
|
if (Files.exists(javafxPath)) {
|
||||||
|
jvmArgs = List.of(
|
||||||
|
"-Dfile.encoding=UTF-8",
|
||||||
|
"-Dsun.stdout.encoding=UTF-8",
|
||||||
|
"-Dsun.stderr.encoding=UTF-8",
|
||||||
|
"--module-path", javafxPath.toAbsolutePath().toString(),
|
||||||
|
"--add-modules", "javafx.controls,javafx.web"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> cmd = new ArrayList<>();
|
||||||
|
cmd.add(javaBin.toAbsolutePath().toString());
|
||||||
|
cmd.addAll(jvmArgs);
|
||||||
|
cmd.add("-jar");
|
||||||
|
cmd.add(jarPath.toAbsolutePath().toString());
|
||||||
|
cmd.add("--jfx");
|
||||||
|
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(cmd);
|
||||||
pb.directory(baseDir.toFile());
|
pb.directory(baseDir.toFile());
|
||||||
pb.inheritIO();
|
pb.inheritIO();
|
||||||
Process p = pb.start();
|
Process p = pb.start();
|
||||||
int code = p.waitFor();
|
int code = p.waitFor();
|
||||||
log("Завершено: " + code);
|
log("Завершено с кодом: " + code);
|
||||||
System.exit(code);
|
System.exit(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void launchCLI() throws Exception {
|
||||||
|
Path javaBin = findJava();
|
||||||
|
Path jarPath = baseDir.resolve(JAR_NAME);
|
||||||
|
|
||||||
|
log("Запуск CLI режима...");
|
||||||
|
log("Java: " + javaBin);
|
||||||
|
log("JAR: " + jarPath);
|
||||||
|
|
||||||
|
// JVM аргументы для UTF-8
|
||||||
|
List<String> jvmArgs = List.of(
|
||||||
|
"-Dfile.encoding=UTF-8",
|
||||||
|
"-Dsun.stdout.encoding=UTF-8",
|
||||||
|
"-Dsun.stderr.encoding=UTF-8"
|
||||||
|
);
|
||||||
|
|
||||||
|
List<String> cmd = new ArrayList<>();
|
||||||
|
cmd.add(javaBin.toAbsolutePath().toString());
|
||||||
|
cmd.addAll(jvmArgs);
|
||||||
|
cmd.add("-jar");
|
||||||
|
cmd.add(jarPath.toAbsolutePath().toString());
|
||||||
|
cmd.add("--cli");
|
||||||
|
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(cmd);
|
||||||
|
pb.directory(baseDir.toFile());
|
||||||
|
pb.inheritIO();
|
||||||
|
Process p = pb.start();
|
||||||
|
int code = p.waitFor();
|
||||||
|
log("Завершено с кодом: " + code);
|
||||||
|
System.exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path findJava() {
|
||||||
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
String javaExe = os.contains("windows") ? "java.exe" : "java";
|
||||||
|
|
||||||
|
// Сначала ищем jre21/bin/java рядом с лаунчером
|
||||||
|
Path javaBin = baseDir.resolve("jre21").resolve("bin").resolve(javaExe);
|
||||||
|
|
||||||
|
// Если нет, пробуем системную Java
|
||||||
|
if (!Files.exists(javaBin)) {
|
||||||
|
javaBin = Paths.get(System.getProperty("java.home"), "bin", javaExe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если и это не найдено - ищем java в PATH
|
||||||
|
if (!Files.exists(javaBin)) {
|
||||||
|
try {
|
||||||
|
Process p = new ProcessBuilder("which", javaExe).start();
|
||||||
|
if (p.waitFor() == 0) {
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
|
||||||
|
String path = br.readLine();
|
||||||
|
if (path != null) {
|
||||||
|
javaBin = Paths.get(path.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.exists(javaBin)) {
|
||||||
|
throw new RuntimeException("Java не найдена. Убедитесь, что jre21 присутствует в папке с лаунчером или Java установлена в системе");
|
||||||
|
}
|
||||||
|
|
||||||
|
return javaBin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
-68
@@ -1,68 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
public class BootstrapVersionTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testVersionParsing() {
|
|
||||||
assertEquals("1.0.8", getVersion("{\"version\":\"1.0.8\",\"updated_at\":\"2026-05-06T04:38:07\"}"));
|
|
||||||
assertEquals("1.0.2", getVersion("{\"version\":\"1.0.2\"}"));
|
|
||||||
assertEquals("2.0.0", getVersion("{\"version\":\"2.0.0\",\"download_jar\":\"/launcher/download/jar\"}"));
|
|
||||||
assertEquals("unknown", getVersion("invalid json"));
|
|
||||||
assertEquals("unknown", getVersion("{\"ver\":\"1.0.8\"}"));
|
|
||||||
assertEquals("unknown", getVersion(""));
|
|
||||||
assertEquals("unknown", getVersion(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testVersionComparison() {
|
|
||||||
assertTrue(isNewer("1.0.8", "1.0.7"));
|
|
||||||
assertTrue(isNewer("1.0.8", "1.0.2"));
|
|
||||||
assertTrue(isNewer("2.0.0", "1.0.8"));
|
|
||||||
assertFalse(isNewer("1.0.8", "1.0.8"));
|
|
||||||
assertFalse(isNewer("1.0.7", "1.0.8"));
|
|
||||||
assertTrue(isNewer("1.0.10", "1.0.9"));
|
|
||||||
assertTrue(isNewer("1.0.9", "1.0.8"));
|
|
||||||
assertFalse(isNewer("unknown", "1.0.8"));
|
|
||||||
assertFalse(isNewer("1.0.8", "unknown"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testEdgeCases() {
|
|
||||||
assertEquals("unknown", getVersion(null));
|
|
||||||
assertEquals("unknown", getVersion(""));
|
|
||||||
assertFalse(isNewer("unknown", "1.0.8"));
|
|
||||||
assertFalse(isNewer("1.0.8", "unknown"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getVersion(String line) {
|
|
||||||
if (line != null && line.contains("version")) {
|
|
||||||
int start = line.indexOf("\"version\":\"");
|
|
||||||
if (start >= 0) {
|
|
||||||
start += 11;
|
|
||||||
int end = line.indexOf("\"", start);
|
|
||||||
if (end > start) {
|
|
||||||
return line.substring(start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isNewer(String server, String current) {
|
|
||||||
try {
|
|
||||||
String[] sa = server.split("\\.");
|
|
||||||
String[] ca = current.split("\\.");
|
|
||||||
for (int i = 0; i < Math.min(sa.length, ca.length); i++) {
|
|
||||||
int sv = Integer.parseInt(sa[i]);
|
|
||||||
int cv = Integer.parseInt(ca[i]);
|
|
||||||
if (sv > cv) return true;
|
|
||||||
if (sv < cv) return false;
|
|
||||||
}
|
|
||||||
return sa.length > ca.length;
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,13 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>me.sashegdev</groupId>
|
<groupId>me.sashegdev</groupId>
|
||||||
<artifactId>ZernMCLauncher</artifactId>
|
<artifactId>ZernMCLauncher</artifactId>
|
||||||
<version>1.0.7</version>
|
<version>1.0.8</version>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.2.3</version>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
<version>3.5.0</version>
|
<version>3.5.0</version>
|
||||||
@@ -24,7 +28,7 @@
|
|||||||
<Implementation-Version>${project.version}</Implementation-Version>
|
<Implementation-Version>${project.version}</Implementation-Version>
|
||||||
<Implementation-Title>ZernMC Launcher</Implementation-Title>
|
<Implementation-Title>ZernMC Launcher</Implementation-Title>
|
||||||
<Implementation-Vendor>SashegDev</Implementation-Vendor>
|
<Implementation-Vendor>SashegDev</Implementation-Vendor>
|
||||||
<Implementation-Description>Полностью самописный Minecraft-лаунчер. Написанный SashegDev(в основном)</Implementation-Description>
|
<Implementation-Description>Samopisnui Minecraft-launcher. by SashegDev</Implementation-Description>
|
||||||
<Implementation-URL>https://github.com/SashegDev/launcher</Implementation-URL>
|
<Implementation-URL>https://github.com/SashegDev/launcher</Implementation-URL>
|
||||||
</manifestEntries>
|
</manifestEntries>
|
||||||
</transformer>
|
</transformer>
|
||||||
@@ -45,10 +49,11 @@
|
|||||||
<goal>launch4j</goal>
|
<goal>launch4j</goal>
|
||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<outfile>../server/builds/ZernMCLauncher.exe</outfile>
|
<outfile>../server/builds/ZernMCLauncher-${project.version}.exe</outfile>
|
||||||
<jar>../server/builds/ZernMCLauncher.jar</jar>
|
<jar>../server/builds/ZernMCLauncher.jar</jar>
|
||||||
<headerType>console</headerType>
|
<headerType>console</headerType>
|
||||||
<dontWrapJar>false</dontWrapJar>
|
<dontWrapJar>false</dontWrapJar>
|
||||||
|
<mainClass>me.sashegdev.zernmc.launcher.Bootstrap</mainClass>
|
||||||
<jre>
|
<jre>
|
||||||
<path>jre21</path>
|
<path>jre21</path>
|
||||||
<minVersion>21</minVersion>
|
<minVersion>21</minVersion>
|
||||||
@@ -56,13 +61,13 @@
|
|||||||
<versionInfo>
|
<versionInfo>
|
||||||
<fileVersion>${project.version}.0</fileVersion>
|
<fileVersion>${project.version}.0</fileVersion>
|
||||||
<txtFileVersion>${project.version}</txtFileVersion>
|
<txtFileVersion>${project.version}</txtFileVersion>
|
||||||
<fileDescription>ZernMC Launcher — A Little Minecraft Launcher</fileDescription>
|
<fileDescription>ZernMC Launcher — just a Minecraft launcher</fileDescription>
|
||||||
<productVersion>${project.version}.0</productVersion>
|
<productVersion>${project.version}.0</productVersion>
|
||||||
<txtProductVersion>${project.version}</txtProductVersion>
|
<txtProductVersion>${project.version}</txtProductVersion>
|
||||||
<productName>ZernMC Launcher</productName>
|
<productName>ZernMC Launcher</productName>
|
||||||
<companyName>ZernMC(SashegDev)</companyName>
|
<companyName>ZernMC(SashegDev)</companyName>
|
||||||
<internalName>ZernMCLauncher</internalName>
|
<internalName>ZernMCLauncher</internalName>
|
||||||
<originalFilename>ZernMCLauncher.exe</originalFilename>
|
<originalFilename>ZernMCLauncher-${project.version}.exe</originalFilename>
|
||||||
</versionInfo>
|
</versionInfo>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
@@ -80,9 +85,15 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<target>
|
<target>
|
||||||
<echo>${project.version}</echo>
|
<echo>${project.version}</echo>
|
||||||
|
<delete />
|
||||||
|
<mkdir />
|
||||||
<copy>
|
<copy>
|
||||||
<fileset />
|
<fileset>
|
||||||
|
<include />
|
||||||
|
<include />
|
||||||
|
</fileset>
|
||||||
</copy>
|
</copy>
|
||||||
|
<move />
|
||||||
<zip />
|
<zip />
|
||||||
</target>
|
</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -109,10 +120,35 @@
|
|||||||
</properties>
|
</properties>
|
||||||
</profile>
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>5.10.1</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<project.description>ZernMC Launcher - just a minimalistic launcher by SashegDev</project.description>
|
||||||
<mainClass>me.sashegdev.zernmc.launcher.Main</mainClass>
|
<mainClass>me.sashegdev.zernmc.launcher.Main</mainClass>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<project.organization.name>ZernMC</project.organization.name>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.inceptionYear>2026</project.inceptionYear>
|
||||||
</properties>
|
</properties>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# Maven
|
|
||||||
*/target/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Build outputs
|
|
||||||
server/builds/
|
|
||||||
server/logs/
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
|
||||||
*.iml
|
|
||||||
.vscode/
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
+156
-70
@@ -2,7 +2,6 @@
|
|||||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
<parent>
|
<parent>
|
||||||
@@ -11,90 +10,96 @@
|
|||||||
<version>1.0.8</version>
|
<version>1.0.8</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>zernmc-launcher</artifactId>
|
<artifactId>zernmclauncher</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>ZernMC Launcher</name>
|
<name>ZernMC Launcher</name>
|
||||||
<description>ZernMC Launcher - UI</description>
|
<description>Main launcher module with JFX UI</description>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- HTTP Client -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JSON -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.json</groupId>
|
||||||
|
<artifactId>json</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Console/Terminal -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.fusesource.jansi</groupId>
|
||||||
|
<artifactId>jansi</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jline</groupId>
|
||||||
|
<artifactId>jline</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>me.tongfei</groupId>
|
||||||
|
<artifactId>progressbar</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- IO -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-io</groupId>
|
||||||
|
<artifactId>commons-io</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JavaFX - Windows -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-controls</artifactId>
|
<artifactId>javafx-controls</artifactId>
|
||||||
<version>21</version>
|
<version>21</version>
|
||||||
<classifier>linux</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-web</artifactId>
|
<artifactId>javafx-web</artifactId>
|
||||||
<version>21</version>
|
<version>21</version>
|
||||||
<classifier>linux</classifier>
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-graphics</artifactId>
|
||||||
|
<version>21</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-base</artifactId>
|
||||||
|
<version>21</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-media</artifactId>
|
<artifactId>javafx-media</artifactId>
|
||||||
<version>21</version>
|
<version>21</version>
|
||||||
<classifier>linux</classifier>
|
<classifier>win</classifier>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.httpcomponents</groupId>
|
|
||||||
<artifactId>httpclient</artifactId>
|
|
||||||
<version>4.5.14</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
|
||||||
<artifactId>jackson-databind</artifactId>
|
|
||||||
<version>2.15.2</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.google.code.gson</groupId>
|
|
||||||
<artifactId>gson</artifactId>
|
|
||||||
<version>2.10.1</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.json</groupId>
|
|
||||||
<artifactId>json</artifactId>
|
|
||||||
<version>20230227</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.java.dev.jna</groupId>
|
|
||||||
<artifactId>jna</artifactId>
|
|
||||||
<version>5.13.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jline</groupId>
|
|
||||||
<artifactId>jline</artifactId>
|
|
||||||
<version>3.21.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.fusesource.jansi</groupId>
|
|
||||||
<artifactId>jansi</artifactId>
|
|
||||||
<version>2.4.0</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Test -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
<version>5.10.0</version>
|
<version>5.10.1</version>
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.mockito</groupId>
|
|
||||||
<artifactId>mockito-core</artifactId>
|
|
||||||
<version>5.7.0</version>
|
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
|
||||||
<version>3.1.2</version>
|
|
||||||
</plugin>
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
@@ -106,14 +111,13 @@
|
|||||||
<goal>shade</goal>
|
<goal>shade</goal>
|
||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<outputFile>../../server/builds/zernmc/bin/ZernMCLauncher.jar</outputFile>
|
<outputFile>../../server/builds/zernmclauncher.jar</outputFile>
|
||||||
<transformers>
|
<transformers>
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
<mainClass>me.sashegdev.zernmc.launcher.Main</mainClass>
|
<mainClass>me.sashegdev.zernmc.launcher.Main</mainClass>
|
||||||
<manifestEntries>
|
<manifestEntries>
|
||||||
<Implementation-Version>1.0.8</Implementation-Version>
|
<Implementation-Version>${project.version}</Implementation-Version>
|
||||||
<Implementation-Title>ZernMC Launcher</Implementation-Title>
|
<Implementation-Title>ZernMC Launcher</Implementation-Title>
|
||||||
<Implementation-Vendor>ZernMC</Implementation-Vendor>
|
|
||||||
</manifestEntries>
|
</manifestEntries>
|
||||||
</transformer>
|
</transformer>
|
||||||
</transformers>
|
</transformers>
|
||||||
@@ -121,6 +125,46 @@
|
|||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Launch4j для создания .exe из bootstrap JAR -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>com.akathist.maven.plugins.launch4j</groupId>
|
||||||
|
<artifactId>launch4j-maven-plugin</artifactId>
|
||||||
|
<version>2.5.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>l4j</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>launch4j</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<outfile>../../server/builds/zernmc-${project.version}.exe</outfile>
|
||||||
|
<jar>../../server/builds/zernmc-bootstrap.jar</jar>
|
||||||
|
<headerType>console</headerType>
|
||||||
|
<dontWrapJar>false</dontWrapJar>
|
||||||
|
<mainClass>me.sashegdev.zernmc.launcher.Bootstrap</mainClass>
|
||||||
|
<jre>
|
||||||
|
<path>lib/jre21</path>
|
||||||
|
<minVersion>21</minVersion>
|
||||||
|
</jre>
|
||||||
|
<versionInfo>
|
||||||
|
<fileVersion>${project.version}.0</fileVersion>
|
||||||
|
<txtFileVersion>${project.version}</txtFileVersion>
|
||||||
|
<fileDescription>ZernMC Launcher</fileDescription>
|
||||||
|
<productVersion>${project.version}.0</productVersion>
|
||||||
|
<txtProductVersion>${project.version}</txtProductVersion>
|
||||||
|
<productName>ZernMC</productName>
|
||||||
|
<companyName>ZernMC</companyName>
|
||||||
|
<internalName>zernmc</internalName>
|
||||||
|
<originalFilename>zernmc-${project.version}.exe</originalFilename>
|
||||||
|
</versionInfo>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Post-build: копирование JRE и создание ZIP -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-antrun-plugin</artifactId>
|
<artifactId>maven-antrun-plugin</artifactId>
|
||||||
@@ -131,17 +175,59 @@
|
|||||||
<goals><goal>run</goal></goals>
|
<goals><goal>run</goal></goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<target>
|
<target>
|
||||||
<!-- Windows ZIP -->
|
<echo file="../../server/builds/build.version">${project.version}</echo>
|
||||||
<zip destfile="../../server/builds/ZernMCLauncher-1.0.8-win.zip"
|
|
||||||
basedir="../../server/builds/zernmc"
|
|
||||||
includes="zernmc.exe,zernmc.jar,bin/**,lib/jre21-custom/**,assets/**"
|
|
||||||
excludes="logs/**"/>
|
|
||||||
|
|
||||||
<!-- Any (Linux/Mac) ZIP -->
|
<!-- Удаляем старую папку lib если есть -->
|
||||||
<zip destfile="../../server/builds/ZernMCLauncher-1.0.8-any.zip"
|
<delete dir="../../server/builds/lib"/>
|
||||||
basedir="../../server/builds/zernmc"
|
|
||||||
includes="zernmc.jar,bin/**,lib/jre21-custom/**,assets/**"
|
<!-- Создаем папку lib -->
|
||||||
excludes="zernmc.exe,logs/**"/>
|
<mkdir dir="../../server/builds/lib"/>
|
||||||
|
|
||||||
|
<!-- Копируем JRE в lib/jre21 -->
|
||||||
|
<copy todir="../../server/builds/lib/jre21" overwrite="true">
|
||||||
|
<fileset dir="${user.home}/launcher/jre/jre21">
|
||||||
|
<include name="*"/>
|
||||||
|
<include name="**/*"/>
|
||||||
|
</fileset>
|
||||||
|
</copy>
|
||||||
|
|
||||||
|
<!-- Копируем JavaFX модули в lib/javafx -->
|
||||||
|
<mkdir dir="../../server/builds/lib/javafx"/>
|
||||||
|
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
||||||
|
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-controls/21">
|
||||||
|
<include name="*win.jar"/>
|
||||||
|
</fileset>
|
||||||
|
</copy>
|
||||||
|
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
||||||
|
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-graphics/21">
|
||||||
|
<include name="*win.jar"/>
|
||||||
|
</fileset>
|
||||||
|
</copy>
|
||||||
|
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
||||||
|
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-base/21">
|
||||||
|
<include name="*win.jar"/>
|
||||||
|
</fileset>
|
||||||
|
</copy>
|
||||||
|
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
||||||
|
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-web/21">
|
||||||
|
<include name="*win.jar"/>
|
||||||
|
</fileset>
|
||||||
|
</copy>
|
||||||
|
<copy todir="../../server/builds/lib/javafx" overwrite="true">
|
||||||
|
<fileset dir="${user.home}/.m2/repository/org/openjfx/javafx-media/21">
|
||||||
|
<include name="*win.jar"/>
|
||||||
|
</fileset>
|
||||||
|
</copy>
|
||||||
|
|
||||||
|
<!-- Переименовываем exe для zip -->
|
||||||
|
<move file="../../server/builds/zernmc-${project.version}.exe"
|
||||||
|
tofile="../../server/builds/zernmc.exe" overwrite="true"/>
|
||||||
|
|
||||||
|
<!-- Создаём zip с exe, jar и lib -->
|
||||||
|
<zip destfile="../../server/builds/ZernMC-win-${project.version}.zip"
|
||||||
|
basedir="../../server/builds"
|
||||||
|
includes="zernmc.exe,zernmclauncher.jar,zernmc-bootstrap.jar,lib/**"
|
||||||
|
excludes="build.version,*-${project.version}.*"/>
|
||||||
</target>
|
</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
|
||||||
import me.sashegdev.zernmc.launcher.menu.*;
|
|
||||||
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
|
|
||||||
import me.sashegdev.zernmc.launcher.ui.jcef.UILauncher;
|
|
||||||
import me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.*;
|
|
||||||
import java.io.IOException;
|
|
||||||
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.nio.file.StandardCopyOption;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class Main {
|
|
||||||
|
|
||||||
private static final String CURRENT_VERSION = Version.getCurrentVersion();
|
|
||||||
private static final LauncherAPI api = new LauncherAPI();
|
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException {
|
|
||||||
System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true");
|
|
||||||
System.setProperty("file.encoding", "UTF-8");
|
|
||||||
System.setProperty("sun.err.encoding", "UTF-8");
|
|
||||||
System.setProperty("sun.stdout.encoding", "UTF-8");
|
|
||||||
|
|
||||||
if (args.length > 0 && args[0].equals("--cli")) {
|
|
||||||
launchCLI(args);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
launchUI(args);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println("UI недоступен, переход в CLI режим: " + e.getMessage());
|
|
||||||
launchCLI(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void launchUI(String[] args) throws Exception {
|
|
||||||
System.out.println("Запуск JFX UI...");
|
|
||||||
JFXLauncher.main(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void launchCLI(String[] args) throws IOException {
|
|
||||||
ZAnsi.install();
|
|
||||||
System.out.print("\033[H\033[2J");
|
|
||||||
System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION));
|
|
||||||
|
|
||||||
ZHttpClient.checkAllServicesOnStartup();
|
|
||||||
checkAndAutoUpdateLauncher();
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка авторизации..."));
|
|
||||||
var sessionResponse = api.checkSession();
|
|
||||||
|
|
||||||
if (!sessionResponse.isSuccess()) {
|
|
||||||
LoginMenu loginMenu = new LoginMenu();
|
|
||||||
boolean loggedIn = loginMenu.show();
|
|
||||||
if (!loggedIn) {
|
|
||||||
System.out.println(ZAnsi.yellow("До свидания!"));
|
|
||||||
ZAnsi.uninstall();
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var sessionInfo = sessionResponse.getData();
|
|
||||||
System.out.println(ZAnsi.brightGreen("Добро пожаловать обратно, " + sessionInfo.getUsername() + "!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
mainLoop();
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println(ZAnsi.brightRed("Критическая ошибка: " + e.getMessage()));
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
ZAnsi.uninstall();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void checkAndAutoUpdateLauncher() {
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка обновлений лаунчера..."));
|
|
||||||
try {
|
|
||||||
String json = ZHttpClient.getLauncherVersionInfo();
|
|
||||||
String serverVersion = extractVersion(json);
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.white("Текущая версия: ") + CURRENT_VERSION);
|
|
||||||
System.out.println(ZAnsi.white("Версия на сервере: ") + serverVersion);
|
|
||||||
|
|
||||||
if (Version.isNewer(CURRENT_VERSION, serverVersion)) {
|
|
||||||
System.out.println(ZAnsi.brightYellow("\nДоступна новая версия лаунчера! (" + serverVersion + ")"));
|
|
||||||
System.out.println(ZAnsi.cyan("Начинается автоматическое обновление...\n"));
|
|
||||||
performAutoUpdate(serverVersion);
|
|
||||||
restartLauncher();
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightGreen("Лаунчер актуален."));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.yellow("Не удалось проверить обновления лаунчера."));
|
|
||||||
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void performAutoUpdate(String newVersion) throws Exception {
|
|
||||||
String downloadUrl = ZHttpClient.getBaseUrl() + "/launcher/download?type=jar";
|
|
||||||
Path currentJar = getCurrentJarPath();
|
|
||||||
Path tempJar = currentJar.getParent().resolve("zernmc-launcher-new.jar");
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Скачивание версии " + newVersion + "..."));
|
|
||||||
|
|
||||||
HttpClient client = HttpClient.newBuilder().build();
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(java.net.URI.create(downloadUrl))
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(tempJar));
|
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
throw new IOException("Сервер вернул код: " + response.statusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
long size = Files.size(tempJar);
|
|
||||||
System.out.println(ZAnsi.brightGreen("Скачано успешно (" + (size / 1024) + " KB)"));
|
|
||||||
|
|
||||||
Files.move(tempJar, currentJar, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
System.out.println(ZAnsi.brightGreen("Обновление успешно установлено!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void restartLauncher() {
|
|
||||||
try {
|
|
||||||
String javaPath = System.getProperty("java.home") + "/bin/java";
|
|
||||||
String jarPath = getCurrentJarPath().toAbsolutePath().toString();
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("Перезапуск лаунчера с новой версией..."));
|
|
||||||
|
|
||||||
new ProcessBuilder(javaPath, "-jar", jarPath)
|
|
||||||
.inheritIO()
|
|
||||||
.start();
|
|
||||||
|
|
||||||
System.exit(0);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println(ZAnsi.brightRed("Не удалось перезапустить лаунчер."));
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractVersion(String json) {
|
|
||||||
try {
|
|
||||||
return json.replaceAll(".*\"version\"\\s*:\\s*\"([^\"]+)\".*", "$1");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path getCurrentJarPath() {
|
|
||||||
try {
|
|
||||||
return Path.of(Main.class.getProtectionDomain()
|
|
||||||
.getCodeSource()
|
|
||||||
.getLocation()
|
|
||||||
.toURI());
|
|
||||||
} catch (Exception e) {
|
|
||||||
return Path.of("zernmc-launcher-1.0-jar-with-dependencies.jar");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ГЛАВНЫЙ ЦИКЛ ======================
|
|
||||||
private static void mainLoop() throws Exception {
|
|
||||||
if (Config.isZernMCBuild()) {
|
|
||||||
zernMCFlow();
|
|
||||||
} else {
|
|
||||||
globalFlow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ZERNMC FLOW ======================
|
|
||||||
private static void zernMCFlow() throws Exception {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.header("=== ZernMC Private Launcher ==="));
|
|
||||||
|
|
||||||
// 1. Проверка подключения к серверу
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка подключения к ZernMC серверу..."));
|
|
||||||
try {
|
|
||||||
String response = ZHttpClient.get("/health");
|
|
||||||
System.out.println(ZAnsi.brightGreen("✓ Сервер доступен"));
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.brightRed("✗ Не удалось подключиться к ZernMC серверу"));
|
|
||||||
System.out.println(ZAnsi.white("Ошибка: " + e.getMessage()));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Авторизация
|
|
||||||
boolean sessionRestored = AuthManager.loadSavedSession();
|
|
||||||
if (!sessionRestored) {
|
|
||||||
LoginMenu loginMenu = new LoginMenu();
|
|
||||||
boolean loggedIn = loginMenu.show();
|
|
||||||
if (!loggedIn) {
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightGreen("Добро пожаловать обратно, " + AuthManager.getUsername() + "!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Запуск меню (LaunchMenu сам определит режим и вызовет нужный flow)
|
|
||||||
LaunchMenu launchMenu = new LaunchMenu();
|
|
||||||
launchMenu.show(); // ← Здесь будет вызван showZernMCOnly() внутри
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== GLOBAL FLOW ======================
|
|
||||||
private static void globalFlow() throws Exception {
|
|
||||||
while (true) {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.header("=== ZernMC Launcher ==="));
|
|
||||||
|
|
||||||
List<String> options = List.of(
|
|
||||||
"Запустить игру",
|
|
||||||
"Проверка обновлений",
|
|
||||||
"Настройки",
|
|
||||||
"Проверка подключения к серверам",
|
|
||||||
"Выход"
|
|
||||||
);
|
|
||||||
|
|
||||||
ArrowMenu menu = new ArrowMenu("Главное меню", options);
|
|
||||||
int choice = menu.show();
|
|
||||||
|
|
||||||
if (choice == -1 || choice == 4) {
|
|
||||||
System.out.println(ZAnsi.yellow("До свидания!"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (choice) {
|
|
||||||
case 0 -> new LaunchMenu().show(); // обычный LaunchMenu
|
|
||||||
case 1 -> new UpdateMenu().show();
|
|
||||||
case 2 -> new SettingsMenu().show();
|
|
||||||
case 3 -> new ServerCheckMenu().show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-254
@@ -1,254 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.ui.jcef;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
|
||||||
import com.sun.net.httpserver.Headers;
|
|
||||||
import me.sashegdev.zernmc.launcher.api.ApiResponse;
|
|
||||||
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
public class LaunchServer {
|
|
||||||
private static final int PORT = 8080;
|
|
||||||
private final LauncherAPI api;
|
|
||||||
private final UIBridge bridge;
|
|
||||||
private HttpServer server;
|
|
||||||
private final Gson gson = new Gson();
|
|
||||||
|
|
||||||
public LaunchServer(UIBridge bridge) {
|
|
||||||
this.api = new LauncherAPI();
|
|
||||||
this.bridge = bridge;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() throws IOException {
|
|
||||||
server = HttpServer.create(new InetSocketAddress("localhost", PORT), 0);
|
|
||||||
|
|
||||||
server.createContext("/api/login", this::handleLogin);
|
|
||||||
server.createContext("/api/account", this::handleAccount);
|
|
||||||
server.createContext("/api/instances", this::handleInstances);
|
|
||||||
server.createContext("/api/launch", this::handleLaunch);
|
|
||||||
server.createContext("/api/install", this::handleInstall);
|
|
||||||
server.createContext("/api/logs", this::handleLogs);
|
|
||||||
server.createContext("/api/exit", this::handleExit);
|
|
||||||
server.createContext("/ui/", this::handleStatic);
|
|
||||||
|
|
||||||
server.setExecutor(Executors.newCachedThreadPool());
|
|
||||||
server.start();
|
|
||||||
|
|
||||||
bridge.log("HTTP сервер запущен на порту " + PORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (server != null) {
|
|
||||||
server.stop(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleLogin(HttpExchange exchange) throws IOException {
|
|
||||||
if (!"POST".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<String, String> body = parseJson(exchange.getRequestBody());
|
|
||||||
String username = body.get("username");
|
|
||||||
String password = body.get("password");
|
|
||||||
|
|
||||||
var result = api.login(username, password);
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
data.put("username", result.getData().getUsername());
|
|
||||||
data.put("token", result.getData().getToken());
|
|
||||||
sendJson(exchange, ApiResponse.success(data));
|
|
||||||
bridge.log("Пользователь вошел: " + username);
|
|
||||||
} else {
|
|
||||||
sendJson(exchange, ApiResponse.error(result.getError()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Ошибка: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleAccount(HttpExchange exchange) throws IOException {
|
|
||||||
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!api.isLoggedIn()) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Не авторизован"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
data.put("username", api.getCurrentUsername());
|
|
||||||
data.put("passActive", AuthManager.hasActivePass());
|
|
||||||
sendJson(exchange, ApiResponse.success(data));
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, ApiResponse.error(e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleInstances(HttpExchange exchange) throws IOException {
|
|
||||||
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var result = api.getAllInstances();
|
|
||||||
sendJson(exchange, result);
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, ApiResponse.error(e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleLaunch(HttpExchange exchange) throws IOException {
|
|
||||||
if (!"POST".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!api.isLoggedIn()) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Не авторизован"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<String, String> body = parseJson(exchange.getRequestBody());
|
|
||||||
String name = body.get("name");
|
|
||||||
|
|
||||||
var result = api.launch(name);
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
data.put("pid", result.getData().getPid());
|
|
||||||
data.put("status", result.getData().getStatus());
|
|
||||||
sendJson(exchange, ApiResponse.success(data));
|
|
||||||
bridge.log("Запущена сборка: " + name);
|
|
||||||
} else {
|
|
||||||
sendJson(exchange, ApiResponse.error(result.getError()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Ошибка запуска: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleInstall(HttpExchange exchange) throws IOException {
|
|
||||||
if (!"POST".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!api.isLoggedIn()) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Не авторизован"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<String, String> body = parseJson(exchange.getRequestBody());
|
|
||||||
String name = body.get("name");
|
|
||||||
String version = body.get("version");
|
|
||||||
String loader = body.get("loader");
|
|
||||||
|
|
||||||
bridge.log("Установка сборки: " + name + " " + version + " " + loader);
|
|
||||||
|
|
||||||
var createResult = api.instances().createInstance(name);
|
|
||||||
if (!createResult.isSuccess()) {
|
|
||||||
sendJson(exchange, ApiResponse.error(createResult.getError()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Instance instance = InstanceManager.getInstance(name);
|
|
||||||
if (instance != null) {
|
|
||||||
instance.setMinecraftVersion(version);
|
|
||||||
instance.setLoaderType(loader);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendJson(exchange, ApiResponse.success(true));
|
|
||||||
bridge.log("Сборка установлена: " + name);
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Ошибка установки: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleLogs(HttpExchange exchange) throws IOException {
|
|
||||||
String logs = bridge.getLogs();
|
|
||||||
sendJson(exchange, ApiResponse.success(logs));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleExit(HttpExchange exchange) throws IOException {
|
|
||||||
bridge.log("Завершение работы...");
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleStatic(HttpExchange exchange) throws IOException {
|
|
||||||
String path = exchange.getRequestURI().getPath();
|
|
||||||
if (path.equals("/ui/") || path.equals("/ui")) {
|
|
||||||
path = "/ui/index.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = getClass().getResource(path);
|
|
||||||
|
|
||||||
if (resource == null) {
|
|
||||||
exchange.sendResponseHeaders(404, 0);
|
|
||||||
exchange.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
byte[] content = resource.openStream().readAllBytes();
|
|
||||||
String contentType = getContentType(path);
|
|
||||||
|
|
||||||
exchange.getResponseHeaders().set("Content-Type", contentType);
|
|
||||||
exchange.sendResponseHeaders(200, content.length);
|
|
||||||
|
|
||||||
OutputStream os = exchange.getResponseBody();
|
|
||||||
os.write(content);
|
|
||||||
os.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getContentType(String path) {
|
|
||||||
if (path.endsWith(".html")) return "text/html; charset=utf-8";
|
|
||||||
if (path.endsWith(".css")) return "text/css; charset=utf-8";
|
|
||||||
if (path.endsWith(".js")) return "application/javascript; charset=utf-8";
|
|
||||||
return "text/plain";
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Map<String, String> parseJson(InputStream body) {
|
|
||||||
try {
|
|
||||||
String json = new String(body.readAllBytes(), StandardCharsets.UTF_8);
|
|
||||||
return gson.fromJson(json, Map.class);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return new HashMap<>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendJson(HttpExchange exchange, ApiResponse response) {
|
|
||||||
try {
|
|
||||||
String json = gson.toJson(response);
|
|
||||||
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
|
||||||
exchange.sendResponseHeaders(200, bytes.length);
|
|
||||||
|
|
||||||
OutputStream os = exchange.getResponseBody();
|
|
||||||
os.write(bytes);
|
|
||||||
os.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.ui.jcef;
|
|
||||||
|
|
||||||
public class UIBridge {
|
|
||||||
public void log(String message) {
|
|
||||||
System.out.println("[UI] " + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLogs() {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.ui.jcef;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.net.URI;
|
|
||||||
|
|
||||||
public class UILauncher {
|
|
||||||
private static final String APP_TITLE = "ZernMC Launcher";
|
|
||||||
private final LauncherAPI api;
|
|
||||||
private final UIBridge bridge;
|
|
||||||
private LaunchServer server;
|
|
||||||
|
|
||||||
public UILauncher() {
|
|
||||||
this.api = new LauncherAPI();
|
|
||||||
this.bridge = new UIBridge();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launch() throws Exception {
|
|
||||||
redirectSystemLogs();
|
|
||||||
bridge.log("Запуск UI...");
|
|
||||||
|
|
||||||
server = new LaunchServer(bridge);
|
|
||||||
server.start();
|
|
||||||
|
|
||||||
openBrowser();
|
|
||||||
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
|
||||||
bridge.log("Выключение...");
|
|
||||||
if (server != null) server.stop();
|
|
||||||
}));
|
|
||||||
|
|
||||||
Thread.currentThread().join();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openBrowser() {
|
|
||||||
String url = "http://localhost:8080/ui/";
|
|
||||||
bridge.log("Открытие браузера: " + url);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
|
||||||
Desktop.getDesktop().browse(URI.create(url));
|
|
||||||
bridge.log("Браузер открыт");
|
|
||||||
} else {
|
|
||||||
bridge.log("Desktop browsing not supported");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
bridge.log("Ошибка открытия браузера: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void redirectSystemLogs() {
|
|
||||||
PrintStream originalOut = System.out;
|
|
||||||
PrintStream originalErr = System.err;
|
|
||||||
|
|
||||||
System.setOut(new PrintStream(new ByteArrayOutputStream() {
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) {
|
|
||||||
String line = new String(b, off, len).trim();
|
|
||||||
if (!line.isEmpty()) {
|
|
||||||
bridge.log(line);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
originalOut.write(b, off, len);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(int b) {
|
|
||||||
try {
|
|
||||||
originalOut.write(b);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
System.setErr(new PrintStream(new ByteArrayOutputStream() {
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) {
|
|
||||||
String line = new String(b, off, len).trim();
|
|
||||||
if (!line.isEmpty()) {
|
|
||||||
bridge.log("[ERROR] " + line);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
originalErr.write(b, off, len);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
try {
|
|
||||||
UILauncher launcher = new UILauncher();
|
|
||||||
launcher.launch();
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("UI launch failed: " + e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.ui.jfx;
|
|
||||||
|
|
||||||
import javafx.application.Application;
|
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.web.WebView;
|
|
||||||
import javafx.scene.web.WebEngine;
|
|
||||||
import javafx.stage.Stage;
|
|
||||||
import javafx.concurrent.Worker;
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.Config;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
|
||||||
import com.sun.net.httpserver.Headers;
|
|
||||||
|
|
||||||
public class JFXLauncher extends Application {
|
|
||||||
private static final int PORT = 8080;
|
|
||||||
private static final String APP_TITLE = "ZernMC Launcher";
|
|
||||||
private final LauncherAPI api = new LauncherAPI();
|
|
||||||
private final Gson gson = new Gson();
|
|
||||||
private HttpServer server;
|
|
||||||
private StringBuilder logBuffer = new StringBuilder();
|
|
||||||
private Stage mainStage;
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
launch(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start(Stage stage) {
|
|
||||||
this.mainStage = stage;
|
|
||||||
|
|
||||||
try {
|
|
||||||
log("Запуск JFX UI...");
|
|
||||||
startServer();
|
|
||||||
|
|
||||||
WebView webView = new WebView();
|
|
||||||
WebEngine engine = webView.getEngine();
|
|
||||||
engine.setJavaScriptEnabled(true);
|
|
||||||
|
|
||||||
engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
|
|
||||||
if (newState == Worker.State.SUCCEEDED) {
|
|
||||||
log("Страница загружена");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
String url = "http://localhost:" + PORT + "/ui/";
|
|
||||||
engine.load(url);
|
|
||||||
|
|
||||||
stage.setTitle(APP_TITLE);
|
|
||||||
stage.setWidth(1200);
|
|
||||||
stage.setHeight(800);
|
|
||||||
stage.setScene(new Scene(webView));
|
|
||||||
stage.show();
|
|
||||||
|
|
||||||
log("Окно отображено");
|
|
||||||
|
|
||||||
stage.setOnCloseRequest(e -> {
|
|
||||||
log("Закрытие...");
|
|
||||||
stopServer();
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log("Ошибка: " + e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startServer() throws Exception {
|
|
||||||
server = HttpServer.create(new InetSocketAddress("localhost", PORT), 0);
|
|
||||||
|
|
||||||
server.createContext("/api/login", this::handleLogin);
|
|
||||||
server.createContext("/api/account", this::handleAccount);
|
|
||||||
server.createContext("/api/instances", this::handleInstances);
|
|
||||||
server.createContext("/api/launch", this::handleLaunch);
|
|
||||||
server.createContext("/api/install", this::handleInstall);
|
|
||||||
server.createContext("/api/logs", this::handleLogs);
|
|
||||||
server.createContext("/api/logs/instance", this::handleInstanceLogs);
|
|
||||||
server.createContext("/api/exit", this::handleExit);
|
|
||||||
server.createContext("/ui/", this::handleStatic);
|
|
||||||
|
|
||||||
server.setExecutor(Executors.newCachedThreadPool());
|
|
||||||
server.start();
|
|
||||||
|
|
||||||
log("HTTP сервер на порту " + PORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopServer() {
|
|
||||||
if (server != null) server.stop(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleLogin(HttpExchange exchange) {
|
|
||||||
try {
|
|
||||||
if (!"POST".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", "Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> body = parseJson(exchange.getRequestBody());
|
|
||||||
String username = body.get("username");
|
|
||||||
String password = body.get("password");
|
|
||||||
|
|
||||||
var result = api.login(username, password);
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
data.put("username", result.getData().getUsername());
|
|
||||||
data.put("token", result.getData().getToken());
|
|
||||||
sendJson(exchange, Map.of("success", true, "data", data));
|
|
||||||
log("Вход: " + username);
|
|
||||||
} else {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", result.getError()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleAccount(HttpExchange exchange) {
|
|
||||||
try {
|
|
||||||
if (!api.isLoggedIn()) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", "Не авторизован"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
data.put("username", api.getCurrentUsername());
|
|
||||||
data.put("passActive", AuthManager.hasActivePass());
|
|
||||||
sendJson(exchange, Map.of("success", true, "data", data));
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleInstances(HttpExchange exchange) {
|
|
||||||
try {
|
|
||||||
var result = api.getAllInstances();
|
|
||||||
Map<String, Object> response = new HashMap<>();
|
|
||||||
response.put("success", result.isSuccess());
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
response.put("data", result.getData());
|
|
||||||
} else {
|
|
||||||
response.put("error", result.getError());
|
|
||||||
}
|
|
||||||
sendJson(exchange, response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleLaunch(HttpExchange exchange) {
|
|
||||||
try {
|
|
||||||
if (!api.isLoggedIn()) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", "Не авторизован"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> body = parseJson(exchange.getRequestBody());
|
|
||||||
String name = body.get("name");
|
|
||||||
|
|
||||||
var result = api.launch(name);
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
data.put("pid", result.getData().getPid());
|
|
||||||
data.put("status", result.getData().getStatus());
|
|
||||||
sendJson(exchange, Map.of("success", true, "data", data));
|
|
||||||
log("Запущено: " + name);
|
|
||||||
} else {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", result.getError()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleInstall(HttpExchange exchange) {
|
|
||||||
try {
|
|
||||||
if (!api.isLoggedIn()) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", "Не авторизован"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> body = parseJson(exchange.getRequestBody());
|
|
||||||
String name = body.get("name");
|
|
||||||
String version = body.get("version");
|
|
||||||
String loader = body.get("loader");
|
|
||||||
|
|
||||||
log("Установка: " + name + " " + version + " " + loader);
|
|
||||||
|
|
||||||
var createResult = api.instances().createInstance(name);
|
|
||||||
if (!createResult.isSuccess()) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", createResult.getError()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Instance instance = InstanceManager.getInstance(name);
|
|
||||||
if (instance != null) {
|
|
||||||
instance.setMinecraftVersion(version);
|
|
||||||
instance.setLoaderType(loader);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendJson(exchange, Map.of("success", true, "data", true));
|
|
||||||
log("Установлено: " + name);
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleLogs(HttpExchange exchange) {
|
|
||||||
sendJson(exchange, Map.of("success", true, "data", logBuffer.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleInstanceLogs(HttpExchange exchange) {
|
|
||||||
try {
|
|
||||||
String query = exchange.getRequestURI().getQuery();
|
|
||||||
String instanceName = null;
|
|
||||||
if (query != null && query.startsWith("name=")) {
|
|
||||||
instanceName = query.substring(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instanceName == null) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", "Укажите имя сборки"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path instanceDir = me.sashegdev.zernmc.launcher.utils.Config.getInstancesDir().resolve(instanceName);
|
|
||||||
Path logsDir = instanceDir.resolve("logs");
|
|
||||||
|
|
||||||
if (!Files.exists(logsDir)) {
|
|
||||||
sendJson(exchange, Map.of("success", true, "data", ""));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder logs = new StringBuilder();
|
|
||||||
try (var stream = Files.list(logsDir)) {
|
|
||||||
stream.filter(f -> f.toString().endsWith(".log"))
|
|
||||||
.sorted((a, b) -> b.compareTo(a))
|
|
||||||
.limit(5)
|
|
||||||
.forEach(logFile -> {
|
|
||||||
try {
|
|
||||||
logs.append("=== ").append(logFile.getFileName()).append(" ===\n");
|
|
||||||
logs.append(Files.readString(logFile));
|
|
||||||
logs.append("\n");
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sendJson(exchange, Map.of("success", true, "data", logs.toString()));
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleExit(HttpExchange exchange) {
|
|
||||||
log("Выход...");
|
|
||||||
if (mainStage != null) mainStage.close();
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleStatic(HttpExchange exchange) {
|
|
||||||
try {
|
|
||||||
String path = exchange.getRequestURI().getPath();
|
|
||||||
if (path.equals("/ui/") || path.equals("/ui")) path = "/ui/index.html";
|
|
||||||
|
|
||||||
var resource = JFXLauncher.class.getResource(path);
|
|
||||||
if (resource == null) {
|
|
||||||
exchange.sendResponseHeaders(404, 0);
|
|
||||||
exchange.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] content = resource.openStream().readAllBytes();
|
|
||||||
String ct = getContentType(path);
|
|
||||||
|
|
||||||
exchange.getResponseHeaders().set("Content-Type", ct);
|
|
||||||
exchange.sendResponseHeaders(200, content.length);
|
|
||||||
exchange.getResponseBody().write(content);
|
|
||||||
exchange.close();
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getContentType(String path) {
|
|
||||||
if (path.endsWith(".html")) return "text/html; charset=utf-8";
|
|
||||||
if (path.endsWith(".css")) return "text/css; charset=utf-8";
|
|
||||||
if (path.endsWith(".js")) return "application/javascript; charset=utf-8";
|
|
||||||
return "text/plain";
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Map<String, String> parseJson(InputStream body) {
|
|
||||||
try {
|
|
||||||
return gson.fromJson(new String(body.readAllBytes(), StandardCharsets.UTF_8), Map.class);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return new HashMap<>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendJson(HttpExchange exchange, Map<String, Object> response) {
|
|
||||||
try {
|
|
||||||
String json = gson.toJson(response);
|
|
||||||
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
|
||||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
|
||||||
exchange.sendResponseHeaders(200, bytes.length);
|
|
||||||
exchange.getResponseBody().write(bytes);
|
|
||||||
exchange.close();
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void log(String msg) {
|
|
||||||
String entry = "[" + java.time.LocalTime.now() + "] " + msg + "\n";
|
|
||||||
logBuffer.append(entry);
|
|
||||||
System.out.println("[JFX] " + msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.utils;
|
|
||||||
|
|
||||||
import java.util.jar.Attributes;
|
|
||||||
import java.util.jar.Manifest;
|
|
||||||
|
|
||||||
public class Version {
|
|
||||||
|
|
||||||
public static String getCurrentVersion() {
|
|
||||||
try {
|
|
||||||
// Способ 1: Из манифеста (самый правильный)
|
|
||||||
Manifest manifest = new Manifest(
|
|
||||||
Version.class.getClassLoader().getResourceAsStream("META-INF/MANIFEST.MF")
|
|
||||||
);
|
|
||||||
|
|
||||||
String version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
|
|
||||||
if (version != null && !version.isBlank()) {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Способ 2: Из Package (запасной)
|
|
||||||
version = Version.class.getPackage().getImplementationVersion();
|
|
||||||
if (version != null && !version.isBlank()) {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// если не получилось прочитать манифест — идём дальше
|
|
||||||
}
|
|
||||||
|
|
||||||
// Финальный fallback
|
|
||||||
return "1.0.0";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isNewer(String current, String server) {
|
|
||||||
if (current == null || server == null) return false;
|
|
||||||
|
|
||||||
current = current.replace("-SNAPSHOT", "").trim();
|
|
||||||
server = server.replace("-SNAPSHOT", "").trim();
|
|
||||||
|
|
||||||
if (current.equals(server)) return false;
|
|
||||||
|
|
||||||
String[] cParts = current.split("\\.");
|
|
||||||
String[] sParts = server.split("\\.");
|
|
||||||
|
|
||||||
int max = Math.max(cParts.length, sParts.length);
|
|
||||||
|
|
||||||
for (int i = 0; i < max; i++) {
|
|
||||||
int c = i < cParts.length ? Integer.parseInt(cParts[i]) : 0;
|
|
||||||
int s = i < sParts.length ? Integer.parseInt(sParts[i]) : 0;
|
|
||||||
|
|
||||||
if (s > c) return true;
|
|
||||||
if (s < c) return false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+92
-61
@@ -7,11 +7,12 @@ import java.nio.file.*;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class Bootstrap {
|
public class Bootstrap {
|
||||||
private static final String VERSION_FILE = "build.version";
|
private static final String VERSION_FILE = "build.version";
|
||||||
private static final String JAR_NAME = "bin/ZernMCLauncher.jar";
|
private static final String JAR_NAME = "ZernMCLauncher.jar";
|
||||||
private static final String BASE_URL = "http://87.120.187.36:1582/launcher/download?type=jar";
|
private static final String BASE_URL = "http://87.120.187.36:1582";
|
||||||
|
|
||||||
private static Path baseDir;
|
private static Path baseDir;
|
||||||
private static Path logDir;
|
private static Path logDir;
|
||||||
@@ -23,26 +24,31 @@ public class Bootstrap {
|
|||||||
|
|
||||||
log("=== ZernMC Launcher ===");
|
log("=== ZernMC Launcher ===");
|
||||||
|
|
||||||
// Если передан аргумент --launcher, запускаем UI напрямую
|
// Определяем режим запуска
|
||||||
if (args.length > 0 && args[0].equals("--launcher")) {
|
List<String> argList = Arrays.asList(args);
|
||||||
launchUI();
|
boolean cliMode = argList.contains("--cli");
|
||||||
return;
|
boolean jfxMode = !cliMode; // по умолчанию JFX
|
||||||
}
|
|
||||||
|
|
||||||
|
// Проверка и обновление лаунчера
|
||||||
String currentVersion = readCurrentVersion();
|
String currentVersion = readCurrentVersion();
|
||||||
String serverVersion = getServerVersion();
|
String serverVersion = getServerVersion();
|
||||||
|
|
||||||
log("Локальная: " + currentVersion);
|
log("Локальная версия: " + currentVersion);
|
||||||
log("Сервер: " + serverVersion);
|
log("Версия на сервере: " + serverVersion);
|
||||||
|
|
||||||
if (isNewer(serverVersion, currentVersion)) {
|
if (isNewer(serverVersion, currentVersion)) {
|
||||||
log("Доступно обновление!");
|
log("Доступно обновление!");
|
||||||
downloadUpdate();
|
downloadUpdate(serverVersion);
|
||||||
} else {
|
} else {
|
||||||
log("Актуально");
|
log("Версия актуальна");
|
||||||
}
|
}
|
||||||
|
|
||||||
launchGame();
|
// Запуск в выбранном режиме
|
||||||
|
if (jfxMode) {
|
||||||
|
launchJFX();
|
||||||
|
} else {
|
||||||
|
launchCLI();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void log(String msg) {
|
private static void log(String msg) {
|
||||||
@@ -94,13 +100,15 @@ public class Bootstrap {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void downloadUpdate() throws Exception {
|
private static void downloadUpdate(String newVersion) throws Exception {
|
||||||
URL url = new URL(BASE_URL);
|
URL url = new URL(BASE_URL + "/launcher/download/jar");
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
conn.setRequestMethod("GET");
|
conn.setRequestMethod("GET");
|
||||||
|
|
||||||
if (conn.getResponseCode() == 200) {
|
if (conn.getResponseCode() == 200) {
|
||||||
Path tmp = baseDir.resolve(JAR_NAME + ".new");
|
Path jarFile = baseDir.resolve(JAR_NAME);
|
||||||
|
Path tmp = jarFile.resolveSibling("zernmc-launcher-new.jar");
|
||||||
|
|
||||||
try (InputStream in = conn.getInputStream();
|
try (InputStream in = conn.getInputStream();
|
||||||
OutputStream out = new FileOutputStream(tmp.toFile())) {
|
OutputStream out = new FileOutputStream(tmp.toFile())) {
|
||||||
byte[] buf = new byte[8192];
|
byte[] buf = new byte[8192];
|
||||||
@@ -114,71 +122,94 @@ public class Bootstrap {
|
|||||||
}
|
}
|
||||||
log("Скачано");
|
log("Скачано");
|
||||||
|
|
||||||
Path jarFile = baseDir.resolve(JAR_NAME);
|
Path backup = jarFile.resolveSibling(JAR_NAME + ".old");
|
||||||
Path backup = baseDir.resolve(JAR_NAME + ".old");
|
|
||||||
|
|
||||||
if (Files.exists(jarFile)) Files.move(jarFile, backup);
|
if (Files.exists(jarFile)) Files.move(jarFile, backup, StandardCopyOption.REPLACE_EXISTING);
|
||||||
Files.move(tmp, jarFile);
|
Files.move(tmp, jarFile, StandardCopyOption.REPLACE_EXISTING);
|
||||||
if (Files.exists(backup)) Files.delete(backup);
|
if (Files.exists(backup)) Files.delete(backup);
|
||||||
|
|
||||||
String newVersion = getServerVersion();
|
|
||||||
Files.writeString(baseDir.resolve(VERSION_FILE), newVersion);
|
Files.writeString(baseDir.resolve(VERSION_FILE), newVersion);
|
||||||
log("Обновлено до v" + newVersion);
|
log("Обновлено до v" + newVersion);
|
||||||
|
} else {
|
||||||
|
throw new IOException("Сервер вернул код: " + conn.getResponseCode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void launchUI() throws Exception {
|
private static void launchJFX() throws Exception {
|
||||||
// Запускаем Main с CLI аргументом
|
Path javaBin = findJava();
|
||||||
me.sashegdev.zernmc.launcher.Main.main(new String[]{"--cli"});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void launchGame() throws Exception {
|
|
||||||
// Сначала ищем в lib/jre21-custom/bin/java
|
|
||||||
Path javaBin = baseDir.resolve("lib").resolve("jre21-custom").resolve("bin").resolve("java");
|
|
||||||
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
|
||||||
javaBin = javaBin.resolveSibling("java.exe");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Потом в bin/ (альтернатива)
|
|
||||||
if (!Files.exists(javaBin)) {
|
|
||||||
javaBin = baseDir.resolve("bin").resolve("java");
|
|
||||||
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
|
||||||
javaBin = javaBin.resolveSibling("java.exe");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Системная java как запасной вариант
|
|
||||||
if (!Files.exists(javaBin)) {
|
|
||||||
javaBin = Paths.get(System.getProperty("java.home"), "bin", "java");
|
|
||||||
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
|
||||||
javaBin = javaBin.resolveSibling("java.exe");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Java: " + javaBin);
|
|
||||||
log("baseDir: " + baseDir);
|
|
||||||
log("exists baseDir: " + Files.exists(baseDir));
|
|
||||||
log("baseDir list: " + Arrays.toString(baseDir.toFile().list()));
|
|
||||||
log("exists java: " + Files.exists(javaBin));
|
|
||||||
log("Запуск...");
|
|
||||||
|
|
||||||
Path jarPath = baseDir.resolve(JAR_NAME);
|
Path jarPath = baseDir.resolve(JAR_NAME);
|
||||||
log("jarPath: " + jarPath);
|
|
||||||
log("jarPath exists: " + Files.exists(jarPath));
|
log("Запуск JFX режима...");
|
||||||
log("jarPath abs: " + jarPath.toAbsolutePath());
|
log("Java: " + javaBin);
|
||||||
log("jarPath str: " + jarPath.toAbsolutePath().toString());
|
log("JAR: " + jarPath);
|
||||||
|
|
||||||
ProcessBuilder pb = new ProcessBuilder(
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
javaBin.toAbsolutePath().toString(),
|
javaBin.toAbsolutePath().toString(),
|
||||||
"-jar",
|
"-jar",
|
||||||
jarPath.toAbsolutePath().toString(),
|
jarPath.toAbsolutePath().toString(),
|
||||||
"--launcher"
|
"--jfx"
|
||||||
);
|
);
|
||||||
pb.directory(baseDir.toFile());
|
pb.directory(baseDir.toFile());
|
||||||
pb.inheritIO();
|
pb.inheritIO();
|
||||||
Process p = pb.start();
|
Process p = pb.start();
|
||||||
int code = p.waitFor();
|
int code = p.waitFor();
|
||||||
log("Завершено: " + code);
|
log("Завершено с кодом: " + code);
|
||||||
System.exit(code);
|
System.exit(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void launchCLI() throws Exception {
|
||||||
|
Path javaBin = findJava();
|
||||||
|
Path jarPath = baseDir.resolve(JAR_NAME);
|
||||||
|
|
||||||
|
log("Запуск CLI режима...");
|
||||||
|
log("Java: " + javaBin);
|
||||||
|
log("JAR: " + jarPath);
|
||||||
|
|
||||||
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
|
javaBin.toAbsolutePath().toString(),
|
||||||
|
"-jar",
|
||||||
|
jarPath.toAbsolutePath().toString(),
|
||||||
|
"--cli"
|
||||||
|
);
|
||||||
|
pb.directory(baseDir.toFile());
|
||||||
|
pb.inheritIO();
|
||||||
|
Process p = pb.start();
|
||||||
|
int code = p.waitFor();
|
||||||
|
log("Завершено с кодом: " + code);
|
||||||
|
System.exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path findJava() {
|
||||||
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
String javaExe = os.contains("windows") ? "java.exe" : "java";
|
||||||
|
|
||||||
|
// Сначала ищем jre21/bin/java рядом с лаунчером
|
||||||
|
Path javaBin = baseDir.resolve("jre21").resolve("bin").resolve(javaExe);
|
||||||
|
|
||||||
|
// Если нет, пробуем системную Java
|
||||||
|
if (!Files.exists(javaBin)) {
|
||||||
|
javaBin = Paths.get(System.getProperty("java.home"), "bin", javaExe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если и это не найдено - ищем java в PATH
|
||||||
|
if (!Files.exists(javaBin)) {
|
||||||
|
try {
|
||||||
|
Process p = new ProcessBuilder("which", javaExe).start();
|
||||||
|
if (p.waitFor() == 0) {
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
|
||||||
|
String path = br.readLine();
|
||||||
|
if (path != null) {
|
||||||
|
javaBin = Paths.get(path.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.exists(javaBin)) {
|
||||||
|
throw new RuntimeException("Java не найдена. Убедитесь, что jre21 присутствует в папке с лаунчером или Java установлена в системе");
|
||||||
|
}
|
||||||
|
|
||||||
|
return javaBin;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+43
-94
@@ -4,14 +4,9 @@ import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
|||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
||||||
import me.sashegdev.zernmc.launcher.menu.*;
|
import me.sashegdev.zernmc.launcher.menu.*;
|
||||||
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
|
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
|
||||||
|
import me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher;
|
||||||
import me.sashegdev.zernmc.launcher.utils.*;
|
import me.sashegdev.zernmc.launcher.utils.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
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.nio.file.StandardCopyOption;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class Main {
|
public class Main {
|
||||||
@@ -20,20 +15,60 @@ public class Main {
|
|||||||
private static final LauncherAPI api = new LauncherAPI();
|
private static final LauncherAPI api = new LauncherAPI();
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException {
|
public static void main(String[] args) throws IOException {
|
||||||
|
// Настройка кодировки для Windows и Linux
|
||||||
System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true");
|
System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true");
|
||||||
System.setProperty("file.encoding", "UTF-8");
|
System.setProperty("file.encoding", "UTF-8");
|
||||||
System.setProperty("sun.err.encoding", "UTF-8");
|
System.setProperty("sun.err.encoding", "UTF-8");
|
||||||
System.setProperty("sun.stdout.encoding", "UTF-8");
|
System.setProperty("sun.stdout.encoding", "UTF-8");
|
||||||
|
|
||||||
|
// Для Windows CMD - пытаемся переключить в UTF-8 режим
|
||||||
|
try {
|
||||||
|
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
||||||
|
new ProcessBuilder("cmd", "/c", "chcp", "65001").inheritIO().start().waitFor();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
ZAnsi.install();
|
ZAnsi.install();
|
||||||
System.out.print("\033[H\033[2J");
|
System.out.print("\033[H\033[2J");
|
||||||
System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION));
|
System.out.println(ZAnsi.brightGreen("Добро пожаловать в ZernMC Launcher " + CURRENT_VERSION));
|
||||||
|
|
||||||
|
// Определяем режим запуска
|
||||||
|
List<String> argList = List.of(args);
|
||||||
|
boolean jfxMode = argList.contains("--jfx");
|
||||||
|
boolean cliMode = argList.contains("--cli");
|
||||||
|
|
||||||
|
if (jfxMode) {
|
||||||
|
launchJFX();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI режим (по умолчанию или с --cli)
|
||||||
|
startCLI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void launchJFX() {
|
||||||
|
System.out.println(ZAnsi.cyan("Запуск JFX интерфейса..."));
|
||||||
|
try {
|
||||||
|
// Устанавливаем параметры для JavaFX (важно для Windows)
|
||||||
|
System.setProperty("javafx.runtime.version", "21");
|
||||||
|
|
||||||
|
JFXLauncher.main(new String[]{});
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println(ZAnsi.brightRed("Ошибка запуска JFX: " + e.getMessage()));
|
||||||
|
// Проверяем, связано ли это с отсутствием JavaFX
|
||||||
|
if (e.getMessage() != null && e.getMessage().contains("QuantumRenderer")) {
|
||||||
|
System.err.println(ZAnsi.yellow("JavaFX недоступен. Возможно, отсутствуют нативные библиотеки."));
|
||||||
|
System.err.println(ZAnsi.yellow("Попробуйте использовать CLI режим: --cli"));
|
||||||
|
}
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void startCLI() throws IOException {
|
||||||
// Проверка всех сервисов при старте
|
// Проверка всех сервисов при старте
|
||||||
ZHttpClient.checkAllServicesOnStartup();
|
ZHttpClient.checkAllServicesOnStartup();
|
||||||
|
|
||||||
checkAndAutoUpdateLauncher();
|
|
||||||
|
|
||||||
// === АВТОРИЗАЦИЯ (используем новый API) ===
|
// === АВТОРИЗАЦИЯ (используем новый API) ===
|
||||||
System.out.println(ZAnsi.cyan("Проверка авторизации..."));
|
System.out.println(ZAnsi.cyan("Проверка авторизации..."));
|
||||||
var sessionResponse = api.checkSession();
|
var sessionResponse = api.checkSession();
|
||||||
@@ -62,92 +97,6 @@ public class Main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void checkAndAutoUpdateLauncher() {
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка обновлений лаунчера..."));
|
|
||||||
try {
|
|
||||||
String json = ZHttpClient.getLauncherVersionInfo();
|
|
||||||
String serverVersion = extractVersion(json);
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.white("Текущая версия: ") + CURRENT_VERSION);
|
|
||||||
System.out.println(ZAnsi.white("Версия на сервере: ") + serverVersion);
|
|
||||||
|
|
||||||
if (Version.isNewer(CURRENT_VERSION, serverVersion)) {
|
|
||||||
System.out.println(ZAnsi.brightYellow("\nДоступна новая версия лаунчера! (" + serverVersion + ")"));
|
|
||||||
System.out.println(ZAnsi.cyan("Начинается автоматическое обновление...\n"));
|
|
||||||
performAutoUpdate(serverVersion);
|
|
||||||
restartLauncher();
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightGreen("Лаунчер актуален."));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.yellow("Не удалось проверить обновления лаунчера."));
|
|
||||||
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void performAutoUpdate(String newVersion) throws Exception {
|
|
||||||
String downloadUrl = ZHttpClient.getBaseUrl() + "/launcher/download?type=jar";
|
|
||||||
Path currentJar = getCurrentJarPath();
|
|
||||||
Path tempJar = currentJar.getParent().resolve("zernmc-launcher-new.jar");
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Скачивание версии " + newVersion + "..."));
|
|
||||||
|
|
||||||
HttpClient client = HttpClient.newBuilder().build();
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(java.net.URI.create(downloadUrl))
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(tempJar));
|
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
throw new IOException("Сервер вернул код: " + response.statusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
long size = Files.size(tempJar);
|
|
||||||
System.out.println(ZAnsi.brightGreen("Скачано успешно (" + (size / 1024) + " KB)"));
|
|
||||||
|
|
||||||
Files.move(tempJar, currentJar, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
System.out.println(ZAnsi.brightGreen("Обновление успешно установлено!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void restartLauncher() {
|
|
||||||
try {
|
|
||||||
String javaPath = System.getProperty("java.home") + "/bin/java";
|
|
||||||
String jarPath = getCurrentJarPath().toAbsolutePath().toString();
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("Перезапуск лаунчера с новой версией..."));
|
|
||||||
|
|
||||||
new ProcessBuilder(javaPath, "-jar", jarPath)
|
|
||||||
.inheritIO()
|
|
||||||
.start();
|
|
||||||
|
|
||||||
System.exit(0);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println(ZAnsi.brightRed("Не удалось перезапустить лаунчер."));
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractVersion(String json) {
|
|
||||||
try {
|
|
||||||
return json.replaceAll(".*\"version\"\\s*:\\s*\"([^\"]+)\".*", "$1");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path getCurrentJarPath() {
|
|
||||||
try {
|
|
||||||
return Path.of(Main.class.getProtectionDomain()
|
|
||||||
.getCodeSource()
|
|
||||||
.getLocation()
|
|
||||||
.toURI());
|
|
||||||
} catch (Exception e) {
|
|
||||||
return Path.of("zernmc-launcher-1.0-jar-with-dependencies.jar");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ГЛАВНЫЙ ЦИКЛ ======================
|
// ====================== ГЛАВНЫЙ ЦИКЛ ======================
|
||||||
private static void mainLoop() throws Exception {
|
private static void mainLoop() throws Exception {
|
||||||
if (Config.isZernMCBuild()) {
|
if (Config.isZernMCBuild()) {
|
||||||
+22
-4
@@ -34,8 +34,9 @@ public class Version {
|
|||||||
public static boolean isNewer(String current, String server) {
|
public static boolean isNewer(String current, String server) {
|
||||||
if (current == null || server == null) return false;
|
if (current == null || server == null) return false;
|
||||||
|
|
||||||
current = current.replace("-SNAPSHOT", "").trim();
|
// Нормализуем версии - убираем суффиксы типа -any, -alpha, -beta, -SNAPSHOT
|
||||||
server = server.replace("-SNAPSHOT", "").trim();
|
current = normalizeVersion(current);
|
||||||
|
server = normalizeVersion(server);
|
||||||
|
|
||||||
if (current.equals(server)) return false;
|
if (current.equals(server)) return false;
|
||||||
|
|
||||||
@@ -45,12 +46,29 @@ public class Version {
|
|||||||
int max = Math.max(cParts.length, sParts.length);
|
int max = Math.max(cParts.length, sParts.length);
|
||||||
|
|
||||||
for (int i = 0; i < max; i++) {
|
for (int i = 0; i < max; i++) {
|
||||||
int c = i < cParts.length ? Integer.parseInt(cParts[i]) : 0;
|
int c = i < cParts.length ? parseVersionPart(cParts[i]) : 0;
|
||||||
int s = i < sParts.length ? Integer.parseInt(sParts[i]) : 0;
|
int s = i < sParts.length ? parseVersionPart(sParts[i]) : 0;
|
||||||
|
|
||||||
if (s > c) return true;
|
if (s > c) return true;
|
||||||
if (s < c) return false;
|
if (s < c) return false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String normalizeVersion(String version) {
|
||||||
|
if (version == null) return "0.0.0";
|
||||||
|
|
||||||
|
// Убираем суффиксы: -any, -alpha1, -beta2, -SNAPSHOT, -rc1 и т.д.
|
||||||
|
return version.split("-")[0].split("\\+")[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseVersionPart(String part) {
|
||||||
|
try {
|
||||||
|
// Убираем всё, что не является цифрой (на случай если суффикс остался)
|
||||||
|
String numeric = part.replaceAll("[^0-9]", "");
|
||||||
|
return numeric.isEmpty() ? 0 : Integer.parseInt(numeric);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>ZernMC Launcher</title>
|
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Grid Background -->
|
|
||||||
<canvas id="grid-canvas"></canvas>
|
|
||||||
|
|
||||||
<div id="app">
|
|
||||||
<!-- Экран логина -->
|
|
||||||
<div id="login-screen" class="screen">
|
|
||||||
<div class="login-container">
|
|
||||||
<h1 class="logo">ZernMC</h1>
|
|
||||||
<p class="subtitle">Private Launcher</p>
|
|
||||||
<form id="login-form">
|
|
||||||
<input type="text" id="username" placeholder="Никнейм" required>
|
|
||||||
<input type="password" id="password" placeholder="Пароль" required>
|
|
||||||
<button type="submit" class="btn-primary">Войти</button>
|
|
||||||
</form>
|
|
||||||
<div id="login-error" class="error hidden"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Главное меню -->
|
|
||||||
<div id="main-screen" class="screen hidden">
|
|
||||||
<!-- Хедер -->
|
|
||||||
<header class="header">
|
|
||||||
<h1 class="logo">ZernMC Launcher</h1>
|
|
||||||
<div class="account-info">
|
|
||||||
<span id="account-name">-</span>
|
|
||||||
<span id="account-status" class="badge">-</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Основной контент -->
|
|
||||||
<main class="main-content">
|
|
||||||
<!-- Слева: выбор сборки -->
|
|
||||||
<aside class="sidebar">
|
|
||||||
<h2>Сборки</h2>
|
|
||||||
<div id="instances-list" class="instances-container">
|
|
||||||
<!-- Динамически заполняется через JS -->
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- По центру: логи -->
|
|
||||||
<section class="logs-panel">
|
|
||||||
<h2>Логи</h2>
|
|
||||||
<div id="logs-container"></div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Низ: управление -->
|
|
||||||
<footer class="footer">
|
|
||||||
<div class="instance-info">
|
|
||||||
<span id="selected-name">-</span>
|
|
||||||
<span id="selected-version">-</span>
|
|
||||||
<span id="selected-loader">-</span>
|
|
||||||
</div>
|
|
||||||
<button id="play-btn" class="btn-play" disabled>Выберите сборку</button>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Модальное окно установки -->
|
|
||||||
<div id="install-modal" class="modal hidden">
|
|
||||||
<div class="modal-content">
|
|
||||||
<h2>Установка сборки</h2>
|
|
||||||
<form id="install-form">
|
|
||||||
<label>Версия Minecraft
|
|
||||||
<select id="install-mc-version">
|
|
||||||
<option value="1.20.4">1.20.4</option>
|
|
||||||
<option value="1.20.2">1.20.2</option>
|
|
||||||
<option value="1.20.1">1.20.1</option>
|
|
||||||
<option value="1.19.2">1.19.2</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Загрузчик
|
|
||||||
<select id="install-loader">
|
|
||||||
<option value="vanilla">Vanilla</option>
|
|
||||||
<option value="fabric">Fabric</option>
|
|
||||||
<option value="forge">Forge</option>
|
|
||||||
<option value="neoforge">NeoForge</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>Имя сборки
|
|
||||||
<input type="text" id="install-name" placeholder="MyServer" required>
|
|
||||||
</label>
|
|
||||||
<div class="modal-buttons">
|
|
||||||
<button type="button" class="btn-secondary" onclick="closeInstallModal()">Отмена</button>
|
|
||||||
<button type="submit" class="btn-primary">Установить</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="launcher.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
const API_BASE = 'http://localhost:8080/api';
|
|
||||||
|
|
||||||
let state = {
|
|
||||||
loggedIn: false,
|
|
||||||
account: null,
|
|
||||||
instances: [],
|
|
||||||
selectedInstance: null
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============ API ============
|
|
||||||
|
|
||||||
async function apiCall(endpoint, options = {}) {
|
|
||||||
const url = `${API_BASE}${endpoint}`;
|
|
||||||
const config = {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, config);
|
|
||||||
const data = await response.json();
|
|
||||||
return data;
|
|
||||||
} catch (e) {
|
|
||||||
log('Ошибка соединения с сервером: ' + e.message, 'error');
|
|
||||||
return { success: false, error: e.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Login ============
|
|
||||||
|
|
||||||
async function login(username, password) {
|
|
||||||
log('Выполняется вход...', 'info');
|
|
||||||
const result = await apiCall('/login', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ username, password })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
state.loggedIn = true;
|
|
||||||
state.account = result.data;
|
|
||||||
log('Вход выполнен: ' + result.data.username, 'success');
|
|
||||||
showMainScreen();
|
|
||||||
await loadInstances();
|
|
||||||
} else {
|
|
||||||
log('Ошибка входа: ' + result.error, 'error');
|
|
||||||
showError(result.error);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message) {
|
|
||||||
const el = document.getElementById('login-error');
|
|
||||||
el.textContent = message;
|
|
||||||
el.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideError() {
|
|
||||||
document.getElementById('login-error').classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Account ============
|
|
||||||
|
|
||||||
async function loadAccountInfo() {
|
|
||||||
const result = await apiCall('/account');
|
|
||||||
if (result.success) {
|
|
||||||
state.account = result.data;
|
|
||||||
state.loggedIn = true;
|
|
||||||
document.getElementById('account-name').textContent = result.data.username;
|
|
||||||
|
|
||||||
const statusEl = document.getElementById('account-status');
|
|
||||||
statusEl.textContent = result.data.passActive ? 'PRO' : 'FREE';
|
|
||||||
statusEl.className = 'badge ' + (result.data.passActive ? 'active' : 'inactive');
|
|
||||||
} else {
|
|
||||||
showLoginScreen();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Instances ============
|
|
||||||
|
|
||||||
async function loadInstances() {
|
|
||||||
log('Загрузка списка сборок...', 'info');
|
|
||||||
const result = await apiCall('/instances');
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
state.instances = result.data;
|
|
||||||
renderInstances();
|
|
||||||
log('Загружено ' + result.data.length + ' сборок', 'success');
|
|
||||||
} else {
|
|
||||||
log('Ошибка загрузки: ' + result.error, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderInstances() {
|
|
||||||
const container = document.getElementById('instances-list');
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
state.instances.forEach(inst => {
|
|
||||||
const card = document.createElement('div');
|
|
||||||
card.className = 'instance-card';
|
|
||||||
card.dataset.name = inst.name;
|
|
||||||
card.onclick = () => selectInstance(inst.name);
|
|
||||||
|
|
||||||
let details = `
|
|
||||||
<span class="instance-version">${inst.version || '?'}</span>
|
|
||||||
<span class="instance-loader">${inst.loaderType || 'vanilla'}</span>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (inst.isServerPack) {
|
|
||||||
details += `<span class="instance-server-version">v${inst.serverVersion}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="instance-name">${inst.name}</div>
|
|
||||||
<div class="instance-details">${details}</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
container.appendChild(card);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectInstance(name) {
|
|
||||||
state.selectedInstance = state.instances.find(i => i.name === name);
|
|
||||||
|
|
||||||
document.querySelectorAll('.instance-card').forEach(c => {
|
|
||||||
c.classList.toggle('selected', c.dataset.name === name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const btn = document.getElementById('play-btn');
|
|
||||||
const inst = state.selectedInstance;
|
|
||||||
|
|
||||||
if (inst) {
|
|
||||||
document.getElementById('selected-name').textContent = inst.name;
|
|
||||||
document.getElementById('selected-version').textContent = inst.version || '-';
|
|
||||||
document.getElementById('selected-loader').textContent = inst.loaderType || 'vanilla';
|
|
||||||
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Играть';
|
|
||||||
btn.classList.remove('update');
|
|
||||||
|
|
||||||
loadInstanceLogs(inst.name);
|
|
||||||
} else {
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Выберите сборку';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Launch ============
|
|
||||||
|
|
||||||
async function launchInstance() {
|
|
||||||
if (!state.selectedInstance) return;
|
|
||||||
|
|
||||||
const name = state.selectedInstance.name;
|
|
||||||
log('Запуск сборки: ' + name, 'info');
|
|
||||||
|
|
||||||
const result = await apiCall('/launch', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ name })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
log('Сборка запущена! PID: ' + result.data.pid, 'success');
|
|
||||||
} else {
|
|
||||||
log('Ошибка запуска: ' + result.error, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Install ============
|
|
||||||
|
|
||||||
function openInstallModal() {
|
|
||||||
document.getElementById('install-modal').classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeInstallModal() {
|
|
||||||
document.getElementById('install-modal').classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function installInstance(formData) {
|
|
||||||
log('Установка сборки...', 'info');
|
|
||||||
const result = await apiCall('/install', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(formData)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
log('Сборка установлена!', 'success');
|
|
||||||
closeInstallModal();
|
|
||||||
await loadInstances();
|
|
||||||
} else {
|
|
||||||
log('Ошибка установки: ' + result.error, 'error');
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Logs ============
|
|
||||||
|
|
||||||
function log(message, type = 'info') {
|
|
||||||
const container = document.getElementById('logs-container');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const line = document.createElement('div');
|
|
||||||
line.className = 'log-line ' + type;
|
|
||||||
line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + message;
|
|
||||||
container.appendChild(line);
|
|
||||||
container.scrollTop = container.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadInstanceLogs(instanceName) {
|
|
||||||
const result = await apiCall('/logs/instance?name=' + encodeURIComponent(instanceName));
|
|
||||||
if (result.success && result.data) {
|
|
||||||
result.data.split('\n').forEach(line => {
|
|
||||||
if (line.trim()) {
|
|
||||||
let type = 'info';
|
|
||||||
if (line.toLowerCase().includes('error')) type = 'error';
|
|
||||||
else if (line.toLowerCase().includes('warn')) type = 'warning';
|
|
||||||
else if (line.toLowerCase().includes('info')) type = 'success';
|
|
||||||
log(line, type);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearLogs() {
|
|
||||||
document.getElementById('logs-container').innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Screens ============
|
|
||||||
|
|
||||||
function showLoginScreen() {
|
|
||||||
document.getElementById('login-screen').classList.remove('hidden');
|
|
||||||
document.getElementById('main-screen').classList.add('hidden');
|
|
||||||
clearError();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMainScreen() {
|
|
||||||
document.getElementById('login-screen').classList.add('hidden');
|
|
||||||
document.getElementById('main-screen').classList.remove('hidden');
|
|
||||||
|
|
||||||
if (state.account) {
|
|
||||||
document.getElementById('account-name').textContent = state.account.username;
|
|
||||||
const statusEl = document.getElementById('account-status');
|
|
||||||
statusEl.textContent = state.account.passActive ? 'PRO' : 'FREE';
|
|
||||||
statusEl.className = 'badge ' + (state.account.passActive ? 'active' : 'inactive');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============ Init ============
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
initGridBackground();
|
|
||||||
log('Запуск лаунчера...', 'info');
|
|
||||||
|
|
||||||
await loadAccountInfo();
|
|
||||||
|
|
||||||
if (!state.loggedIn) {
|
|
||||||
showLoginScreen();
|
|
||||||
} else {
|
|
||||||
showMainScreen();
|
|
||||||
await loadInstances();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============ Form Handlers ============
|
|
||||||
|
|
||||||
document.getElementById('login-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
hideError();
|
|
||||||
|
|
||||||
const username = document.getElementById('username').value;
|
|
||||||
const password = document.getElementById('password').value;
|
|
||||||
|
|
||||||
await login(username, password);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('play-btn').addEventListener('click', async () => {
|
|
||||||
await launchInstance();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('install-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const formData = {
|
|
||||||
name: document.getElementById('install-name').value,
|
|
||||||
version: document.getElementById('install-mc-version').value,
|
|
||||||
loader: document.getElementById('install-loader').value
|
|
||||||
};
|
|
||||||
|
|
||||||
await installInstance(formData);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expose functions globally for inline handlers
|
|
||||||
window.closeInstallModal = closeInstallModal;
|
|
||||||
|
|
||||||
// ============ Grid Background ============
|
|
||||||
|
|
||||||
function initGridBackground() {
|
|
||||||
const canvas = document.getElementById('grid-canvas');
|
|
||||||
if (!canvas) return;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
let width, height;
|
|
||||||
let mouseX = 0, mouseY = 0;
|
|
||||||
let points = [];
|
|
||||||
const spacing = 40;
|
|
||||||
const depthLayers = 3;
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
width = canvas.width = window.innerWidth;
|
|
||||||
height = canvas.height = window.innerHeight;
|
|
||||||
initPoints();
|
|
||||||
}
|
|
||||||
|
|
||||||
function initPoints() {
|
|
||||||
points = [];
|
|
||||||
for (let z = 0; z < depthLayers; z++) {
|
|
||||||
const layer = [];
|
|
||||||
const scale = 0.6 + z * 0.2;
|
|
||||||
const cols = Math.ceil(width / spacing / scale) + 1;
|
|
||||||
const rows = Math.ceil(height / spacing / scale) + 1;
|
|
||||||
|
|
||||||
for (let y = 0; y < rows; y++) {
|
|
||||||
for (let x = 0; x < cols; x++) {
|
|
||||||
layer.push({
|
|
||||||
x: x * spacing * scale,
|
|
||||||
y: y * spacing * scale,
|
|
||||||
baseX: x * spacing * scale,
|
|
||||||
baseY: y * spacing * scale,
|
|
||||||
depth: z
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
points.push(layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function draw() {
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
|
|
||||||
const centerX = width / 2;
|
|
||||||
const centerY = height / 2;
|
|
||||||
|
|
||||||
for (let z = 0; z < depthLayers; z++) {
|
|
||||||
const layer = points[z];
|
|
||||||
const alpha = 0.15 + z * 0.1;
|
|
||||||
const scale = 0.6 + z * 0.2;
|
|
||||||
|
|
||||||
ctx.strokeStyle = z === depthLayers - 1
|
|
||||||
? `rgba(59, 130, 246, ${alpha})` // Blue - primary
|
|
||||||
: `rgba(245, 158, 11, ${alpha * 0.5})`; // Orange - secondary
|
|
||||||
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
|
|
||||||
for (const p of layer) {
|
|
||||||
const dx = (mouseX - centerX) * 0.02 * scale * (z + 1);
|
|
||||||
const dy = (mouseY - centerY) * 0.02 * scale * (z + 1);
|
|
||||||
|
|
||||||
const px = p.baseX + dx;
|
|
||||||
const py = p.baseY + dy;
|
|
||||||
|
|
||||||
// Horizontal line
|
|
||||||
if ((p.baseX + dx) > 0 && (p.baseX + dx) < width - spacing * scale) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(px, py);
|
|
||||||
ctx.lineTo(px + spacing * scale, py);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical line
|
|
||||||
if ((p.baseY + dy) > 0 && (p.baseY + dy) < height - spacing * scale) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(px, py);
|
|
||||||
ctx.lineTo(px, py + spacing * scale);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(draw);
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', (e) => {
|
|
||||||
mouseX = e.clientX;
|
|
||||||
mouseY = e.clientY;
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.addEventListener('mouseleave', () => {
|
|
||||||
mouseX = width / 2;
|
|
||||||
mouseY = height / 2;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
|
||||||
resize();
|
|
||||||
draw();
|
|
||||||
}
|
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
:root {
|
|
||||||
--bg-primary: #0f172a;
|
|
||||||
--bg-secondary: #1e293b;
|
|
||||||
--bg-tertiary: #334155;
|
|
||||||
--accent: #f59e0b;
|
|
||||||
--accent-secondary: #3b82f6;
|
|
||||||
--accent-hover: #fbbf24;
|
|
||||||
--text-primary: #f1f5f9;
|
|
||||||
--text-secondary: #94a3b8;
|
|
||||||
--success: #22c55e;
|
|
||||||
--warning: #f59e0b;
|
|
||||||
--error: #ef4444;
|
|
||||||
--border: #475569;
|
|
||||||
--shadow: rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#grid-canvas {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Screens */
|
|
||||||
.screen {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Login Screen */
|
|
||||||
#login-screen {
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
|
||||||
background: rgba(30, 41, 59, 0.95);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
padding: 3rem;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 25px 50px var(--shadow);
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--accent);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
input, select {
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
color: var(--text-primary);
|
|
||||||
padding: 0.875rem 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus, select:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
input::placeholder {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--accent);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.875rem 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: var(--accent-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:disabled {
|
|
||||||
background: var(--text-secondary);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 0.875rem 1rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
color: var(--error);
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: rgba(239, 68, 68, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Screen */
|
|
||||||
#main-screen {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.header {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .logo {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#account-name {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge.active {
|
|
||||||
background: rgba(74, 222, 128, 0.2);
|
|
||||||
color: var(--success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge.inactive {
|
|
||||||
background: rgba(239, 68, 68, 0.2);
|
|
||||||
color: var(--error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Content */
|
|
||||||
.main-content {
|
|
||||||
flex: 1;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 280px 1fr;
|
|
||||||
gap: 1px;
|
|
||||||
background: var(--border);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
.sidebar {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
padding: 1rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar h2 {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instances-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-card {
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-card:hover {
|
|
||||||
border-color: var(--accent);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-card.selected {
|
|
||||||
border-color: var(--accent);
|
|
||||||
background: rgba(233, 69, 96, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-name {
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-details {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-version, .instance-loader {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-server-version {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: rgba(251, 191, 36, 0.2);
|
|
||||||
color: var(--warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logs Panel */
|
|
||||||
.logs-panel {
|
|
||||||
background: var(--bg-primary);
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logs-panel h2 {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#logs-container {
|
|
||||||
flex: 1;
|
|
||||||
background: #0d0d1a;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1rem;
|
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-line {
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.log-line.info { color: var(--text-primary); }
|
|
||||||
.log-line.success { color: var(--success); }
|
|
||||||
.log-line.warning { color: var(--warning); }
|
|
||||||
.log-line.error { color: var(--error); }
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.footer {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-info {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instance-info span {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#selected-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-play {
|
|
||||||
background: var(--success);
|
|
||||||
color: #0a0a0a;
|
|
||||||
border: none;
|
|
||||||
padding: 0.875rem 2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-play:hover:not(:disabled) {
|
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 0 20px rgba(74, 222, 128, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-play:disabled {
|
|
||||||
background: var(--text-secondary);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-play.update {
|
|
||||||
background: var(--warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Modal */
|
|
||||||
.modal {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.8);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 16px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 450px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content h2 {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#install-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#install-form label {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
#install-form select, #install-form input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: var(--bg-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--bg-tertiary);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: var(--border);
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
class VersionTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testVersionParsing() {
|
|
||||||
assertEquals("1.0.8", parseVersion("{\"version\":\"1.0.8\"}"));
|
|
||||||
assertEquals("1.0.2", parseVersion("version:1.0.2"));
|
|
||||||
assertEquals("unknown", parseVersion("invalid"));
|
|
||||||
assertEquals("2.0.0", parseVersion("\"version\":\"2.0.0\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testVersionComparison() {
|
|
||||||
assertTrue(isNewer("1.0.8", "1.0.7"));
|
|
||||||
assertTrue(isNewer("1.0.8", "1.0.2"));
|
|
||||||
assertTrue(isNewer("2.0.0", "1.0.8"));
|
|
||||||
assertFalse(isNewer("1.0.8", "1.0.8"));
|
|
||||||
assertFalse(isNewer("1.0.7", "1.0.8"));
|
|
||||||
assertTrue(isNewer("1.0.10", "1.0.9"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String parseVersion(String line) {
|
|
||||||
if (line != null && line.contains("version")) {
|
|
||||||
int start = line.indexOf("\"version\":\"");
|
|
||||||
if (start >= 0) {
|
|
||||||
start += 11;
|
|
||||||
int end = line.indexOf("\"", start);
|
|
||||||
if (end > start) {
|
|
||||||
return line.substring(start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isNewer(String server, String current) {
|
|
||||||
try {
|
|
||||||
String[] sa = server.split("\\.");
|
|
||||||
String[] ca = current.split("\\.");
|
|
||||||
for (int i = 0; i < Math.min(sa.length, ca.length); i++) {
|
|
||||||
int sv = Integer.parseInt(sa[i]);
|
|
||||||
int cv = Integer.parseInt(ca[i]);
|
|
||||||
if (sv > cv) return true;
|
|
||||||
if (sv < cv) return false;
|
|
||||||
}
|
|
||||||
return sa.length > ca.length;
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+43
-75
@@ -7,7 +7,15 @@
|
|||||||
<groupId>me.sashegdev</groupId>
|
<groupId>me.sashegdev</groupId>
|
||||||
<artifactId>ZernMCLauncher</artifactId>
|
<artifactId>ZernMCLauncher</artifactId>
|
||||||
<version>1.0.8</version>
|
<version>1.0.8</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<name>ZernMC Launcher Parent</name>
|
||||||
|
<description>ZernMC Launcher - Multi-module project</description>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>bootstrap</module>
|
||||||
|
<module>launcher</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
@@ -15,10 +23,10 @@
|
|||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.organization.name>ZernMC</project.organization.name>
|
<project.organization.name>ZernMC</project.organization.name>
|
||||||
<project.inceptionYear>2026</project.inceptionYear>
|
<project.inceptionYear>2026</project.inceptionYear>
|
||||||
<project.description>ZernMC Launcher - just a minimalistic launcher by SashegDev</project.description>
|
<project.description>ZernMC Launcher - Multi-module project</project.description>
|
||||||
<mainClass>me.sashegdev.zernmc.launcher.Main</mainClass>
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.httpcomponents</groupId>
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
@@ -60,13 +68,39 @@
|
|||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
<version>2.15.1</version>
|
<version>2.15.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- JavaFX for Windows -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>javafx-controls</artifactId>
|
||||||
<version>5.10.1</version>
|
<version>21</version>
|
||||||
<scope>test</scope>
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-web</artifactId>
|
||||||
|
<version>21</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-graphics</artifactId>
|
||||||
|
<version>21</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-base</artifactId>
|
||||||
|
<version>21</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-media</artifactId>
|
||||||
|
<version>21</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
@@ -88,7 +122,7 @@
|
|||||||
<goal>shade</goal>
|
<goal>shade</goal>
|
||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<outputFile>../server/builds/ZernMCLauncher.jar</outputFile>
|
<outputFile>../../server/builds/ZernMCLauncher.jar</outputFile>
|
||||||
<transformers>
|
<transformers>
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
<mainClass>${mainClass}</mainClass>
|
<mainClass>${mainClass}</mainClass>
|
||||||
@@ -103,73 +137,7 @@
|
|||||||
</transformers>
|
</transformers>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
|
||||||
|
|
||||||
<!-- Launch4j для создания .exe -->
|
|
||||||
<plugin>
|
|
||||||
<groupId>com.akathist.maven.plugins.launch4j</groupId>
|
|
||||||
<artifactId>launch4j-maven-plugin</artifactId>
|
|
||||||
<version>2.5.0</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>l4j</id>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals>
|
|
||||||
<goal>launch4j</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<outfile>../server/builds/ZernMCLauncher-${project.version}.exe</outfile>
|
|
||||||
<jar>../server/builds/ZernMCLauncher.jar</jar>
|
|
||||||
<headerType>console</headerType>
|
|
||||||
<dontWrapJar>false</dontWrapJar>
|
|
||||||
<jre>
|
|
||||||
<path>jre21</path>
|
|
||||||
<minVersion>21</minVersion>
|
|
||||||
</jre>
|
|
||||||
<versionInfo>
|
|
||||||
<fileVersion>${project.version}.0</fileVersion>
|
|
||||||
<txtFileVersion>${project.version}</txtFileVersion>
|
|
||||||
<fileDescription>ZernMC Launcher — just a Minecraft launcher</fileDescription>
|
|
||||||
<productVersion>${project.version}.0</productVersion>
|
|
||||||
<txtProductVersion>${project.version}</txtProductVersion>
|
|
||||||
<productName>ZernMC Launcher</productName>
|
|
||||||
<companyName>ZernMC(SashegDev)</companyName>
|
|
||||||
<internalName>ZernMCLauncher</internalName>
|
|
||||||
<originalFilename>ZernMCLauncher-${project.version}.exe</originalFilename>
|
|
||||||
</versionInfo>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
|
|
||||||
<!-- Antrun: копирование JRE и создание build.version + zip -->
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-antrun-plugin</artifactId>
|
|
||||||
<version>3.1.0</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals><goal>run</goal></goals>
|
|
||||||
<configuration>
|
|
||||||
<target>
|
|
||||||
<echo file="../server/builds/build.version">${project.version}</echo>
|
|
||||||
|
|
||||||
<!-- Копируем содержимое jre/jre21 в папку jre21 (без лишней вложенности) -->
|
|
||||||
<copy todir="../server/builds/jre21" overwrite="true">
|
|
||||||
<fileset dir="${user.home}/launcher/jre/jre21"/>
|
|
||||||
</copy>
|
|
||||||
|
|
||||||
<!-- Создаём zip только с .exe и jre21 (без .jar и build.version) -->
|
|
||||||
<zip destfile="../server/builds/ZernMCLauncher-${project.version}.zip"
|
|
||||||
basedir="../server/builds"
|
|
||||||
includes="ZernMCLauncher.exe,jre21/**"
|
|
||||||
excludes="*.jar,build.version"/>
|
|
||||||
</target>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.api;
|
|
||||||
|
|
||||||
public class ApiResponse<T> {
|
|
||||||
private boolean success;
|
|
||||||
private T data;
|
|
||||||
private String error;
|
|
||||||
|
|
||||||
public ApiResponse(boolean success, T data, String error) {
|
|
||||||
this.success = success;
|
|
||||||
this.data = data;
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> ApiResponse<T> success(T data) {
|
|
||||||
return new ApiResponse<>(true, data, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> ApiResponse<T> error(String error) {
|
|
||||||
return new ApiResponse<>(false, null, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSuccess() {
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
public T getData() {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getError() {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.api;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.api.auth.AuthService;
|
|
||||||
import me.sashegdev.zernmc.launcher.api.instance.InstanceService;
|
|
||||||
import me.sashegdev.zernmc.launcher.api.launch.LaunchService;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Центральный фасад для внутреннего API лаунчера.
|
|
||||||
* Используется как единая точка входа для UI и других компонентов.
|
|
||||||
*/
|
|
||||||
public class LauncherAPI {
|
|
||||||
|
|
||||||
private final AuthService authService;
|
|
||||||
private final InstanceService instanceService;
|
|
||||||
private final LaunchService launchService;
|
|
||||||
|
|
||||||
public LauncherAPI() {
|
|
||||||
this.authService = new AuthService();
|
|
||||||
this.instanceService = new InstanceService();
|
|
||||||
this.launchService = new LaunchService();
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthService auth() {
|
|
||||||
return authService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InstanceService instances() {
|
|
||||||
return instanceService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LaunchService launch() {
|
|
||||||
return launchService;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== Удобные методы ======================
|
|
||||||
|
|
||||||
public boolean isLoggedIn() {
|
|
||||||
return authService.isLoggedIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCurrentUsername() {
|
|
||||||
return authService.getCurrentUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<AuthService.SessionInfo> checkSession() {
|
|
||||||
return authService.checkSession();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<AuthService.LoginResult> login(String username, String password) {
|
|
||||||
return authService.login(username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<Boolean> logout() {
|
|
||||||
return authService.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<List<InstanceService.InstanceInfo>> getAllInstances() {
|
|
||||||
return instanceService.getAllInstances();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<LaunchService.InstanceInfo> getLaunchInfo(String instanceName) {
|
|
||||||
return launchService.getLaunchInfo(instanceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<LaunchService.LaunchInfo> prepareLaunch(String instanceName) {
|
|
||||||
return launchService.prepareLaunch(instanceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<LaunchService.ProcessInfo> launch(String instanceName) {
|
|
||||||
return launchService.launch(instanceName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.api.auth;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.api.ApiResponse;
|
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class AuthService {
|
|
||||||
|
|
||||||
public ApiResponse<LoginResult> login(String username, String password) {
|
|
||||||
try {
|
|
||||||
AuthManager.AuthResult result = AuthManager.login(username, password);
|
|
||||||
if (result.success) {
|
|
||||||
LoginResult loginResult = new LoginResult(AuthManager.getUsername(), AuthManager.getAccessToken());
|
|
||||||
return ApiResponse.success(loginResult);
|
|
||||||
}
|
|
||||||
return ApiResponse.error(result.error != null ? result.error : "Неверный логин или пароль");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка авторизации: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<Boolean> logout() {
|
|
||||||
try {
|
|
||||||
AuthManager.logout();
|
|
||||||
return ApiResponse.success(true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка при выходе: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<SessionInfo> checkSession() {
|
|
||||||
try {
|
|
||||||
boolean restored = AuthManager.loadSavedSession();
|
|
||||||
if (restored) {
|
|
||||||
SessionInfo info = new SessionInfo(
|
|
||||||
AuthManager.getUsername(),
|
|
||||||
AuthManager.getAccessToken(),
|
|
||||||
AuthManager.hasActivePass()
|
|
||||||
);
|
|
||||||
return ApiResponse.success(info);
|
|
||||||
}
|
|
||||||
return ApiResponse.error("Сессия не найдена");
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка проверки сессии: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<Boolean> activatePass(String passCode) {
|
|
||||||
try {
|
|
||||||
String response = post("/auth/pass/activate",
|
|
||||||
"{\"code\":\"" + passCode + "\"}");
|
|
||||||
return ApiResponse.success(true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка активации проходки: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String post(String endpoint, String jsonBody) throws Exception {
|
|
||||||
String fullUrl = ZHttpClient.getBaseUrl() + endpoint;
|
|
||||||
java.net.URL url = new java.net.URL(fullUrl);
|
|
||||||
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
|
|
||||||
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
conn.setRequestProperty("User-Agent", "ZernMC-Launcher/1.0");
|
|
||||||
|
|
||||||
if (AuthManager.getAccessToken() != null && !AuthManager.getAccessToken().equals("0")) {
|
|
||||||
conn.setRequestProperty("Authorization", "Bearer " + AuthManager.getAccessToken());
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
|
|
||||||
try (var os = conn.getOutputStream()) {
|
|
||||||
byte[] input = jsonBody.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
|
||||||
os.write(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
int statusCode = conn.getResponseCode();
|
|
||||||
var is = (statusCode >= 200 && statusCode < 300) ? conn.getInputStream() : conn.getErrorStream();
|
|
||||||
|
|
||||||
String responseBody;
|
|
||||||
try (var scanner = new java.util.Scanner(is, java.nio.charset.StandardCharsets.UTF_8.name())) {
|
|
||||||
responseBody = scanner.useDelimiter("\\A").hasNext() ? scanner.next() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.disconnect();
|
|
||||||
|
|
||||||
if (statusCode != 200) {
|
|
||||||
throw new IOException("HTTP " + statusCode + ": " + responseBody);
|
|
||||||
}
|
|
||||||
|
|
||||||
return responseBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isLoggedIn() {
|
|
||||||
return AuthManager.isLoggedIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCurrentUsername() {
|
|
||||||
return AuthManager.getUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LoginResult {
|
|
||||||
private String username;
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
public LoginResult(String username, String token) {
|
|
||||||
this.username = username;
|
|
||||||
this.token = token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUsername() { return username; }
|
|
||||||
public String getToken() { return token; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SessionInfo {
|
|
||||||
private String username;
|
|
||||||
private String token;
|
|
||||||
private boolean passActive;
|
|
||||||
|
|
||||||
public SessionInfo(String username, String token, boolean passActive) {
|
|
||||||
this.username = username;
|
|
||||||
this.token = token;
|
|
||||||
this.passActive = passActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUsername() { return username; }
|
|
||||||
public String getToken() { return token; }
|
|
||||||
public boolean isPassActive() { return passActive; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.api.instance;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.api.ApiResponse;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class InstanceService {
|
|
||||||
|
|
||||||
public ApiResponse<List<InstanceInfo>> getAllInstances() {
|
|
||||||
try {
|
|
||||||
List<Instance> instances = InstanceManager.getAllInstances();
|
|
||||||
List<InstanceInfo> infoList = instances.stream()
|
|
||||||
.map(this::toInstanceInfo)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
return ApiResponse.success(infoList);
|
|
||||||
} catch (IOException e) {
|
|
||||||
return ApiResponse.error("Ошибка получения списка сборок: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<InstanceInfo> getInstance(String name) {
|
|
||||||
try {
|
|
||||||
Instance instance = InstanceManager.getInstance(name);
|
|
||||||
if (instance == null) {
|
|
||||||
return ApiResponse.error("Сборка не найдена: " + name);
|
|
||||||
}
|
|
||||||
return ApiResponse.success(toInstanceInfo(instance));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка получения сборки: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<InstanceInfo> createInstance(String name) {
|
|
||||||
try {
|
|
||||||
boolean created = InstanceManager.createInstanceFolder(name);
|
|
||||||
if (!created) {
|
|
||||||
return ApiResponse.error("Сборка с таким именем уже существует: " + name);
|
|
||||||
}
|
|
||||||
Instance instance = InstanceManager.getInstance(name);
|
|
||||||
return ApiResponse.success(toInstanceInfo(instance));
|
|
||||||
} catch (IOException e) {
|
|
||||||
return ApiResponse.error("Ошибка создания сборки: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<Boolean> deleteInstance(String name) {
|
|
||||||
try {
|
|
||||||
boolean deleted = InstanceManager.deleteInstance(name);
|
|
||||||
if (!deleted) {
|
|
||||||
return ApiResponse.error("Не удалось удалить сборку: " + name);
|
|
||||||
}
|
|
||||||
return ApiResponse.success(true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка удаления сборки: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<Boolean> isInstanceExists(String name) {
|
|
||||||
try {
|
|
||||||
Instance instance = InstanceManager.getInstance(name);
|
|
||||||
return ApiResponse.success(instance != null);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка проверки сборки: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private InstanceInfo toInstanceInfo(Instance instance) {
|
|
||||||
return new InstanceInfo(
|
|
||||||
instance.getName(),
|
|
||||||
instance.getPath().toString(),
|
|
||||||
instance.getMinecraftVersion(),
|
|
||||||
instance.getLoaderType()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class InstanceInfo {
|
|
||||||
private String name;
|
|
||||||
private String path;
|
|
||||||
private String version;
|
|
||||||
private String loaderType;
|
|
||||||
|
|
||||||
public InstanceInfo(String name, String path, String version, String loaderType) {
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
this.version = version;
|
|
||||||
this.loaderType = loaderType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() { return name; }
|
|
||||||
public String getPath() { return path; }
|
|
||||||
public String getVersion() { return version; }
|
|
||||||
public String getLoaderType() { return loaderType; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.api.launch;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.api.ApiResponse;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class LaunchService {
|
|
||||||
|
|
||||||
public ApiResponse<LaunchInfo> prepareLaunch(String instanceName) {
|
|
||||||
try {
|
|
||||||
Instance instance = InstanceManager.getInstance(instanceName);
|
|
||||||
if (instance == null) {
|
|
||||||
return ApiResponse.error("Сборка не найдена: " + instanceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
|
||||||
LaunchOptions options = new LaunchOptions();
|
|
||||||
|
|
||||||
List<String> command = builder.build(options);
|
|
||||||
|
|
||||||
LaunchInfo info = new LaunchInfo(
|
|
||||||
instanceName,
|
|
||||||
command,
|
|
||||||
instance.getPath().toString()
|
|
||||||
);
|
|
||||||
return ApiResponse.success(info);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка подготовки запуска: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<ProcessInfo> launch(String instanceName) {
|
|
||||||
try {
|
|
||||||
Instance instance = InstanceManager.getInstance(instanceName);
|
|
||||||
if (instance == null) {
|
|
||||||
return ApiResponse.error("Сборка не найдена: " + instanceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
|
||||||
LaunchOptions options = new LaunchOptions();
|
|
||||||
|
|
||||||
List<String> command = builder.build(options);
|
|
||||||
|
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
|
||||||
processBuilder.directory(instance.getPath().toFile());
|
|
||||||
processBuilder.inheritIO();
|
|
||||||
|
|
||||||
Process process = processBuilder.start();
|
|
||||||
|
|
||||||
ProcessInfo info = new ProcessInfo(
|
|
||||||
instanceName,
|
|
||||||
process.pid(),
|
|
||||||
"RUNNING"
|
|
||||||
);
|
|
||||||
return ApiResponse.success(info);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка запуска: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<Boolean> isReady(String instanceName) {
|
|
||||||
try {
|
|
||||||
Instance instance = InstanceManager.getInstance(instanceName);
|
|
||||||
if (instance == null) {
|
|
||||||
return ApiResponse.error("Сборка не найдена: " + instanceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
Path versionJson = instance.getPath().resolve("version.json");
|
|
||||||
boolean hasVersionJson = versionJson.toFile().exists();
|
|
||||||
|
|
||||||
return ApiResponse.success(hasVersionJson);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка проверки готовности: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiResponse<InstanceInfo> getLaunchInfo(String instanceName) {
|
|
||||||
try {
|
|
||||||
Instance instance = InstanceManager.getInstance(instanceName);
|
|
||||||
if (instance == null) {
|
|
||||||
return ApiResponse.error("Сборка не найдена: " + instanceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
InstanceInfo info = new InstanceInfo(
|
|
||||||
instance.getName(),
|
|
||||||
instance.getMinecraftVersion(),
|
|
||||||
instance.getLoaderType(),
|
|
||||||
instance.getLoaderVersion(),
|
|
||||||
instance.getAssetIndex()
|
|
||||||
);
|
|
||||||
return ApiResponse.success(info);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return ApiResponse.error("Ошибка получения информации: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LaunchInfo {
|
|
||||||
private String instanceName;
|
|
||||||
private List<String> command;
|
|
||||||
private String workingDirectory;
|
|
||||||
|
|
||||||
public LaunchInfo(String instanceName, List<String> command, String workingDirectory) {
|
|
||||||
this.instanceName = instanceName;
|
|
||||||
this.command = command;
|
|
||||||
this.workingDirectory = workingDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getInstanceName() { return instanceName; }
|
|
||||||
public List<String> getCommand() { return command; }
|
|
||||||
public String getWorkingDirectory() { return workingDirectory; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ProcessInfo {
|
|
||||||
private String instanceName;
|
|
||||||
private long pid;
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
public ProcessInfo(String instanceName, long pid, String status) {
|
|
||||||
this.instanceName = instanceName;
|
|
||||||
this.pid = pid;
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getInstanceName() { return instanceName; }
|
|
||||||
public long getPid() { return pid; }
|
|
||||||
public String getStatus() { return status; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class InstanceInfo {
|
|
||||||
private String name;
|
|
||||||
private String minecraftVersion;
|
|
||||||
private String loaderType;
|
|
||||||
private String loaderVersion;
|
|
||||||
private String assetIndex;
|
|
||||||
|
|
||||||
public InstanceInfo(String name, String minecraftVersion, String loaderType,
|
|
||||||
String loaderVersion, String assetIndex) {
|
|
||||||
this.name = name;
|
|
||||||
this.minecraftVersion = minecraftVersion;
|
|
||||||
this.loaderType = loaderType;
|
|
||||||
this.loaderVersion = loaderVersion;
|
|
||||||
this.assetIndex = assetIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() { return name; }
|
|
||||||
public String getMinecraftVersion() { return minecraftVersion; }
|
|
||||||
public String getLoaderType() { return loaderType; }
|
|
||||||
public String getLoaderVersion() { return loaderVersion; }
|
|
||||||
public String getAssetIndex() { return assetIndex; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,354 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.auth;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.Config;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class AuthManager {
|
|
||||||
|
|
||||||
private static final Path AUTH_FILE = Config.getConfigDir().resolve("auth.json");
|
|
||||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
|
||||||
|
|
||||||
private static volatile AuthSession session = null;
|
|
||||||
private static volatile UserInfo userInfo = null;
|
|
||||||
|
|
||||||
// === Роли ===
|
|
||||||
public static final int ROLE_USER = 0;
|
|
||||||
public static final int ROLE_PASS_HOLDER = 1;
|
|
||||||
public static final int ROLE_MODERATOR = 2;
|
|
||||||
public static final int ROLE_ELDER = 3;
|
|
||||||
public static final int ROLE_CREATOR = 4;
|
|
||||||
|
|
||||||
// === Права доступа ===
|
|
||||||
public static final String PERM_VIEW_PACKS = "view_packs";
|
|
||||||
public static final String PERM_DOWNLOAD_PACK = "download_pack";
|
|
||||||
|
|
||||||
public static boolean loadSavedSession() {
|
|
||||||
if (!Files.exists(AUTH_FILE)) return false;
|
|
||||||
try {
|
|
||||||
String json = Files.readString(AUTH_FILE);
|
|
||||||
AuthSession loaded = GSON.fromJson(json, AuthSession.class);
|
|
||||||
if (loaded == null || loaded.accessToken == null) return false;
|
|
||||||
|
|
||||||
session = loaded;
|
|
||||||
userInfo = fetchUserInfo();
|
|
||||||
|
|
||||||
if (isAccessTokenExpired()) {
|
|
||||||
return tryRefresh();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== АВТОРИЗАЦИЯ ======================
|
|
||||||
public static AuthResult login(String username, String password) {
|
|
||||||
return authRequest("/auth/login", username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AuthResult register(String username, String password) {
|
|
||||||
return authRequest("/auth/register", username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AuthResult authRequest(String endpoint, String username, String password) {
|
|
||||||
try {
|
|
||||||
String body = GSON.toJson(new LoginRequest(username, password));
|
|
||||||
SimpleHttpResponse resp = post(endpoint, body);
|
|
||||||
|
|
||||||
if (resp.statusCode() == 200) {
|
|
||||||
session = GSON.fromJson(resp.body(), AuthSession.class);
|
|
||||||
session.expiresAt = System.currentTimeMillis() / 1000L + session.expiresIn;
|
|
||||||
saveSession();
|
|
||||||
userInfo = fetchUserInfo();
|
|
||||||
return AuthResult.ok();
|
|
||||||
} else if (resp.statusCode() == 422) {
|
|
||||||
return AuthResult.fail("Ошибка валидации: " + extractError(resp.body()));
|
|
||||||
} else {
|
|
||||||
return AuthResult.fail(extractError(resp.body()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return AuthResult.fail("Ошибка соединения: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void logout() {
|
|
||||||
if (session != null && session.refreshToken != null) {
|
|
||||||
try {
|
|
||||||
post("/auth/logout", "{\"refresh_token\":\"" + session.refreshToken + "\"}");
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
session = null;
|
|
||||||
userInfo = null;
|
|
||||||
try { Files.deleteIfExists(AUTH_FILE); } catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isLoggedIn() {
|
|
||||||
return session != null && session.accessToken != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getUsername() {
|
|
||||||
return session != null ? session.username : "Player";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getUuid() {
|
|
||||||
return session != null ? session.uuid : "00000000-0000-0000-0000-000000000000";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getAccessToken() {
|
|
||||||
if (session == null) return "0";
|
|
||||||
if (isAccessTokenExpired()) {
|
|
||||||
tryRefresh();
|
|
||||||
}
|
|
||||||
return session != null && session.accessToken != null ? session.accessToken : "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isAccessTokenExpired() {
|
|
||||||
if (session == null) return true;
|
|
||||||
return System.currentTimeMillis() / 1000L >= session.expiresAt - 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean tryRefresh() {
|
|
||||||
if (session == null || session.refreshToken == null) return false;
|
|
||||||
try {
|
|
||||||
String body = "{\"refresh_token\":\"" + session.refreshToken + "\"}";
|
|
||||||
SimpleHttpResponse resp = post("/auth/refresh", body);
|
|
||||||
|
|
||||||
if (resp.statusCode() == 200) {
|
|
||||||
AuthSession newSession = GSON.fromJson(resp.body(), AuthSession.class);
|
|
||||||
newSession.expiresAt = System.currentTimeMillis() / 1000L + newSession.expiresIn;
|
|
||||||
session = newSession;
|
|
||||||
userInfo = fetchUserInfo();
|
|
||||||
saveSession();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
session = null;
|
|
||||||
userInfo = null;
|
|
||||||
try { Files.deleteIfExists(AUTH_FILE); } catch (Exception ignored) {}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void saveSession() {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(AUTH_FILE.getParent());
|
|
||||||
Files.writeString(AUTH_FILE, GSON.toJson(session));
|
|
||||||
} catch (IOException e) {
|
|
||||||
System.err.println(ZAnsi.yellow("Не удалось сохранить сессию: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== ПОЛУЧЕНИЕ ИНФОРМАЦИИ О ПОЛЬЗОВАТЕЛЕ ====================
|
|
||||||
private static UserInfo fetchUserInfo() {
|
|
||||||
if (!isLoggedIn() || session.accessToken == null) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Используем существующий метод ZHttpClient.get() + вручную добавляем токен
|
|
||||||
java.net.HttpURLConnection conn = null;
|
|
||||||
try {
|
|
||||||
URL url = new URL(ZHttpClient.getBaseUrl() + "/admin/me");
|
|
||||||
conn = (java.net.HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
conn.setRequestProperty("Authorization", "Bearer " + session.accessToken);
|
|
||||||
conn.setConnectTimeout(10000);
|
|
||||||
conn.setReadTimeout(10000);
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
if (responseCode != 200) return null;
|
|
||||||
|
|
||||||
StringBuilder response = new StringBuilder();
|
|
||||||
try (var reader = new java.io.BufferedReader(
|
|
||||||
new java.io.InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
response.append(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return GSON.fromJson(response.toString(), UserInfo.class);
|
|
||||||
} finally {
|
|
||||||
if (conn != null) conn.disconnect();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Не удалось получить UserInfo: " + e.getMessage());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== ПРОВЕРКИ ПРАВ ====================
|
|
||||||
public static boolean hasPass() {
|
|
||||||
if (userInfo != null) return userInfo.has_pass;
|
|
||||||
return getRole() >= ROLE_PASS_HOLDER;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean canViewPacks() {
|
|
||||||
if (userInfo != null && userInfo.permissions != null) {
|
|
||||||
return userInfo.permissions.contains(PERM_VIEW_PACKS);
|
|
||||||
}
|
|
||||||
return hasPass(); // fallback для старых аккаунтов
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean canDownloadPacks() {
|
|
||||||
if (userInfo != null && userInfo.permissions != null) {
|
|
||||||
return userInfo.permissions.contains(PERM_DOWNLOAD_PACK);
|
|
||||||
}
|
|
||||||
return hasPass(); // fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getRole() {
|
|
||||||
return session != null ? session.role : ROLE_USER;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== POST ======================
|
|
||||||
private static SimpleHttpResponse post(String endpoint, String jsonBody) throws Exception {
|
|
||||||
String fullUrl = ZHttpClient.getBaseUrl() + endpoint;
|
|
||||||
HttpURLConnection conn = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
URL url = new URL(fullUrl);
|
|
||||||
conn = (HttpURLConnection) url.openConnection();
|
|
||||||
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
conn.setRequestProperty("User-Agent", "ZernMC-Launcher/1.0");
|
|
||||||
conn.setRequestProperty("Connection", "close");
|
|
||||||
|
|
||||||
if (session != null && session.accessToken != null) {
|
|
||||||
conn.setRequestProperty("Authorization", "Bearer " + session.accessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
conn.setConnectTimeout(15000);
|
|
||||||
conn.setReadTimeout(15000);
|
|
||||||
|
|
||||||
byte[] input = jsonBody.getBytes(StandardCharsets.UTF_8);
|
|
||||||
conn.setFixedLengthStreamingMode(input.length);
|
|
||||||
|
|
||||||
try (var os = conn.getOutputStream()) {
|
|
||||||
os.write(input);
|
|
||||||
os.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
int statusCode = conn.getResponseCode();
|
|
||||||
var is = (statusCode >= 200 && statusCode < 300) ? conn.getInputStream() : conn.getErrorStream();
|
|
||||||
|
|
||||||
String responseBody;
|
|
||||||
try (var scanner = new java.util.Scanner(is, StandardCharsets.UTF_8.name())) {
|
|
||||||
responseBody = scanner.useDelimiter("\\A").hasNext() ? scanner.next() : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SimpleHttpResponse(statusCode, responseBody);
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (conn != null) conn.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String extractError(String body) {
|
|
||||||
try {
|
|
||||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
|
||||||
if (json.has("detail")) {
|
|
||||||
if (json.get("detail").isJsonArray()) {
|
|
||||||
return json.getAsJsonArray("detail").get(0).getAsJsonObject().get("msg").getAsString();
|
|
||||||
}
|
|
||||||
return json.get("detail").getAsString();
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
return body.length() > 200 ? body.substring(0, 200) + "..." : body;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasActivePass() {
|
|
||||||
if (!isLoggedIn()) return false;
|
|
||||||
try {
|
|
||||||
String response = ZHttpClient.get("/auth/pass/my");
|
|
||||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
|
||||||
return json.has("has_active") && json.get("has_active").getAsBoolean();
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println(ZAnsi.red("Не удалось проверить проходки: ") + e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getPassStatus() {
|
|
||||||
if (!isLoggedIn()) return "Не авторизован";
|
|
||||||
try {
|
|
||||||
String response = ZHttpClient.get("/auth/pass/my");
|
|
||||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
|
||||||
boolean hasActive = json.has("has_active") && json.get("has_active").getAsBoolean();
|
|
||||||
return hasActive ? "Есть активная проходка" : "Проходка отсутствует";
|
|
||||||
} catch (Exception e) {
|
|
||||||
return "Ошибка проверки";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ВНУТРЕННИЕ КЛАССЫ ======================
|
|
||||||
public static class AuthSession {
|
|
||||||
@SerializedName("access_token") public String accessToken;
|
|
||||||
@SerializedName("refresh_token") public String refreshToken;
|
|
||||||
@SerializedName("expires_in") public int expiresIn;
|
|
||||||
public transient long expiresAt;
|
|
||||||
public String username;
|
|
||||||
public String uuid;
|
|
||||||
public int role;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class UserInfo {
|
|
||||||
public int id;
|
|
||||||
public String username;
|
|
||||||
public String uuid;
|
|
||||||
public int role;
|
|
||||||
public String role_name;
|
|
||||||
public boolean has_pass;
|
|
||||||
public List<String> permissions;
|
|
||||||
|
|
||||||
public boolean hasPermission(String perm) {
|
|
||||||
return permissions != null && permissions.contains(perm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class LoginRequest {
|
|
||||||
final String username;
|
|
||||||
final String password;
|
|
||||||
LoginRequest(String u, String p) {
|
|
||||||
this.username = u;
|
|
||||||
this.password = p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class AuthResult {
|
|
||||||
public final boolean success;
|
|
||||||
public final String error;
|
|
||||||
private AuthResult(boolean s, String e) { success = s; error = e; }
|
|
||||||
public static AuthResult ok() { return new AuthResult(true, null); }
|
|
||||||
public static AuthResult fail(String msg) { return new AuthResult(false, msg); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ВСПОМОГАТЕЛЬНЫЙ КЛАСС ======================
|
|
||||||
class SimpleHttpResponse {
|
|
||||||
final int statusCode;
|
|
||||||
final String body;
|
|
||||||
|
|
||||||
SimpleHttpResponse(int statusCode, String body) {
|
|
||||||
this.statusCode = statusCode;
|
|
||||||
this.body = body;
|
|
||||||
}
|
|
||||||
|
|
||||||
int statusCode() { return statusCode; }
|
|
||||||
String body() { return body; }
|
|
||||||
}
|
|
||||||
@@ -1,770 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.menu;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
|
||||||
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;
|
|
||||||
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.Config;
|
|
||||||
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.awt.*;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class LaunchMenu {
|
|
||||||
|
|
||||||
public void show() throws Exception {
|
|
||||||
if (Config.isZernMCBuild()) {
|
|
||||||
showZernMCOnly();
|
|
||||||
} else {
|
|
||||||
showGlobal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ZERNMC BUILD ======================
|
|
||||||
private void showZernMCOnly() throws Exception {
|
|
||||||
while (true) {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.header("=== ZernMC Private Launcher ==="));
|
|
||||||
System.out.println(ZAnsi.cyan("Доступны только серверные сборки"));
|
|
||||||
|
|
||||||
if (!awaitActivePass()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PackDownloader tempDownloader = new PackDownloader(null);
|
|
||||||
List<ServerPack> availablePacks = tempDownloader.getAvailablePacks();
|
|
||||||
|
|
||||||
if (availablePacks.isEmpty()) {
|
|
||||||
System.out.println(ZAnsi.yellow("На данный момент нет доступных сборок на сервере."));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
installAndRunServerPack(selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean awaitActivePass() throws Exception {
|
|
||||||
if (AuthManager.hasActivePass()) {
|
|
||||||
System.out.println(ZAnsi.brightGreen("✓ Активная проходка подтверждена"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.brightRed("У вас нет активной проходки!"));
|
|
||||||
System.out.println(ZAnsi.white("Для доступа к сборкам ZernMC требуется активная проходка."));
|
|
||||||
System.out.println();
|
|
||||||
|
|
||||||
openActivationWebsite();
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Ожидаем активацию проходки... (проверка каждые 10 секунд)"));
|
|
||||||
System.out.println(ZAnsi.white("Нажмите Enter для отмены"));
|
|
||||||
|
|
||||||
for (int i = 0; i < 60; i++) {
|
|
||||||
try {
|
|
||||||
if (System.in.available() > 0) {
|
|
||||||
Input.readLine();
|
|
||||||
System.out.println(ZAnsi.yellow("\nОжидание отменено."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
|
|
||||||
Thread.sleep(10000);
|
|
||||||
|
|
||||||
if (AuthManager.hasActivePass()) {
|
|
||||||
System.out.println(ZAnsi.brightGreen("\n✓ Проходка успешно активирована!"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.print(ZAnsi.cyan("."));
|
|
||||||
if ((i + 1) % 6 == 0) System.out.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightRed("\n\nВремя ожидания истекло."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openActivationWebsite() {
|
|
||||||
//String url = "https://launcher.ru.zernmc.ru/activate-pass";
|
|
||||||
String url = ZHttpClient.getBaseUrl() + "/activate-pass";
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
|
||||||
Desktop.getDesktop().browse(new URI(url));
|
|
||||||
System.out.println(ZAnsi.cyan("Браузер открыт: " + url));
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.yellow("Не удалось открыть браузер автоматически."));
|
|
||||||
System.out.println(ZAnsi.white("Откройте вручную: " + url));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Ошибка открытия браузера: " + e.getMessage()));
|
|
||||||
System.out.println(ZAnsi.white("Ссылка: " + url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void installAndRunServerPack(ServerPack selected) throws Exception {
|
|
||||||
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() != null ? " " + selected.getLoaderVersion() : ""));
|
|
||||||
System.out.println(ZAnsi.white(" Версия: v") + selected.getVersion());
|
|
||||||
System.out.println(ZAnsi.white(" Файлов: ") + selected.getFilesCount());
|
|
||||||
|
|
||||||
String localName = askPackName();
|
|
||||||
if (localName == null) return;
|
|
||||||
|
|
||||||
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.brightRed("\n[FAIL] Не удалось установить сборку."));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("\n[OK] Сборка '" + localName + "' успешно установлена!"));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
|
|
||||||
launchExistingInstance(newInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== GLOBAL BUILD ======================
|
|
||||||
private void showGlobal() throws Exception {
|
|
||||||
while (true) {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
List<Instance> instances = InstanceManager.getAllInstances();
|
|
||||||
|
|
||||||
List<String> options = instances.stream()
|
|
||||||
.map(Instance::toString)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
options.add("Установить новую сборку");
|
|
||||||
options.add("Назад в главное меню");
|
|
||||||
|
|
||||||
ArrowMenu menu = new ArrowMenu("Управление сборками", options);
|
|
||||||
int choice = menu.show();
|
|
||||||
|
|
||||||
if (choice == -1 || choice == options.size() - 1) break;
|
|
||||||
|
|
||||||
if (choice == instances.size()) {
|
|
||||||
installNewPackGlobal();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Instance selected = instances.get(choice);
|
|
||||||
manageInstance(selected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void installNewPackGlobal() 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 -> installServerPackGlobal();
|
|
||||||
case 1 -> createVanillaInstance();
|
|
||||||
case 2 -> createCustomInstance();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void installServerPackGlobal() throws Exception {
|
|
||||||
if (!awaitActivePass()) return;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.print(ZAnsi.white("\nВведите название локальной сборки (Enter = имя пака): "));
|
|
||||||
String localName = Input.readLine().trim();
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== manageInstance — полностью восстановлен ======================
|
|
||||||
private void manageInstance(Instance instance) throws Exception {
|
|
||||||
while (true) {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.header("Управление сборкой: " + instance.getName()));
|
|
||||||
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 == options.size() - 1) return;
|
|
||||||
|
|
||||||
switch (choice) {
|
|
||||||
case 0 -> launchExistingInstance(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 if ("neoforge".equalsIgnoreCase(currentLoader)) {
|
|
||||||
newLoaderVersion = askNeoForgeVersion(mcVersion);
|
|
||||||
} else {
|
|
||||||
newLoaderVersion = askForgeVersion(mcVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLoaderVersion == null) return;
|
|
||||||
|
|
||||||
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 if ("neoforge".equalsIgnoreCase(currentLoader)) {
|
|
||||||
success = lib.installNeoForge(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();
|
|
||||||
|
|
||||||
List<String> confirmOptions = List.of(
|
|
||||||
"Да, удалить сборку",
|
|
||||||
"Нет, отменить"
|
|
||||||
);
|
|
||||||
|
|
||||||
ArrowMenu confirmMenu = new ArrowMenu(
|
|
||||||
"Вы действительно хотите удалить сборку '" + instance.getName() + "'?",
|
|
||||||
confirmOptions
|
|
||||||
);
|
|
||||||
|
|
||||||
int choice = confirmMenu.show();
|
|
||||||
|
|
||||||
if (choice == 0) {
|
|
||||||
boolean deleted = InstanceManager.deleteInstance(instance.getName());
|
|
||||||
if (deleted) {
|
|
||||||
System.out.println(ZAnsi.brightGreen("Сборка '" + instance.getName() + "' успешно удалена."));
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("Не удалось удалить сборку."));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.yellow("Удаление отменено."));
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void launchExistingInstance(Instance instance) {
|
|
||||||
if (instance.isServerPack() && !AuthManager.hasActivePass()) {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.brightRed("Для запуска серверной сборки требуется активная проходка!"));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName()));
|
|
||||||
|
|
||||||
MinecraftLib lib = new MinecraftLib(instance);
|
|
||||||
LaunchOptions options = new LaunchOptions();
|
|
||||||
|
|
||||||
options.setUsername(AuthManager.getUsername());
|
|
||||||
options.setUuid(AuthManager.getUuid());
|
|
||||||
options.setAccessToken(AuthManager.getAccessToken());
|
|
||||||
|
|
||||||
try {
|
|
||||||
lib.launch(options);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Ошибка при запуске: " + e.getMessage()));
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== Остальные вспомогательные методы ======================
|
|
||||||
|
|
||||||
private String askPackName() {
|
|
||||||
System.out.print(ZAnsi.white("\nВведите название новой сборки: "));
|
|
||||||
String name = Input.readLine().trim();
|
|
||||||
if (name.isEmpty()) {
|
|
||||||
System.out.println(ZAnsi.yellow("Отменено."));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createVanillaInstance() throws Exception {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.cyan("Получение списка версий Minecraft..."));
|
|
||||||
|
|
||||||
VersionInstaller versionInstaller = new VersionInstaller(null);
|
|
||||||
List<MinecraftVersion> allVersions = versionInstaller.getAvailableVersions();
|
|
||||||
|
|
||||||
List<String> versionOptions = allVersions.stream()
|
|
||||||
.map(v -> v.getId() + " (" + v.getType() + ")")
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
versionOptions.add("Назад");
|
|
||||||
|
|
||||||
ArrowMenu versionMenu = new ArrowMenu("Выбор версии Minecraft", versionOptions);
|
|
||||||
int versionChoice = versionMenu.show();
|
|
||||||
|
|
||||||
if (versionChoice == -1 || versionChoice == versionOptions.size() - 1) return;
|
|
||||||
|
|
||||||
MinecraftVersion selectedMc = allVersions.get(versionChoice);
|
|
||||||
String mcVersion = selectedMc.getId();
|
|
||||||
|
|
||||||
String packName = askPackName();
|
|
||||||
if (packName == null) return;
|
|
||||||
|
|
||||||
if (InstanceManager.getInstance(packName) != null) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Сборка с таким именем уже существует!"));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
InstanceManager.createInstanceFolder(packName);
|
|
||||||
Instance newInstance = InstanceManager.getInstance(packName);
|
|
||||||
|
|
||||||
MinecraftLib lib = new MinecraftLib(newInstance);
|
|
||||||
boolean success = lib.installMinecraft(mcVersion);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
System.out.println(ZAnsi.brightGreen("\n[OK] Vanilla сборка '" + packName + "' успешно создана!"));
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("\n[FAIL] Не удалось создать сборку."));
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createCustomInstance() throws Exception {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
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;
|
|
||||||
if (selectedLoader.contains("Fabric")) {
|
|
||||||
loaderType = "fabric";
|
|
||||||
} else if (selectedLoader.contains("NeoForge")) {
|
|
||||||
loaderType = "neoforge";
|
|
||||||
} else {
|
|
||||||
loaderType = "forge";
|
|
||||||
}
|
|
||||||
|
|
||||||
String loaderVersion;
|
|
||||||
if (loaderType.equals("fabric")) {
|
|
||||||
loaderVersion = askFabricLoaderVersion();
|
|
||||||
} else if (loaderType.equals("neoforge")) {
|
|
||||||
loaderVersion = askNeoForgeVersion(mcVersion);
|
|
||||||
} 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;
|
|
||||||
if (loaderType.equals("fabric")) {
|
|
||||||
success = lib.installFabric(mcVersion, loaderVersion);
|
|
||||||
} else if (loaderType.equals("neoforge")) {
|
|
||||||
success = lib.installNeoForge(mcVersion, loaderVersion);
|
|
||||||
} else {
|
|
||||||
success = lib.installForge(mcVersion, loaderVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
System.out.println(ZAnsi.brightGreen("\n[OK] Сборка '" + packName + "' успешно установлена!"));
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("\n[FAIL] Не удалось установить сборку."));
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> buildLoaderOptions(String mcVersion) {
|
|
||||||
List<String> options = new ArrayList<>();
|
|
||||||
|
|
||||||
if (isFabricSupported(mcVersion)) options.add("Fabric");
|
|
||||||
if (isNeoForgeSupported(mcVersion)) options.add("NeoForge");
|
|
||||||
if (isForgeSupported(mcVersion)) options.add("Forge");
|
|
||||||
options.add("Vanilla");
|
|
||||||
options.add("Назад");
|
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFabricSupported(String version) {
|
|
||||||
return version.matches("^1\\.(1[4-9]|[2-9]\\d).*");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isForgeSupported(String version) {
|
|
||||||
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 boolean isNeoForgeSupported(String version) {
|
|
||||||
return version.matches("^1\\.20\\.[1-9].*") ||
|
|
||||||
version.matches("^1\\.21.*") ||
|
|
||||||
version.matches("^\\d{2}\\..*");
|
|
||||||
}
|
|
||||||
|
|
||||||
private String askFabricLoaderVersion() throws Exception {
|
|
||||||
System.out.println(ZAnsi.cyan("Получение списка версий Fabric Loader..."));
|
|
||||||
List<String> versions = ZHttpClient.getFabricLoaderVersions();
|
|
||||||
|
|
||||||
List<String> options = versions.stream()
|
|
||||||
.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 + "..."));
|
|
||||||
|
|
||||||
List<String> allForgeVersions = getAllForgeVersions();
|
|
||||||
|
|
||||||
List<String> compatibleVersions = allForgeVersions.stream()
|
|
||||||
.filter(v -> v.startsWith(mcVersion + "-"))
|
|
||||||
.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 List<String> getAllForgeVersions() throws Exception {
|
|
||||||
String xml = ZHttpClient.downloadString("https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml");
|
|
||||||
|
|
||||||
List<String> versions = new ArrayList<>();
|
|
||||||
int index = 0;
|
|
||||||
|
|
||||||
while ((index = xml.indexOf("<version>", index)) != -1) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String askNeoForgeVersion(String mcVersion) throws Exception {
|
|
||||||
System.out.println(ZAnsi.cyan("Получение списка версий NeoForge для " + mcVersion + "..."));
|
|
||||||
|
|
||||||
List<String> allNeoForgeVersions = getAllNeoForgeVersions();
|
|
||||||
|
|
||||||
List<String> compatibleVersions = allNeoForgeVersions.stream()
|
|
||||||
.filter(v -> isNeoForgeVersionCompatible(v, mcVersion))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (compatibleVersions.isEmpty()) {
|
|
||||||
System.out.println(ZAnsi.yellow("Не найдено совместимых версий NeoForge для " + mcVersion));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> options = compatibleVersions.stream()
|
|
||||||
.limit(30)
|
|
||||||
.map(v -> "NeoForge " + v)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
options.add("Назад");
|
|
||||||
|
|
||||||
ArrowMenu menu = new ArrowMenu("Выбор версии NeoForge для " + mcVersion, options);
|
|
||||||
int choice = menu.show();
|
|
||||||
|
|
||||||
if (choice == -1 || choice == options.size() - 1) return null;
|
|
||||||
|
|
||||||
return compatibleVersions.get(choice);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isNeoForgeVersionCompatible(String version, String mcVersion) {
|
|
||||||
if (mcVersion.equals("1.20.1")) {
|
|
||||||
return version.startsWith("47.");
|
|
||||||
}
|
|
||||||
String majorMinor = mcVersion.replace("1.", "");
|
|
||||||
String[] parts = majorMinor.split("\\.");
|
|
||||||
int targetMajor = Integer.parseInt(parts[0]);
|
|
||||||
return version.startsWith(targetMajor + ".");
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getAllNeoForgeVersions() throws Exception {
|
|
||||||
List<String> versions = new ArrayList<>();
|
|
||||||
|
|
||||||
String[] mavenUrls = {
|
|
||||||
"https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml",
|
|
||||||
"https://maven.neoforged.net/releases/net/neoforged/forge/maven-metadata.xml"
|
|
||||||
};
|
|
||||||
|
|
||||||
for (String mavenUrl : mavenUrls) {
|
|
||||||
try {
|
|
||||||
String xml = ZHttpClient.downloadString(mavenUrl);
|
|
||||||
int index = 0;
|
|
||||||
while ((index = xml.indexOf("<version>", index)) != -1) {
|
|
||||||
int start = index + 9;
|
|
||||||
int end = xml.indexOf("</version>", start);
|
|
||||||
if (end == -1) break;
|
|
||||||
|
|
||||||
String version = xml.substring(start, end).trim();
|
|
||||||
if (!versions.contains(version)) {
|
|
||||||
versions.add(version);
|
|
||||||
}
|
|
||||||
index = end;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Skip if one maven doesn't have the artifact
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
versions.sort((a, b) -> b.compareTo(a));
|
|
||||||
return versions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.menu;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager.AuthResult;
|
|
||||||
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 java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Экран входа/регистрации.
|
|
||||||
* Показывается при старте лаунчера, если нет сохранённой сессии.
|
|
||||||
*
|
|
||||||
* show() возвращает true — пользователь вошёл/зарегистрировался
|
|
||||||
* false — пользователь выбрал выход из лаунчера
|
|
||||||
*/
|
|
||||||
public class LoginMenu {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Главный экран выбора действия.
|
|
||||||
*/
|
|
||||||
public boolean show() throws IOException {
|
|
||||||
while (true) {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
printBanner();
|
|
||||||
|
|
||||||
List<String> options = List.of(
|
|
||||||
"Войти в аккаунт",
|
|
||||||
"Создать аккаунт",
|
|
||||||
"Выйти из лаунчера"
|
|
||||||
);
|
|
||||||
|
|
||||||
ArrowMenu menu = new ArrowMenu("Добро пожаловать в ZernMC!", options);
|
|
||||||
int choice = menu.show();
|
|
||||||
|
|
||||||
if (choice == -1 || choice == 2) return false;
|
|
||||||
|
|
||||||
boolean success = switch (choice) {
|
|
||||||
case 0 -> doLogin();
|
|
||||||
case 1 -> doRegister();
|
|
||||||
default -> false;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (success) return true;
|
|
||||||
// Если не успех — покажем меню снова (ошибка уже напечатана внутри методов)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Показывается когда пользователь уже вошёл — предлагает выйти из аккаунта.
|
|
||||||
*/
|
|
||||||
public void showAccountMenu() throws IOException {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.header("=== Аккаунт ==="));
|
|
||||||
System.out.println();
|
|
||||||
System.out.println(ZAnsi.white(" Игрок: ") + ZAnsi.brightGreen(AuthManager.getUsername()));
|
|
||||||
System.out.println(ZAnsi.white(" UUID: ") + ZAnsi.cyan(AuthManager.getUuid()));
|
|
||||||
System.out.println();
|
|
||||||
|
|
||||||
List<String> options = List.of(
|
|
||||||
"Выйти из аккаунта",
|
|
||||||
"Назад"
|
|
||||||
);
|
|
||||||
|
|
||||||
ArrowMenu menu = new ArrowMenu("Управление аккаунтом", options);
|
|
||||||
int choice = menu.show();
|
|
||||||
|
|
||||||
if (choice == 0) {
|
|
||||||
AuthManager.logout();
|
|
||||||
System.out.println(ZAnsi.yellow("Вы вышли из аккаунта."));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ПРИВАТНЫЕ МЕТОДЫ ======================
|
|
||||||
|
|
||||||
private boolean doLogin() throws IOException {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
printBanner();
|
|
||||||
System.out.println(ZAnsi.cyan(" [ Вход в аккаунт ]"));
|
|
||||||
System.out.println();
|
|
||||||
|
|
||||||
String username = Input.readLine(ZAnsi.white(" Имя пользователя: "));
|
|
||||||
if (username.isEmpty()) return false;
|
|
||||||
|
|
||||||
String password = readPassword(" Пароль: ");
|
|
||||||
if (password.isEmpty()) return false;
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
System.out.print(ZAnsi.cyan(" Выполняем вход..."));
|
|
||||||
|
|
||||||
AuthResult result = AuthManager.login(username, password);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
System.out.println("\r" + ZAnsi.brightGreen(" Добро пожаловать, " + AuthManager.getUsername() + "! "));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
System.out.println("\r" + ZAnsi.brightRed(" Ошибка: " + result.error + " "));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean doRegister() throws IOException {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
printBanner();
|
|
||||||
System.out.println(ZAnsi.cyan(" [ Создание аккаунта ]"));
|
|
||||||
System.out.println();
|
|
||||||
System.out.println(ZAnsi.yellow(" Допустимые символы в имени: a-z, A-Z, 0-9, _"));
|
|
||||||
System.out.println(ZAnsi.yellow(" Длина имени: 3-16 символов | Длина пароля: от 6 символов"));
|
|
||||||
System.out.println();
|
|
||||||
|
|
||||||
String username = Input.readLine(ZAnsi.white(" Имя пользователя: "));
|
|
||||||
if (username.isEmpty()) return false;
|
|
||||||
|
|
||||||
String password = readPassword(" Пароль: ");
|
|
||||||
if (password.isEmpty()) return false;
|
|
||||||
|
|
||||||
String confirm = readPassword(" Повторите пароль: ");
|
|
||||||
if (!password.equals(confirm)) {
|
|
||||||
System.out.println(ZAnsi.brightRed("\n Пароли не совпадают!"));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
System.out.print(ZAnsi.cyan(" Создаём аккаунт..."));
|
|
||||||
|
|
||||||
AuthResult result = AuthManager.register(username, password);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
System.out.println("\r" + ZAnsi.brightGreen(" Аккаунт создан! Добро пожаловать, " + AuthManager.getUsername() + "! "));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
System.out.println("\r" + ZAnsi.brightRed(" Ошибка: " + result.error + " "));
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Читаем пароль — стараемся скрыть вывод через Console,
|
|
||||||
* если недоступно (IDE/терминал без TTY) — читаем обычным способом.
|
|
||||||
*/
|
|
||||||
private String readPassword(String prompt) throws IOException {
|
|
||||||
org.jline.terminal.Terminal passTerminal = org.jline.terminal.TerminalBuilder.builder()
|
|
||||||
.system(true)
|
|
||||||
.jna(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
passTerminal.enterRawMode();
|
|
||||||
passTerminal.writer().print(prompt);
|
|
||||||
passTerminal.writer().flush();
|
|
||||||
|
|
||||||
StringBuilder password = new StringBuilder();
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
int key = passTerminal.reader().read();
|
|
||||||
|
|
||||||
if (key == 27) {
|
|
||||||
// Escape sequence — consume remaining bytes (arrow keys, etc.)
|
|
||||||
int next = passTerminal.reader().read(50);
|
|
||||||
if (next == 91) { // '[' — arrow key sequence
|
|
||||||
passTerminal.reader().read(50); // consume 'A'/'B'/'C'/'D'
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key == 13 || key == 10) { // Enter
|
|
||||||
passTerminal.writer().println();
|
|
||||||
break;
|
|
||||||
} else if (key == 127 || key == 8) { // Backspace
|
|
||||||
if (password.length() > 0) {
|
|
||||||
password.setLength(password.length() - 1);
|
|
||||||
passTerminal.writer().print("\b \b");
|
|
||||||
passTerminal.writer().flush();
|
|
||||||
}
|
|
||||||
} else if (key == 3) { // Ctrl+C
|
|
||||||
passTerminal.writer().println();
|
|
||||||
System.exit(0);
|
|
||||||
} else if (key >= 32 && key < 127) { // Printable characters
|
|
||||||
password.append((char) key);
|
|
||||||
passTerminal.writer().print('*');
|
|
||||||
passTerminal.writer().flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
passTerminal.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
return password.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void printBanner() {
|
|
||||||
System.out.println(ZAnsi.header("╔══════════════════════════════╗"));
|
|
||||||
System.out.println(ZAnsi.header("║ ZernMC Launcher ║"));
|
|
||||||
System.out.println(ZAnsi.header("╚══════════════════════════════╝"));
|
|
||||||
System.out.println();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.menu;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ServerCheckMenu {
|
|
||||||
|
|
||||||
public void show() throws IOException {
|
|
||||||
while (true) {
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
System.out.println(ZAnsi.header("Диагностика подключения"));
|
|
||||||
|
|
||||||
List<String> options = List.of(
|
|
||||||
"Проверить подключение к ZernMC серверу",
|
|
||||||
"Проверить доступ к Mojang (Minecraft)",
|
|
||||||
"Проверить доступ к Fabric Meta",
|
|
||||||
"Проверить доступ к Forge Maven",
|
|
||||||
"Назад в главное меню"
|
|
||||||
);
|
|
||||||
|
|
||||||
ArrowMenu menu = new ArrowMenu("Выберите проверку", options);
|
|
||||||
int choice = menu.show();
|
|
||||||
|
|
||||||
if (choice == -1 || choice == 4) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
|
|
||||||
switch (choice) {
|
|
||||||
case 0 -> checkZernServer();
|
|
||||||
case 1 -> checkMojang();
|
|
||||||
case 2 -> checkFabric();
|
|
||||||
case 3 -> checkForge();
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkZernServer() {
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка подключения к ZernMC серверу..."));
|
|
||||||
|
|
||||||
try {
|
|
||||||
String response = ZHttpClient.get("/health");
|
|
||||||
System.out.println(ZAnsi.brightGreen("[OK] ZernMC сервер успешно подключён!"));
|
|
||||||
System.out.println(ZAnsi.white("Ответ сервера: ") + response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.brightRed("[FAIL] Не удалось подключиться к ZernMC серверу"));
|
|
||||||
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkMojang() {
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка доступа к Mojang..."));
|
|
||||||
|
|
||||||
try {
|
|
||||||
HttpClient client = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(10))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"))
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
System.out.println(ZAnsi.brightGreen("[OK] Mojang доступен"));
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("[FAIL] Mojang вернул код " + response.statusCode()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.brightRed("[FAIL] Нет доступа к Mojang"));
|
|
||||||
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkFabric() {
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка доступа к Fabric Meta..."));
|
|
||||||
|
|
||||||
try {
|
|
||||||
HttpClient client = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(10))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create("https://meta.fabricmc.net/v2/versions"))
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
System.out.println(ZAnsi.brightGreen("[OK] Fabric Meta доступен"));
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("[FAIL] Fabric Meta вернул код " + response.statusCode()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.brightRed("[FAIL] Нет доступа к Fabric Meta"));
|
|
||||||
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkForge() {
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка доступа к Forge Maven..."));
|
|
||||||
|
|
||||||
try {
|
|
||||||
HttpClient client = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(10))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create("https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml"))
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
System.out.println(ZAnsi.brightGreen("[OK] Forge Maven доступен"));
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("[FAIL] Forge Maven вернул код " + response.statusCode()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.brightRed("[FAIL] Нет доступа к Forge Maven"));
|
|
||||||
System.out.println(ZAnsi.white("Ошибка: ") + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.menu;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.Config;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.Input;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class SettingsMenu {
|
|
||||||
|
|
||||||
public void show() throws IOException {
|
|
||||||
List<String> options = List.of(
|
|
||||||
"Настроить путь к Java",
|
|
||||||
"Настроить выделенную память (RAM)",
|
|
||||||
"Дополнительные JVM параметры",
|
|
||||||
"Назад в главное меню"
|
|
||||||
);
|
|
||||||
|
|
||||||
ArrowMenu menu = new ArrowMenu("Настройки лаунчера", options);
|
|
||||||
int choice = menu.show();
|
|
||||||
|
|
||||||
if (choice == -1 || choice == 3) return;
|
|
||||||
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
|
|
||||||
switch (choice) {
|
|
||||||
case 0 -> configureJava();
|
|
||||||
case 1 -> configureRam();
|
|
||||||
case 2 -> configureJvmArgs();
|
|
||||||
}
|
|
||||||
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureJava() {
|
|
||||||
System.out.println(ZAnsi.cyan("Путь к Java:"));
|
|
||||||
System.out.println(" " + Config.getJreDir().toAbsolutePath());
|
|
||||||
System.out.println(ZAnsi.white("\nJava будет искаться автоматически в папке ~/.zernmc/jre/"));
|
|
||||||
System.out.println("Если нужно — положите туда свою версию Java.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureRam() {
|
|
||||||
System.out.println(ZAnsi.cyan("Настройка выделенной памяти"));
|
|
||||||
System.out.println(Config.getRamInfo());
|
|
||||||
|
|
||||||
int newRam = Input.readInt(
|
|
||||||
ZAnsi.white("\nВведите новое значение RAM в MB (или 0 для отмены): "),
|
|
||||||
0, 32768
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newRam == 0) {
|
|
||||||
System.out.println(ZAnsi.yellow("Настройка отменена."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Config.setMaxMemory(newRam);
|
|
||||||
System.out.println(ZAnsi.brightGreen("Выделенная память изменена на " + newRam + " MB"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureJvmArgs() {
|
|
||||||
System.out.println(ZAnsi.yellow("Дополнительные JVM параметры"));
|
|
||||||
System.out.println("Пока в разработке.");
|
|
||||||
System.out.println("В будущем здесь будет список предустановленных оптимизаций.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
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 {
|
|
||||||
|
|
||||||
public void show() throws IOException {
|
|
||||||
List<String> options = List.of(
|
|
||||||
"Проверить обновления сборки (модпака)",
|
|
||||||
"Проверить обновления лаунчера",
|
|
||||||
"Назад в главное меню"
|
|
||||||
);
|
|
||||||
|
|
||||||
ArrowMenu menu = new ArrowMenu("Проверка обновлений", options);
|
|
||||||
int choice = menu.show();
|
|
||||||
|
|
||||||
if (choice == -1 || choice == 2) return;
|
|
||||||
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
|
|
||||||
if (choice == 0) {
|
|
||||||
try {
|
|
||||||
checkPackUpdates();
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Ошибка: " + e.getMessage()));
|
|
||||||
e.printStackTrace();
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
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,172 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ЭТОТ КЛАСС РАБОТАЕТ НЕ ТРОГАТЬ ТОТ КТО БУДЕТ ЧИТАТЬ (на момент 1.0.2)
|
|
||||||
public class Instance {
|
|
||||||
private final String name;
|
|
||||||
private final Path path;
|
|
||||||
|
|
||||||
private String minecraftVersion;
|
|
||||||
private String loaderType; // vanilla, fabric, forge, neoforge
|
|
||||||
private String loaderVersion;
|
|
||||||
private String assetIndex;
|
|
||||||
private boolean isServerPack; // флаг, что это сборка с сервера
|
|
||||||
private int serverVersion; // версия сборки на сервере
|
|
||||||
private String serverPackName; // имя пака на сервере
|
|
||||||
private String fabricVersionId;
|
|
||||||
|
|
||||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
|
||||||
|
|
||||||
public Instance(String name, Path path) {
|
|
||||||
this.name = name;
|
|
||||||
this.path = path;
|
|
||||||
loadMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() { return name; }
|
|
||||||
public Path getPath() { return path; }
|
|
||||||
|
|
||||||
public String getMinecraftVersion() { return minecraftVersion; }
|
|
||||||
public void setMinecraftVersion(String minecraftVersion) {
|
|
||||||
this.minecraftVersion = minecraftVersion;
|
|
||||||
saveMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLoaderType() { return loaderType != null ? loaderType : "vanilla"; }
|
|
||||||
public void setLoaderType(String loaderType) {
|
|
||||||
this.loaderType = loaderType;
|
|
||||||
saveMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLoaderVersion() { return loaderVersion; }
|
|
||||||
public void setLoaderVersion(String loaderVersion) {
|
|
||||||
this.loaderVersion = loaderVersion;
|
|
||||||
saveMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAssetIndex() {
|
|
||||||
return assetIndex != null ? assetIndex : minecraftVersion; // fallback для старых сборок
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAssetIndex(String assetIndex) {
|
|
||||||
this.assetIndex = assetIndex;
|
|
||||||
saveMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFabricVersionId() {
|
|
||||||
return fabricVersionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFabricVersionId(String fabricVersionId) {
|
|
||||||
this.fabricVersionId = fabricVersionId;
|
|
||||||
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);
|
|
||||||
if (minecraftVersion != null) {
|
|
||||||
sb.append(" [").append(minecraftVersion);
|
|
||||||
if (!"vanilla".equalsIgnoreCase(getLoaderType())) {
|
|
||||||
sb.append(" + ").append(getLoaderType());
|
|
||||||
if (loaderVersion != null) sb.append(" ").append(loaderVersion);
|
|
||||||
}
|
|
||||||
sb.append("]");
|
|
||||||
if (isServerPack) {
|
|
||||||
sb.append("v").append(serverVersion);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sb.append(" [?]");
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== Метаданные ======================
|
|
||||||
private void loadMetadata() {
|
|
||||||
Path metaFile = path.resolve("instance.json");
|
|
||||||
if (!Files.exists(metaFile)) return;
|
|
||||||
try {
|
|
||||||
String json = Files.readString(metaFile);
|
|
||||||
InstanceMeta meta = GSON.fromJson(json, InstanceMeta.class);
|
|
||||||
this.minecraftVersion = meta.minecraftVersion;
|
|
||||||
this.loaderType = meta.loaderType;
|
|
||||||
this.loaderVersion = meta.loaderVersion;
|
|
||||||
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,
|
|
||||||
isServerPack, serverVersion, serverPackName
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
Files.writeString(metaFile, GSON.toJson(meta));
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class InstanceMeta {
|
|
||||||
String minecraftVersion;
|
|
||||||
String loaderType;
|
|
||||||
String loaderVersion;
|
|
||||||
String assetIndex;
|
|
||||||
boolean isServerPack = false;
|
|
||||||
int serverVersion = 0;
|
|
||||||
String serverPackName;
|
|
||||||
|
|
||||||
|
|
||||||
public InstanceMeta(String minecraftVersion, String loaderType,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.Config;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class InstanceManager {
|
|
||||||
|
|
||||||
private static final Path INSTANCES_DIR = Config.getInstancesDir();
|
|
||||||
|
|
||||||
public static List<Instance> getAllInstances() throws IOException {
|
|
||||||
if (!Files.exists(INSTANCES_DIR)) {
|
|
||||||
Files.createDirectories(INSTANCES_DIR);
|
|
||||||
return List.of();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Files.list(INSTANCES_DIR)
|
|
||||||
.filter(Files::isDirectory)
|
|
||||||
.map(path -> new Instance(path.getFileName().toString(), path))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Instance getInstance(String name) {
|
|
||||||
Path instancePath = INSTANCES_DIR.resolve(name);
|
|
||||||
if (Files.exists(instancePath) && Files.isDirectory(instancePath)) {
|
|
||||||
return new Instance(name, instancePath);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean deleteInstance(String instanceName) {
|
|
||||||
if (instanceName == null || instanceName.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path instancePath = INSTANCES_DIR.resolve(instanceName);
|
|
||||||
|
|
||||||
if (!Files.exists(instancePath)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Рекурсивно удаляем всю папку сборки
|
|
||||||
Files.walk(instancePath)
|
|
||||||
.sorted((a, b) -> b.compareTo(a)) // удаляем снизу вверх
|
|
||||||
.forEach(path -> {
|
|
||||||
try {
|
|
||||||
Files.deleteIfExists(path);
|
|
||||||
} catch (IOException e) {
|
|
||||||
System.err.println("Не удалось удалить: " + path);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean createInstanceFolder(String name) throws IOException {
|
|
||||||
Path path = INSTANCES_DIR.resolve(name);
|
|
||||||
if (Files.exists(path)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Files.createDirectories(path);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.installer.FabricInstaller;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.installer.ForgeInstaller;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.installer.NeoForgeInstaller;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.installer.VersionInstaller;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class MinecraftLib {
|
|
||||||
|
|
||||||
private final Instance instance;
|
|
||||||
|
|
||||||
public MinecraftLib(Instance instance) {
|
|
||||||
this.instance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Установка
|
|
||||||
public boolean installMinecraft(String versionId) throws Exception {
|
|
||||||
VersionInstaller installer = new VersionInstaller(instance.getPath());
|
|
||||||
|
|
||||||
String assetIndex = installer.install(versionId); // ← теперь возвращается String
|
|
||||||
|
|
||||||
if (assetIndex != null && !assetIndex.isEmpty()) {
|
|
||||||
instance.setMinecraftVersion(versionId);
|
|
||||||
instance.setAssetIndex(assetIndex); // ← сохраняем правильный индекс!
|
|
||||||
instance.setLoaderType("vanilla");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean installForge(String minecraftVersion, String forgeVersion) throws Exception {
|
|
||||||
ForgeInstaller installer = new ForgeInstaller(instance);
|
|
||||||
return installer.install(minecraftVersion, forgeVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean installNeoForge(String minecraftVersion, String neoforgeVersion) throws Exception {
|
|
||||||
NeoForgeInstaller installer = new NeoForgeInstaller(instance);
|
|
||||||
return installer.install(minecraftVersion, neoforgeVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean installFabric(String minecraftVersion, String loaderVersion) throws Exception {
|
|
||||||
FabricInstaller installer = new FabricInstaller(instance);
|
|
||||||
boolean success = installer.install(minecraftVersion, loaderVersion);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// Сохраняем информацию в Instance
|
|
||||||
instance.setMinecraftVersion(minecraftVersion);
|
|
||||||
instance.setLoaderType("fabric");
|
|
||||||
instance.setLoaderVersion(loaderVersion);
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Полная установка сборки (vanilla + loader + моды)
|
|
||||||
* Пока заглушка — будем расширять
|
|
||||||
*/
|
|
||||||
public boolean installPack(String packName, String minecraftVersion, String loaderType, String loaderVersion) throws Exception {
|
|
||||||
System.out.println(ZAnsi.cyan("Начинается полная установка сборки: " + packName));
|
|
||||||
|
|
||||||
// 1. Устанавливаем Minecraft
|
|
||||||
boolean mcInstalled = installMinecraft(minecraftVersion);
|
|
||||||
if (!mcInstalled) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Не удалось установить Minecraft " + minecraftVersion));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Устанавливаем лоадер
|
|
||||||
if ("fabric".equalsIgnoreCase(loaderType)) {
|
|
||||||
boolean fabricInstalled = installFabric(minecraftVersion, loaderVersion);
|
|
||||||
if (!fabricInstalled) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Не удалось установить Fabric"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if ("forge".equalsIgnoreCase(loaderType)) {
|
|
||||||
boolean forgeInstalled = installForge(minecraftVersion, loaderVersion);
|
|
||||||
if (!forgeInstalled) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Не удалось установить Forge"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if ("neoforge".equalsIgnoreCase(loaderType)) {
|
|
||||||
boolean neoforgeInstalled = installNeoForge(minecraftVersion, loaderVersion);
|
|
||||||
if (!neoforgeInstalled) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Не удалось установить NeoForge"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. В будущем здесь будет diff и скачивание модов
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("Базовая установка сборки завершена!"));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Запуск
|
|
||||||
public void launch(LaunchOptions options) throws Exception {
|
|
||||||
System.out.println(ZAnsi.brightGreen("Запуск сборки: " + instance.getName()));
|
|
||||||
cleanupOldLoaders();
|
|
||||||
|
|
||||||
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
|
|
||||||
List<String> command = builder.build(options);
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Команда запуска (" + command.size() + " аргументов):"));
|
|
||||||
command.forEach(arg -> System.out.println(" " + arg));
|
|
||||||
|
|
||||||
ProcessBuilder pb = new ProcessBuilder(command);
|
|
||||||
pb.directory(instance.getPath().toFile());
|
|
||||||
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
|
|
||||||
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
|
|
||||||
pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("\nЗапускаем Minecraft...\n"));
|
|
||||||
ConsoleUtils.clearScreen();
|
|
||||||
|
|
||||||
Process process = pb.start();
|
|
||||||
int exitCode = process.waitFor();
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.yellow("\nMinecraft завершился с кодом: " + exitCode));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void safeDeleteDirectory(Path dir) {
|
|
||||||
try {
|
|
||||||
Files.walk(dir)
|
|
||||||
.sorted((a, b) -> b.compareTo(a))
|
|
||||||
.forEach(p -> {
|
|
||||||
try { Files.deleteIfExists(p); }
|
|
||||||
catch (IOException ignored) {}
|
|
||||||
});
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteOldVersionDirs(Path versionsDir, String keepVersion) throws IOException {
|
|
||||||
if (!Files.exists(versionsDir)) return;
|
|
||||||
|
|
||||||
try (var stream = Files.walk(versionsDir)) {
|
|
||||||
stream.filter(Files::isDirectory)
|
|
||||||
.filter(dir -> dir.getFileName().toString().contains("fabric-loader") ||
|
|
||||||
dir.getFileName().toString().contains("forge") ||
|
|
||||||
dir.getFileName().toString().contains("neoforge"))
|
|
||||||
.filter(dir -> !dir.getFileName().toString().contains(keepVersion))
|
|
||||||
.forEach(this::safeDeleteDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteAllExcept(Path baseDir, String keepVersion) throws IOException {
|
|
||||||
if (!Files.exists(baseDir)) return;
|
|
||||||
|
|
||||||
try (var stream = Files.walk(baseDir)) {
|
|
||||||
stream.filter(Files::isDirectory)
|
|
||||||
.filter(dir -> {
|
|
||||||
String name = dir.getFileName().toString();
|
|
||||||
return name.contains(".") && !name.contains(keepVersion);
|
|
||||||
})
|
|
||||||
.forEach(this::safeDeleteDirectory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanupOldLoaders() throws IOException {
|
|
||||||
String loaderType = instance.getLoaderType().toLowerCase();
|
|
||||||
String currentLoaderVer = instance.getLoaderVersion();
|
|
||||||
|
|
||||||
if (currentLoaderVer == null) return;
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.yellow("Выполняем очистку старых версий лоадера..."));
|
|
||||||
|
|
||||||
// Удаляем все старые fabric-loader / forge
|
|
||||||
Path libraries = instance.getPath().resolve("libraries");
|
|
||||||
|
|
||||||
if ("fabric".equals(loaderType)) {
|
|
||||||
deleteAllExcept(libraries.resolve("net/fabricmc/fabric-loader"), currentLoaderVer);
|
|
||||||
} else if ("forge".equals(loaderType)) {
|
|
||||||
deleteAllExcept(libraries.resolve("net/minecraftforge/forge"), currentLoaderVer);
|
|
||||||
} else if ("neoforge".equals(loaderType)) {
|
|
||||||
deleteAllExcept(libraries.resolve("net/neoforged/neoforge"), currentLoaderVer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Также чистим versions/ от старых fabric/forge версий
|
|
||||||
Path versionsDir = instance.getPath().resolve("versions");
|
|
||||||
deleteOldVersionDirs(versionsDir, currentLoaderVer);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public Instance getInstance() {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,567 +0,0 @@
|
|||||||
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.auth.AuthManager;
|
|
||||||
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 accessToken = AuthManager.getAccessToken();
|
|
||||||
if (accessToken == null) {
|
|
||||||
throw new IOException("Не авторизован. Требуется проходка для просмотра сборок.");
|
|
||||||
}
|
|
||||||
if (!AuthManager.canViewPacks()) {
|
|
||||||
throw new IOException("Для просмотра сборок требуется активная проходка");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Используем HttpURLConnection для GET с авторизацией
|
|
||||||
java.net.HttpURLConnection connection = null;
|
|
||||||
try {
|
|
||||||
java.net.URL url = new java.net.URL(ZHttpClient.getBaseUrl() + "/packs");
|
|
||||||
connection = (java.net.HttpURLConnection) url.openConnection();
|
|
||||||
connection.setRequestMethod("GET");
|
|
||||||
connection.setRequestProperty("Accept", "application/json");
|
|
||||||
connection.setRequestProperty("Authorization", "Bearer " + accessToken);
|
|
||||||
connection.setConnectTimeout(15000);
|
|
||||||
connection.setReadTimeout(15000);
|
|
||||||
|
|
||||||
int responseCode = connection.getResponseCode();
|
|
||||||
|
|
||||||
if (responseCode == 403) {
|
|
||||||
throw new IOException("Для просмотра сборок требуется активная проходка");
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseCode != 200) {
|
|
||||||
throw new IOException("HTTP " + responseCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsePacksResponse(response.toString());
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (connection != null) {
|
|
||||||
connection.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ServerPack> parsePacksResponse(String responseBody) {
|
|
||||||
JsonObject root = JsonParser.parseString(responseBody).getAsJsonObject();
|
|
||||||
JsonArray packsArray = root.getAsJsonArray("packs");
|
|
||||||
List<ServerPack> result = new ArrayList<>();
|
|
||||||
|
|
||||||
for (JsonElement elem : packsArray) {
|
|
||||||
JsonObject pack = elem.getAsJsonObject();
|
|
||||||
|
|
||||||
if (pack.has("error") || (pack.has("status") && "not_scanned".equals(pack.get("status").getAsString()))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String name = pack.get("name").getAsString();
|
|
||||||
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 = LocalDateTime.parse(pack.get("updated_at").getAsString(),
|
|
||||||
DateTimeFormatter.ISO_DATE_TIME);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.add(new ServerPack(name, version, minecraftVersion, loaderType,
|
|
||||||
loaderVersion, updatedAt, filesCount));
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Ошибка парсинга пака: " + 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 ("neoforge".equalsIgnoreCase(manifest.getLoaderType())) {
|
|
||||||
boolean success = lib.installNeoForge(manifest.getMinecraftVersion(), manifest.getLoaderVersion());
|
|
||||||
if (!success) {
|
|
||||||
System.err.println(ZAnsi.brightRed("Не удалось установить NeoForge"));
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Получаем токен авторизации
|
|
||||||
String accessToken = AuthManager.getAccessToken();
|
|
||||||
if (accessToken == null) {
|
|
||||||
throw new IOException("Не авторизован. Требуется проходка для скачивания сборок.");
|
|
||||||
}
|
|
||||||
if (!AuthManager.canDownloadPacks()) {
|
|
||||||
throw new IOException("Для скачивания сборок требуется активная проходка");
|
|
||||||
}
|
|
||||||
|
|
||||||
String url = ZHttpClient.getBaseUrl() + "/pack/" + packName + "/diff";
|
|
||||||
|
|
||||||
// Используем HttpURLConnection для полного контроля
|
|
||||||
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("Authorization", "Bearer " + accessToken);
|
|
||||||
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();
|
|
||||||
|
|
||||||
// Читаем ответ
|
|
||||||
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();
|
|
||||||
|
|
||||||
if (responseCode == 403) {
|
|
||||||
throw new IOException("Для скачивания сборок требуется активная проходка. Обратитесь к администратору.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (responseCode != 200) {
|
|
||||||
throw new IOException("HTTP " + responseCode + ": " + extractErrorFromResponse(responseBody));
|
|
||||||
}
|
|
||||||
|
|
||||||
return gson.fromJson(responseBody, DiffResponse.class);
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (connection != null) {
|
|
||||||
connection.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String extractErrorFromResponse(String body) {
|
|
||||||
try {
|
|
||||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
|
||||||
if (json.has("detail")) {
|
|
||||||
return json.get("detail").getAsString();
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
return body.length() > 200 ? body.substring(0, 200) + "..." : body;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Применить 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== Вложенные классы ======================
|
|
||||||
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-213
@@ -1,213 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft.installer;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ProgressBar;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
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.time.Duration;
|
|
||||||
|
|
||||||
public class FabricInstaller {
|
|
||||||
|
|
||||||
private final Instance instance;
|
|
||||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(15))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public FabricInstaller(Instance instance) {
|
|
||||||
this.instance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean install(String minecraftVersion, String loaderVersion) throws Exception {
|
|
||||||
System.out.println(ZAnsi.cyan("Установка Fabric " + loaderVersion + " для Minecraft " + minecraftVersion));
|
|
||||||
|
|
||||||
Path instancePath = instance.getPath();
|
|
||||||
cleanOldFabricLoaders();
|
|
||||||
|
|
||||||
VersionInstaller versionInstaller = new VersionInstaller(instancePath);
|
|
||||||
String assetIndex = versionInstaller.install(minecraftVersion);
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.green("Asset index получен: " + assetIndex));
|
|
||||||
|
|
||||||
instance.setAssetIndex(assetIndex);
|
|
||||||
instance.setMinecraftVersion(minecraftVersion);
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
if (!Files.exists(installerJar)) {
|
|
||||||
ProgressBar.show("Скачивание Fabric Installer", 0, 100, "%");
|
|
||||||
downloadFileWithFallback(installerUrl, installerJar);
|
|
||||||
ProgressBar.finish("Fabric Installer скачан");
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Запуск Fabric Installer..."));
|
|
||||||
|
|
||||||
String fabricVersionId = "fabric-loader-" + loaderVersion + "-" + minecraftVersion;
|
|
||||||
|
|
||||||
ProcessBuilder pb = new ProcessBuilder(
|
|
||||||
"java", "-jar", installerJar.toAbsolutePath().toString(),
|
|
||||||
"client",
|
|
||||||
"-dir", instancePath.toAbsolutePath().toString(),
|
|
||||||
"-mcversion", minecraftVersion,
|
|
||||||
"-loader", loaderVersion,
|
|
||||||
"-noprofile"
|
|
||||||
);
|
|
||||||
|
|
||||||
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
|
|
||||||
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
|
|
||||||
|
|
||||||
Process process = pb.start();
|
|
||||||
int exitCode = process.waitFor();
|
|
||||||
|
|
||||||
if (exitCode != 0) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Fabric Installer завершился с ошибкой (код " + exitCode + ")"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path fabricVersionDir = instancePath.resolve("versions").resolve(fabricVersionId);
|
|
||||||
|
|
||||||
if (Files.exists(fabricVersionDir)) {
|
|
||||||
System.out.println(ZAnsi.brightGreen("Fabric успешно установлен!"));
|
|
||||||
|
|
||||||
instance.setLoaderType("fabric");
|
|
||||||
instance.setLoaderVersion(loaderVersion);
|
|
||||||
instance.setFabricVersionId(fabricVersionId); // ← СОХРАНЯЕМ
|
|
||||||
|
|
||||||
ensureAssetIndexInFabricVersion(fabricVersionDir, assetIndex);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("Fabric Installer отработал, но версия не найдена."));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadFileWithFallback(String url, Path target) throws Exception {
|
|
||||||
try {
|
|
||||||
ZHttpClient.downloadFile(url, target);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.yellow("Не удалось скачать Fabric Installer: " + e.getMessage()));
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureAssetIndexInFabricVersion(Path fabricVersionDir, String assetIndex) 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);
|
|
||||||
|
|
||||||
// Проверяем и исправляем asset index
|
|
||||||
if (!content.contains("\"assets\":\"" + assetIndex + "\"")) {
|
|
||||||
System.out.println(ZAnsi.yellow("Исправляем asset index в JSON файле версии..."));
|
|
||||||
|
|
||||||
// Заменяем assets на правильное значение
|
|
||||||
content = content.replaceAll("\"assets\":\\s*\"[^\"]*\"", "\"assets\": \"" + assetIndex + "\"");
|
|
||||||
|
|
||||||
// Также проверяем assetIndex
|
|
||||||
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;
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.yellow("Очистка старых версий Fabric Loader..."));
|
|
||||||
|
|
||||||
try (var stream = Files.walk(librariesDir)) {
|
|
||||||
stream.filter(Files::isDirectory)
|
|
||||||
.filter(dir -> dir.getFileName().toString().matches("\\d+\\.\\d+\\.\\d+.*"))
|
|
||||||
.forEach(dir -> {
|
|
||||||
try {
|
|
||||||
Files.walk(dir)
|
|
||||||
.sorted((a,b) -> b.compareTo(a))
|
|
||||||
.forEach(p -> {
|
|
||||||
try { Files.deleteIfExists(p); }
|
|
||||||
catch (IOException ignored) {}
|
|
||||||
});
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getLatestInstallerVersion() throws Exception {
|
|
||||||
try {
|
|
||||||
// Используем ZHttpClient с умным прокси
|
|
||||||
String xml = ZHttpClient.downloadString("https://maven.fabricmc.net/net/fabricmc/fabric-installer/maven-metadata.xml");
|
|
||||||
int start = xml.indexOf("<latest>") + 8;
|
|
||||||
int end = xml.indexOf("</latest>", start);
|
|
||||||
return xml.substring(start, end).trim();
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.yellow("Ошибка получения версии Fabric Installer: " + e.getMessage()));
|
|
||||||
throw new Exception("Не удалось получить версию Fabric Installer", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// под рефактор оставить
|
|
||||||
private String downloadString(String url) throws Exception {
|
|
||||||
Exception lastException = null;
|
|
||||||
|
|
||||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
|
||||||
try {
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(url))
|
|
||||||
.timeout(Duration.ofSeconds(30 * attempt))
|
|
||||||
.header("User-Agent", "ZernMC-Launcher/1.0")
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<String> resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
if (resp.statusCode() == 200) {
|
|
||||||
return resp.body();
|
|
||||||
}
|
|
||||||
throw new IOException("HTTP " + resp.statusCode());
|
|
||||||
} catch (Exception e) {
|
|
||||||
lastException = e;
|
|
||||||
System.out.println(ZAnsi.yellow("Попытка " + attempt + " не удалась: " + e.getMessage()));
|
|
||||||
if (attempt < 3) {
|
|
||||||
Thread.sleep(1000 * attempt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw lastException;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadFile(String url, Path target) throws Exception {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-270
@@ -1,270 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft.installer;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ProgressBar;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.URI;
|
|
||||||
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.nio.file.StandardOpenOption;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class ForgeInstaller {
|
|
||||||
|
|
||||||
private final Instance instance;
|
|
||||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(java.time.Duration.ofSeconds(30))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public ForgeInstaller(Instance instance) {
|
|
||||||
this.instance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean install(String mcVersion, String forgeVersion) throws Exception {
|
|
||||||
System.out.println(ZAnsi.cyan("Установка Forge " + forgeVersion + " для Minecraft " + mcVersion));
|
|
||||||
|
|
||||||
// Шаг 1: Устанавливаем vanilla и получаем настоящий assetIndex
|
|
||||||
System.out.println(ZAnsi.cyan("Установка базовой версии Minecraft " + mcVersion + "..."));
|
|
||||||
VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath());
|
|
||||||
|
|
||||||
String assetIndex = vanillaInstaller.install(mcVersion);
|
|
||||||
|
|
||||||
if (assetIndex == null || assetIndex.isEmpty()) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Не удалось установить базовую версию Minecraft"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.setAssetIndex(assetIndex);
|
|
||||||
|
|
||||||
// Шаг 2: Создаём launcher_profiles.json
|
|
||||||
createLauncherProfile();
|
|
||||||
|
|
||||||
// Шаг 3: Скачиваем Forge Installer с прогресс-баром
|
|
||||||
String installerUrl = "https://maven.minecraftforge.net/net/minecraftforge/forge/"
|
|
||||||
+ mcVersion + "-" + forgeVersion
|
|
||||||
+ "/forge-" + mcVersion + "-" + forgeVersion + "-installer.jar";
|
|
||||||
|
|
||||||
Path installerJar = instance.getPath().resolve("forge-installer.jar");
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Скачивание Forge Installer..."));
|
|
||||||
downloadFileWithProgress(installerUrl, installerJar);
|
|
||||||
|
|
||||||
// Шаг 4: Запускаем Forge Installer и показываем его вывод
|
|
||||||
System.out.println(ZAnsi.cyan("Запуск Forge Installer..."));
|
|
||||||
System.out.println(ZAnsi.yellow("Это может занять несколько минут. Пожалуйста, подождите...\n"));
|
|
||||||
|
|
||||||
boolean success = runForgeInstaller(installerJar);
|
|
||||||
|
|
||||||
// После успешной установки Forge, но перед сохранением метаданных
|
|
||||||
if (success) {
|
|
||||||
// Докачиваем пропущенные библиотеки
|
|
||||||
try {
|
|
||||||
downloadMissingLibraries(mcVersion, forgeVersion);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.yellow("Предупреждение: не удалось докачать некоторые библиотеки: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("\nForge " + forgeVersion + " успешно установлен!"));
|
|
||||||
instance.setMinecraftVersion(mcVersion);
|
|
||||||
instance.setLoaderType("forge");
|
|
||||||
instance.setLoaderVersion(forgeVersion);
|
|
||||||
|
|
||||||
// Очищаем временный файл установщика
|
|
||||||
Files.deleteIfExists(installerJar);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("\nОшибка при установке Forge!"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createLauncherProfile() throws IOException {
|
|
||||||
Path profilePath = instance.getPath().resolve("launcher_profiles.json");
|
|
||||||
if (Files.exists(profilePath)) return;
|
|
||||||
|
|
||||||
String minimalProfile = """
|
|
||||||
{
|
|
||||||
"profiles": {},
|
|
||||||
"selectedProfile": "Default"
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
Files.writeString(profilePath, minimalProfile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
|
||||||
System.out.println(ZAnsi.yellow("Создан launcher_profiles.json"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadFileWithProgress(String url, Path target) throws Exception {
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(url))
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
throw new IOException("HTTP " + response.statusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
long contentLength = response.headers().firstValueAsLong("Content-Length").orElse(-1);
|
|
||||||
|
|
||||||
try (InputStream in = response.body();
|
|
||||||
FileOutputStream out = new FileOutputStream(target.toFile())) {
|
|
||||||
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
int bytesRead;
|
|
||||||
long totalRead = 0;
|
|
||||||
int lastPercent = -1;
|
|
||||||
|
|
||||||
while ((bytesRead = in.read(buffer)) != -1) {
|
|
||||||
out.write(buffer, 0, bytesRead);
|
|
||||||
totalRead += bytesRead;
|
|
||||||
|
|
||||||
if (contentLength > 0) {
|
|
||||||
int percent = (int) ((totalRead * 100) / contentLength);
|
|
||||||
if (percent != lastPercent) {
|
|
||||||
String downloaded = ProgressBar.formatBytes(totalRead);
|
|
||||||
String total = ProgressBar.formatBytes(contentLength);
|
|
||||||
ProgressBar.show("Forge Installer", percent, 100, "% (" + downloaded + "/" + total + ")");
|
|
||||||
lastPercent = percent;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Если размер неизвестен, показываем анимацию
|
|
||||||
char[] spinner = {'|', '/', '-', '\\'};
|
|
||||||
int idx = (int) (totalRead / 1024) % 4;
|
|
||||||
System.out.print("\rСкачивание Forge Installer: " + ProgressBar.formatBytes(totalRead) + " " + spinner[idx]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressBar.finish("Forge Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean runForgeInstaller(Path installerJar) throws IOException, InterruptedException {
|
|
||||||
// Пробуем до 3 раз с разными опциями
|
|
||||||
int maxRetries = 3;
|
|
||||||
int attempt = 1;
|
|
||||||
|
|
||||||
while (attempt <= maxRetries) {
|
|
||||||
System.out.println(ZAnsi.cyan("Попытка " + attempt + " из " + maxRetries));
|
|
||||||
|
|
||||||
ProcessBuilder pb = new ProcessBuilder(
|
|
||||||
"java",
|
|
||||||
"-jar",
|
|
||||||
installerJar.toAbsolutePath().toString(),
|
|
||||||
"--installClient"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Добавляем JVM аргументы для увеличения таймаутов
|
|
||||||
pb.environment().put("JAVA_OPTS", "-Dhttp.connectionTimeout=60000 -Dhttp.socketTimeout=60000");
|
|
||||||
|
|
||||||
pb.directory(instance.getPath().toFile());
|
|
||||||
pb.redirectErrorStream(true);
|
|
||||||
|
|
||||||
Process process = pb.start();
|
|
||||||
|
|
||||||
// Читаем вывод в реальном времени
|
|
||||||
StringBuilder output = new StringBuilder();
|
|
||||||
boolean hasErrors = false;
|
|
||||||
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
output.append(line).append("\n");
|
|
||||||
|
|
||||||
// Форматируем вывод Forge Installer
|
|
||||||
if (line.contains("Downloading") || line.contains("Extracting")) {
|
|
||||||
System.out.println(ZAnsi.blue(" -> " + line));
|
|
||||||
} else if (line.contains("SUCCESS") || line.contains("successfully")) {
|
|
||||||
System.out.println(ZAnsi.brightGreen(" + " + line));
|
|
||||||
} else if (line.contains("WARNING") || line.contains("warning")) {
|
|
||||||
System.out.println(ZAnsi.yellow(" ! " + line));
|
|
||||||
} else if (line.contains("ERROR") || line.contains("error") || line.contains("failed") || line.contains("timed out")) {
|
|
||||||
System.out.println(ZAnsi.brightRed(" X " + line));
|
|
||||||
if (line.contains("timed out") || line.contains("failed to download")) {
|
|
||||||
hasErrors = true;
|
|
||||||
}
|
|
||||||
} else if (!line.isBlank()) {
|
|
||||||
System.out.println(" " + line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int exitCode = process.waitFor();
|
|
||||||
|
|
||||||
// Если успешно или нет ошибок скачивания
|
|
||||||
if (exitCode == 0 && !hasErrors) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Если ошибка и это не последняя попытка
|
|
||||||
if (attempt < maxRetries) {
|
|
||||||
System.out.println(ZAnsi.yellow("Ошибка при установке. Повторная попытка через 5 секунд..."));
|
|
||||||
Thread.sleep(5000);
|
|
||||||
|
|
||||||
// Очищаем временные файлы перед повтором
|
|
||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
|
||||||
if (Files.exists(librariesDir)) {
|
|
||||||
// Удаляем только частично скачанные библиотеки Forge
|
|
||||||
try (var stream = Files.walk(librariesDir)) {
|
|
||||||
stream.filter(p -> p.toString().contains("asm") && p.toString().endsWith(".jar"))
|
|
||||||
.forEach(p -> {
|
|
||||||
try { Files.deleteIfExists(p); }
|
|
||||||
catch (IOException e) { /* ignore */ }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("Forge Installer завершился с кодом ошибки: " + exitCode));
|
|
||||||
|
|
||||||
// Показываем возможное решение
|
|
||||||
if (output.toString().contains("timed out")) {
|
|
||||||
System.out.println(ZAnsi.yellow("\nВозможные решения:"));
|
|
||||||
System.out.println(ZAnsi.yellow("1. Проверьте интернет-соединение"));
|
|
||||||
System.out.println(ZAnsi.yellow("2. Запустите лаунчер от имени администратора"));
|
|
||||||
System.out.println(ZAnsi.yellow("3. Временно отключите антивирус/брандмауэр"));
|
|
||||||
System.out.println(ZAnsi.yellow("4. Попробуйте установить другую версию Forge"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attempt++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadMissingLibraries(String mcVersion, String forgeVersion) throws Exception {
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка и докачка отсутствующих библиотек..."));
|
|
||||||
|
|
||||||
// Список проблемных библиотек и их альтернативные URL
|
|
||||||
Map<String, String> alternativeUrls = new HashMap<>();
|
|
||||||
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar",
|
|
||||||
"https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar");
|
|
||||||
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar",
|
|
||||||
"https://mirrors.huaweicloud.com/repository/maven/org/ow2/asm/asm/9.6/asm-9.6.jar");
|
|
||||||
|
|
||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
|
||||||
|
|
||||||
for (Map.Entry<String, String> entry : alternativeUrls.entrySet()) {
|
|
||||||
Path target = librariesDir.resolve(entry.getKey());
|
|
||||||
if (!Files.exists(target)) {
|
|
||||||
Files.createDirectories(target.getParent());
|
|
||||||
System.out.println(ZAnsi.yellow("Докачка: " + target.getFileName()));
|
|
||||||
|
|
||||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
|
||||||
try {
|
|
||||||
downloadFileWithProgress(entry.getValue(), target);
|
|
||||||
break;
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (attempt == 3) throw e;
|
|
||||||
System.out.println(ZAnsi.yellow("Повторная попытка " + attempt + "/3..."));
|
|
||||||
Thread.sleep(2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-271
@@ -1,271 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft.installer;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ProgressBar;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.URI;
|
|
||||||
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.nio.file.StandardOpenOption;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class NeoForgeInstaller {
|
|
||||||
|
|
||||||
private final Instance instance;
|
|
||||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(java.time.Duration.ofSeconds(30))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
public NeoForgeInstaller(Instance instance) {
|
|
||||||
this.instance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean install(String mcVersion, String neoForgeVersion) throws Exception {
|
|
||||||
System.out.println(ZAnsi.cyan("Установка NeoForge " + neoForgeVersion + " для Minecraft " + mcVersion));
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Установка базовой версии Minecraft " + mcVersion + "..."));
|
|
||||||
VersionInstaller vanillaInstaller = new VersionInstaller(instance.getPath());
|
|
||||||
String assetIndex = vanillaInstaller.install(mcVersion);
|
|
||||||
|
|
||||||
if (assetIndex == null || assetIndex.isEmpty()) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Не удалось установить базовую версию Minecraft"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
instance.setAssetIndex(assetIndex);
|
|
||||||
createLauncherProfile();
|
|
||||||
|
|
||||||
String mavenGroup = getMavenGroup(mcVersion);
|
|
||||||
String mavenArtifact = getMavenArtifact(mcVersion);
|
|
||||||
|
|
||||||
String installerUrl = "https://maven.neoforged.net/releases/"
|
|
||||||
+ mavenGroup.replace('.', '/') + "/"
|
|
||||||
+ mavenArtifact + "/"
|
|
||||||
+ neoForgeVersion
|
|
||||||
+ "/" + mavenArtifact + "-" + neoForgeVersion + "-installer.jar";
|
|
||||||
|
|
||||||
Path installerJar = instance.getPath().resolve("neoforge-installer.jar");
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Скачивание NeoForge Installer..."));
|
|
||||||
downloadFileWithProgress(installerUrl, installerJar);
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Запуск NeoForge Installer..."));
|
|
||||||
System.out.println(ZAnsi.yellow("Это может занять несколько минут. Пожалуйста, подождите...\n"));
|
|
||||||
|
|
||||||
boolean success = runNeoForgeInstaller(installerJar);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
try {
|
|
||||||
downloadMissingLibraries(mcVersion, neoForgeVersion, mavenGroup, mavenArtifact);
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.yellow("Предупреждение: не удалось докачать некоторые библиотеки: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("\nNeoForge " + neoForgeVersion + " успешно установлен!"));
|
|
||||||
instance.setMinecraftVersion(mcVersion);
|
|
||||||
instance.setLoaderType("neoforge");
|
|
||||||
instance.setLoaderVersion(neoForgeVersion);
|
|
||||||
|
|
||||||
Files.deleteIfExists(installerJar);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("\nОшибка при установке NeoForge!"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getMavenGroup(String mcVersion) {
|
|
||||||
if (mcVersion.equals("1.20.1")) {
|
|
||||||
return "net.neoforged";
|
|
||||||
}
|
|
||||||
return "net.neoforged";
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getMavenArtifact(String mcVersion) {
|
|
||||||
if (mcVersion.equals("1.20.1")) {
|
|
||||||
return "forge";
|
|
||||||
}
|
|
||||||
return "neoforge";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createLauncherProfile() throws IOException {
|
|
||||||
Path profilePath = instance.getPath().resolve("launcher_profiles.json");
|
|
||||||
if (Files.exists(profilePath)) return;
|
|
||||||
|
|
||||||
String minimalProfile = """
|
|
||||||
{
|
|
||||||
"profiles": {},
|
|
||||||
"selectedProfile": "Default"
|
|
||||||
}
|
|
||||||
""";
|
|
||||||
Files.writeString(profilePath, minimalProfile, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
|
||||||
System.out.println(ZAnsi.yellow("Создан launcher_profiles.json"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadFileWithProgress(String url, Path target) throws Exception {
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(url))
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
|
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
throw new IOException("HTTP " + response.statusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
long contentLength = response.headers().firstValueAsLong("Content-Length").orElse(-1);
|
|
||||||
|
|
||||||
try (InputStream in = response.body();
|
|
||||||
FileOutputStream out = new FileOutputStream(target.toFile())) {
|
|
||||||
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
int bytesRead;
|
|
||||||
long totalRead = 0;
|
|
||||||
int lastPercent = -1;
|
|
||||||
|
|
||||||
while ((bytesRead = in.read(buffer)) != -1) {
|
|
||||||
out.write(buffer, 0, bytesRead);
|
|
||||||
totalRead += bytesRead;
|
|
||||||
|
|
||||||
if (contentLength > 0) {
|
|
||||||
int percent = (int) ((totalRead * 100) / contentLength);
|
|
||||||
if (percent != lastPercent) {
|
|
||||||
String downloaded = ProgressBar.formatBytes(totalRead);
|
|
||||||
String total = ProgressBar.formatBytes(contentLength);
|
|
||||||
ProgressBar.show("NeoForge Installer", percent, 100, "% (" + downloaded + "/" + total + ")");
|
|
||||||
lastPercent = percent;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
char[] spinner = {'|', '/', '-', '\\'};
|
|
||||||
int idx = (int) (totalRead / 1024) % 4;
|
|
||||||
System.out.print("\rСкачивание NeoForge Installer: " + ProgressBar.formatBytes(totalRead) + " " + spinner[idx]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressBar.finish("NeoForge Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean runNeoForgeInstaller(Path installerJar) throws IOException, InterruptedException {
|
|
||||||
int maxRetries = 3;
|
|
||||||
int attempt = 1;
|
|
||||||
|
|
||||||
while (attempt <= maxRetries) {
|
|
||||||
System.out.println(ZAnsi.cyan("Попытка " + attempt + " из " + maxRetries));
|
|
||||||
|
|
||||||
ProcessBuilder pb = new ProcessBuilder(
|
|
||||||
"java",
|
|
||||||
"-jar",
|
|
||||||
installerJar.toAbsolutePath().toString(),
|
|
||||||
"--installClient"
|
|
||||||
);
|
|
||||||
|
|
||||||
pb.environment().put("JAVA_OPTS", "-Dhttp.connectionTimeout=60000 -Dhttp.socketTimeout=60000");
|
|
||||||
pb.directory(instance.getPath().toFile());
|
|
||||||
pb.redirectErrorStream(true);
|
|
||||||
|
|
||||||
Process process = pb.start();
|
|
||||||
|
|
||||||
StringBuilder output = new StringBuilder();
|
|
||||||
boolean hasErrors = false;
|
|
||||||
|
|
||||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
output.append(line).append("\n");
|
|
||||||
|
|
||||||
if (line.contains("Downloading") || line.contains("Extracting")) {
|
|
||||||
System.out.println(ZAnsi.blue(" -> " + line));
|
|
||||||
} else if (line.contains("SUCCESS") || line.contains("successfully")) {
|
|
||||||
System.out.println(ZAnsi.brightGreen(" + " + line));
|
|
||||||
} else if (line.contains("WARNING") || line.contains("warning")) {
|
|
||||||
System.out.println(ZAnsi.yellow(" ! " + line));
|
|
||||||
} else if (line.contains("ERROR") || line.contains("error") || line.contains("failed") || line.contains("timed out")) {
|
|
||||||
System.out.println(ZAnsi.brightRed(" X " + line));
|
|
||||||
if (line.contains("timed out") || line.contains("failed to download")) {
|
|
||||||
hasErrors = true;
|
|
||||||
}
|
|
||||||
} else if (!line.isBlank()) {
|
|
||||||
System.out.println(" " + line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int exitCode = process.waitFor();
|
|
||||||
|
|
||||||
if (exitCode == 0 && !hasErrors) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempt < maxRetries) {
|
|
||||||
System.out.println(ZAnsi.yellow("Ошибка при установке. Повторная попытка через 5 секунд..."));
|
|
||||||
Thread.sleep(5000);
|
|
||||||
|
|
||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
|
||||||
if (Files.exists(librariesDir)) {
|
|
||||||
try (var stream = Files.walk(librariesDir)) {
|
|
||||||
stream.filter(p -> p.toString().contains("asm") && p.toString().endsWith(".jar"))
|
|
||||||
.forEach(p -> {
|
|
||||||
try { Files.deleteIfExists(p); }
|
|
||||||
catch (IOException e) { /* ignore */ }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.brightRed("NeoForge Installer завершился с кодом ошибки: " + exitCode));
|
|
||||||
|
|
||||||
if (output.toString().contains("timed out")) {
|
|
||||||
System.out.println(ZAnsi.yellow("\nВозможные решения:"));
|
|
||||||
System.out.println(ZAnsi.yellow("1. Проверьте интернет-соединение"));
|
|
||||||
System.out.println(ZAnsi.yellow("2. Запустите лаунчер от имени администратора"));
|
|
||||||
System.out.println(ZAnsi.yellow("3. Временно отключите антивирус/брандмауэр"));
|
|
||||||
System.out.println(ZAnsi.yellow("4. Попробуйте установить другую версию NeoForge"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
attempt++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadMissingLibraries(String mcVersion, String neoForgeVersion, String mavenGroup, String mavenArtifact) throws Exception {
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка и докачка отсутствующих библиотек..."));
|
|
||||||
|
|
||||||
Map<String, String> alternativeUrls = new HashMap<>();
|
|
||||||
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar",
|
|
||||||
"https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar");
|
|
||||||
alternativeUrls.put("org/ow2/asm/asm-commons/9.6/asm-commons-9.6.jar",
|
|
||||||
"https://repo1.maven.org/maven2/org/ow2/asm/asm-commons/9.6/asm-commons-9.6.jar");
|
|
||||||
alternativeUrls.put("org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar",
|
|
||||||
"https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar");
|
|
||||||
|
|
||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
|
||||||
|
|
||||||
for (Map.Entry<String, String> entry : alternativeUrls.entrySet()) {
|
|
||||||
Path target = librariesDir.resolve(entry.getKey());
|
|
||||||
if (!Files.exists(target)) {
|
|
||||||
Files.createDirectories(target.getParent());
|
|
||||||
System.out.println(ZAnsi.yellow("Докачка: " + target.getFileName()));
|
|
||||||
|
|
||||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
|
||||||
try {
|
|
||||||
downloadFileWithProgress(entry.getValue(), target);
|
|
||||||
break;
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (attempt == 3) throw e;
|
|
||||||
System.out.println(ZAnsi.yellow("Повторная попытка " + attempt + "/3..."));
|
|
||||||
Thread.sleep(2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-245
@@ -1,245 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft.installer;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.model.MinecraftVersion;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ProgressBar;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
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.time.Duration;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
public class VersionInstaller {
|
|
||||||
|
|
||||||
private final Path minecraftDir;
|
|
||||||
private final HttpClient httpClient;
|
|
||||||
private final ExecutorService executor = Executors.newFixedThreadPool(32);
|
|
||||||
|
|
||||||
public VersionInstaller(Path minecraftDir) {
|
|
||||||
this.minecraftDir = minecraftDir;
|
|
||||||
this.httpClient = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(15))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MinecraftVersion> getAvailableVersions() throws Exception {
|
|
||||||
String jsonString = downloadString("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json");
|
|
||||||
JSONObject root = new JSONObject(jsonString);
|
|
||||||
JSONArray versionsArray = root.getJSONArray("versions");
|
|
||||||
|
|
||||||
List<MinecraftVersion> versions = new ArrayList<>();
|
|
||||||
|
|
||||||
for (int i = 0; i < versionsArray.length(); i++) {
|
|
||||||
JSONObject v = versionsArray.getJSONObject(i);
|
|
||||||
String id = v.getString("id");
|
|
||||||
String type = v.getString("type");
|
|
||||||
String releaseTimeStr = v.getString("releaseTime").replace("Z", "").replace("+00:00", "");
|
|
||||||
String url = v.getString("url");
|
|
||||||
|
|
||||||
LocalDateTime releaseTime = LocalDateTime.parse(releaseTimeStr);
|
|
||||||
versions.add(new MinecraftVersion(id, type, releaseTime, url));
|
|
||||||
}
|
|
||||||
|
|
||||||
versions.sort((a, b) -> b.getReleaseTime().compareTo(a.getReleaseTime()));
|
|
||||||
return versions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String install(String versionId) throws Exception {
|
|
||||||
System.out.println(ZAnsi.cyan("Полная установка Minecraft " + versionId + "..."));
|
|
||||||
Path versionDir = minecraftDir.resolve("versions").resolve(versionId);
|
|
||||||
Files.createDirectories(versionDir);
|
|
||||||
|
|
||||||
String versionUrl = getVersionUrl(versionId);
|
|
||||||
if (versionUrl == null) throw new Exception("Версия " + versionId + " не найдена");
|
|
||||||
|
|
||||||
String versionJson = downloadString(versionUrl);
|
|
||||||
Files.writeString(versionDir.resolve(versionId + ".json"), versionJson);
|
|
||||||
|
|
||||||
JSONObject versionData = new JSONObject(versionJson);
|
|
||||||
|
|
||||||
// client.jar
|
|
||||||
downloadFile(versionData.getJSONObject("downloads").getJSONObject("client").getString("url"),
|
|
||||||
versionDir.resolve(versionId + ".jar"), "client.jar");
|
|
||||||
|
|
||||||
// Библиотеки
|
|
||||||
System.out.println(ZAnsi.cyan("Скачивание библиотек..."));
|
|
||||||
downloadLibraries(versionData.getJSONArray("libraries"));
|
|
||||||
|
|
||||||
String assetIndex;
|
|
||||||
if (versionData.has("assetIndex")) {
|
|
||||||
assetIndex = versionData.getJSONObject("assetIndex").getString("id");
|
|
||||||
} else {
|
|
||||||
assetIndex = versionData.getString("assets");
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Asset index: " + assetIndex));
|
|
||||||
|
|
||||||
// Скачиваем ассеты используя правильный индекс
|
|
||||||
System.out.println(ZAnsi.cyan("Скачивание ассетов..."));
|
|
||||||
downloadAssets(versionData, assetIndex);
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("\nMinecraft " + versionId + " полностью установлен!"));
|
|
||||||
return assetIndex; // ← возвращаем "5" а не "1.20.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadLibraries(JSONArray libraries) throws Exception {
|
|
||||||
int total = libraries.length();
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
|
||||||
JSONObject lib = libraries.getJSONObject(i);
|
|
||||||
if (lib.has("downloads") && lib.getJSONObject("downloads").has("artifact")) {
|
|
||||||
JSONObject art = lib.getJSONObject("downloads").getJSONObject("artifact");
|
|
||||||
String url = art.getString("url");
|
|
||||||
String path = art.getString("path");
|
|
||||||
|
|
||||||
Path target = minecraftDir.resolve("libraries").resolve(path);
|
|
||||||
Files.createDirectories(target.getParent());
|
|
||||||
|
|
||||||
try {
|
|
||||||
downloadFile(url, target, "library");
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Пропускаем проблемные библиотеки
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count++;
|
|
||||||
ProgressBar.show("Библиотеки", count, total, "файлов");
|
|
||||||
}
|
|
||||||
ProgressBar.finish("Библиотеки загружены");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadAssets(JSONObject versionData, String assetIndex) throws Exception {
|
|
||||||
// Находим URL для asset index
|
|
||||||
JSONObject assetIndexInfo = versionData.getJSONObject("assetIndex");
|
|
||||||
String indexUrl = assetIndexInfo.getString("url");
|
|
||||||
|
|
||||||
Path indexesDir = minecraftDir.resolve("assets/indexes");
|
|
||||||
Files.createDirectories(indexesDir);
|
|
||||||
Path indexPath = indexesDir.resolve(assetIndex + ".json"); // ← используем assetIndex
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Скачивание asset index (" + assetIndex + ")..."));
|
|
||||||
downloadFile(indexUrl, indexPath, "asset index");
|
|
||||||
|
|
||||||
String jsonContent = Files.readString(indexPath);
|
|
||||||
JSONObject root = new JSONObject(jsonContent);
|
|
||||||
JSONObject objects = root.getJSONObject("objects");
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Скачивание " + objects.length() + " объектов ассетов (index: " + assetIndex + ")..."));
|
|
||||||
|
|
||||||
int total = objects.length();
|
|
||||||
int[] success = {0};
|
|
||||||
int[] failed = {0};
|
|
||||||
|
|
||||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
|
||||||
|
|
||||||
for (String key : objects.keySet()) {
|
|
||||||
JSONObject asset = objects.getJSONObject(key);
|
|
||||||
String hash = asset.getString("hash"); // ← вот это правильный хеш!
|
|
||||||
|
|
||||||
String url = "https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash;
|
|
||||||
Path target = minecraftDir.resolve("assets/objects")
|
|
||||||
.resolve(hash.substring(0, 2))
|
|
||||||
.resolve(hash);
|
|
||||||
|
|
||||||
Files.createDirectories(target.getParent());
|
|
||||||
|
|
||||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
|
|
||||||
boolean downloaded = false;
|
|
||||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
|
||||||
try {
|
|
||||||
downloadFile(url, target, "");
|
|
||||||
synchronized (this) {
|
|
||||||
success[0]++;
|
|
||||||
ProgressBar.show("Ассеты", success[0], total, "файлов");
|
|
||||||
}
|
|
||||||
downloaded = true;
|
|
||||||
break;
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (attempt == 3) {
|
|
||||||
synchronized (this) {
|
|
||||||
failed[0]++;
|
|
||||||
}
|
|
||||||
System.err.println("Не удалось скачать " + hash);
|
|
||||||
} else {
|
|
||||||
try { Thread.sleep(500 * attempt); } catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, executor);
|
|
||||||
|
|
||||||
futures.add(future);
|
|
||||||
}
|
|
||||||
|
|
||||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
|
||||||
|
|
||||||
ProgressBar.finish("Ассеты загружены (" + success[0] + " успешно, " + failed[0] + " пропущено)");
|
|
||||||
|
|
||||||
if (failed[0] > 0) {
|
|
||||||
System.out.println(ZAnsi.yellow("Предупреждение: " + failed[0] + " файлов ассетов не удалось скачать."));
|
|
||||||
System.out.println(ZAnsi.yellow("Игра запустится, но некоторые текстуры/звуки могут отсутствовать."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAssetIndexId(String versionId) throws Exception {
|
|
||||||
String versionUrl = getVersionUrl(versionId);
|
|
||||||
if (versionUrl == null) throw new Exception("Версия не найдена");
|
|
||||||
|
|
||||||
String versionJson = downloadString(versionUrl);
|
|
||||||
JSONObject versionData = new JSONObject(versionJson);
|
|
||||||
|
|
||||||
if (versionData.has("assetIndex") && versionData.getJSONObject("assetIndex").has("id")) {
|
|
||||||
return versionData.getJSONObject("assetIndex").getString("id"); // "5" для 1.20.1
|
|
||||||
}
|
|
||||||
return versionData.getString("assets"); // fallback (очень старые версии)
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getVersionUrl(String versionId) throws Exception {
|
|
||||||
for (MinecraftVersion v : getAvailableVersions()) {
|
|
||||||
if (v.getId().equals(versionId)) return v.getUrl();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String downloadString(String url) throws Exception {
|
|
||||||
HttpRequest req = HttpRequest.newBuilder().uri(URI.create(url)).GET().build();
|
|
||||||
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
|
|
||||||
if (resp.statusCode() != 200) throw new IOException("HTTP " + resp.statusCode());
|
|
||||||
return resp.body();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void downloadFile(String url, Path target, String label) throws Exception {
|
|
||||||
if (!label.isEmpty()) {
|
|
||||||
ProgressBar.clearLine();
|
|
||||||
System.out.println(ZAnsi.cyan("Скачивание " + label + "..."));
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(url))
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target));
|
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
if (label.isEmpty()) return; // для ассетов молча
|
|
||||||
throw new IOException("HTTP " + response.statusCode() + " при скачивании " + label);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!label.isEmpty()) {
|
|
||||||
long size = Files.size(target);
|
|
||||||
ProgressBar.finish(label + " (" + ProgressBar.formatBytes(size) + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-426
@@ -1,426 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft.launch;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class LaunchCommandBuilder {
|
|
||||||
|
|
||||||
private final Instance instance;
|
|
||||||
|
|
||||||
public LaunchCommandBuilder(Instance instance) {
|
|
||||||
this.instance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> build(LaunchOptions options) throws Exception {
|
|
||||||
System.out.println(ZAnsi.cyan("Генерация команды запуска для " + instance.getName() + "..."));
|
|
||||||
|
|
||||||
List<String> command = new ArrayList<>();
|
|
||||||
|
|
||||||
String javaPath = "java";
|
|
||||||
command.add(javaPath);
|
|
||||||
|
|
||||||
command.addAll(getJvmArguments(options));
|
|
||||||
|
|
||||||
Path nativesDir = instance.getPath().resolve("natives");
|
|
||||||
if (!Files.exists(nativesDir)) {
|
|
||||||
Files.createDirectories(nativesDir);
|
|
||||||
}
|
|
||||||
command.add("-Djava.library.path=" + nativesDir.toAbsolutePath());
|
|
||||||
|
|
||||||
VersionManifest manifest = resolveVersionManifest();
|
|
||||||
if (manifest != null) {
|
|
||||||
command.add("-cp");
|
|
||||||
command.add(buildClasspathFromManifest(manifest));
|
|
||||||
|
|
||||||
String mainClass = resolveMainClass(manifest);
|
|
||||||
command.add(mainClass);
|
|
||||||
|
|
||||||
command.addAll(resolveGameArguments(manifest, options));
|
|
||||||
} else {
|
|
||||||
command.add("-cp");
|
|
||||||
command.add(buildVanillaClasspath());
|
|
||||||
command.add(getVanillaMainClass());
|
|
||||||
command.addAll(getVanillaGameArguments(options));
|
|
||||||
}
|
|
||||||
|
|
||||||
return command;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VersionManifest resolveVersionManifest() {
|
|
||||||
try {
|
|
||||||
Path versionJson = findVersionJson();
|
|
||||||
if (versionJson != null && Files.exists(versionJson)) {
|
|
||||||
String content = Files.readString(versionJson);
|
|
||||||
JSONObject json = new JSONObject(content);
|
|
||||||
System.out.println(ZAnsi.green("Найден version.json: " + versionJson.getFileName()));
|
|
||||||
return new VersionManifest(json);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println(ZAnsi.yellow("Не удалось загрузить version.json: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path findVersionJson() {
|
|
||||||
Path versionsDir = instance.getPath().resolve("versions");
|
|
||||||
String loaderType = instance.getLoaderType().toLowerCase();
|
|
||||||
String mcVersion = instance.getMinecraftVersion();
|
|
||||||
String loaderVersion = instance.getLoaderVersion();
|
|
||||||
|
|
||||||
if ("forge".equals(loaderType) || "neoforge".equals(loaderType)) {
|
|
||||||
String[] candidates = {
|
|
||||||
getVersionId(),
|
|
||||||
mcVersion + "-" + loaderType + "-" + loaderVersion,
|
|
||||||
loaderType + "-" + loaderVersion,
|
|
||||||
mcVersion + "-" + loaderVersion,
|
|
||||||
mcVersion
|
|
||||||
};
|
|
||||||
for (String candidate : candidates) {
|
|
||||||
Path jsonPath = versionsDir.resolve(candidate).resolve(candidate + ".json");
|
|
||||||
if (Files.exists(jsonPath)) {
|
|
||||||
return jsonPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Files.exists(versionsDir)) {
|
|
||||||
try (var stream = Files.list(versionsDir)) {
|
|
||||||
return stream
|
|
||||||
.filter(Files::isDirectory)
|
|
||||||
.filter(dir -> dir.getFileName().toString().contains("forge") ||
|
|
||||||
dir.getFileName().toString().contains("neoforge"))
|
|
||||||
.filter(dir -> dir.getFileName().toString().contains(mcVersion))
|
|
||||||
.findFirst()
|
|
||||||
.map(dir -> dir.resolve(dir.getFileName().toString() + ".json"))
|
|
||||||
.filter(Files::exists)
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Path fallback = versionsDir.resolve(mcVersion).resolve(mcVersion + ".json");
|
|
||||||
if (Files.exists(fallback)) {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getVersionId() {
|
|
||||||
String loaderType = instance.getLoaderType().toLowerCase();
|
|
||||||
String mcVersion = instance.getMinecraftVersion();
|
|
||||||
String loaderVer = instance.getLoaderVersion();
|
|
||||||
|
|
||||||
if ("vanilla".equals(loaderType)) {
|
|
||||||
return mcVersion;
|
|
||||||
}
|
|
||||||
else if ("fabric".equals(loaderType)) {
|
|
||||||
String fabricId = instance.getFabricVersionId();
|
|
||||||
if (fabricId != null && !fabricId.isEmpty()) {
|
|
||||||
return fabricId;
|
|
||||||
}
|
|
||||||
return "fabric-loader-" + loaderVer + "-" + mcVersion;
|
|
||||||
}
|
|
||||||
else if ("forge".equals(loaderType)) {
|
|
||||||
return mcVersion + "-forge-" + loaderVer;
|
|
||||||
}
|
|
||||||
else if ("neoforge".equals(loaderType)) {
|
|
||||||
if (mcVersion.equals("1.20.1")) {
|
|
||||||
return mcVersion + "-neoforge-" + loaderVer;
|
|
||||||
}
|
|
||||||
return "neoforge-" + loaderVer;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mcVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveMainClass(VersionManifest manifest) {
|
|
||||||
return manifest.getMainClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getVanillaMainClass() {
|
|
||||||
String loaderType = instance.getLoaderType().toLowerCase();
|
|
||||||
if ("fabric".equals(loaderType)) {
|
|
||||||
return "net.fabricmc.loader.impl.launch.knot.KnotClient";
|
|
||||||
}
|
|
||||||
return "net.minecraft.client.main.Main";
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> resolveGameArguments(VersionManifest manifest, LaunchOptions options) {
|
|
||||||
List<String> args = new ArrayList<>();
|
|
||||||
Map<String, String> vars = buildVariableMap(options);
|
|
||||||
|
|
||||||
for (String raw : manifest.getGameArguments()) {
|
|
||||||
args.add(resolveVariable(raw, vars));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.getWidth() > 0) {
|
|
||||||
args.add("--width");
|
|
||||||
args.add(String.valueOf(options.getWidth()));
|
|
||||||
}
|
|
||||||
if (options.getHeight() > 0) {
|
|
||||||
args.add("--height");
|
|
||||||
args.add(String.valueOf(options.getHeight()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getVanillaGameArguments(LaunchOptions options) {
|
|
||||||
List<String> args = new ArrayList<>();
|
|
||||||
|
|
||||||
args.add("--version");
|
|
||||||
args.add(instance.getName());
|
|
||||||
args.add("--gameDir");
|
|
||||||
args.add(instance.getPath().toAbsolutePath().toString());
|
|
||||||
args.add("--assetsDir");
|
|
||||||
args.add(instance.getPath().resolve("assets").toAbsolutePath().toString());
|
|
||||||
args.add("--assetIndex");
|
|
||||||
String assetIndex = instance.getAssetIndex();
|
|
||||||
if (assetIndex == null || assetIndex.isEmpty()) {
|
|
||||||
assetIndex = instance.getMinecraftVersion();
|
|
||||||
System.out.println(ZAnsi.yellow("Asset index не найден, использую версию: " + assetIndex));
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.green("Использую asset index: " + assetIndex));
|
|
||||||
}
|
|
||||||
args.add(assetIndex);
|
|
||||||
args.add("--username");
|
|
||||||
args.add(options.getUsername() != null ? options.getUsername() : "Player");
|
|
||||||
args.add("--accessToken");
|
|
||||||
args.add(options.getAccessToken() != null ? options.getAccessToken() : "0");
|
|
||||||
args.add("--uuid");
|
|
||||||
args.add(options.getUuid() != null ? options.getUuid() : "00000000-0000-0000-0000-000000000000");
|
|
||||||
args.add("--userType");
|
|
||||||
args.add("legacy");
|
|
||||||
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, String> buildVariableMap(LaunchOptions options) {
|
|
||||||
Map<String, String> vars = new HashMap<>();
|
|
||||||
|
|
||||||
Path gameDir = instance.getPath().toAbsolutePath();
|
|
||||||
Path assetsDir = gameDir.resolve("assets");
|
|
||||||
Path nativesDir = gameDir.resolve("natives");
|
|
||||||
Path librariesDir = gameDir.resolve("libraries");
|
|
||||||
|
|
||||||
vars.put("version_name", instance.getName());
|
|
||||||
vars.put("game_directory", gameDir.toString());
|
|
||||||
vars.put("assets_root", assetsDir.toString());
|
|
||||||
vars.put("assets_index_name", instance.getAssetIndex() != null ? instance.getAssetIndex() : instance.getMinecraftVersion());
|
|
||||||
vars.put("auth_player_name", options.getUsername() != null ? options.getUsername() : "Player");
|
|
||||||
vars.put("auth_access_token", options.getAccessToken() != null ? options.getAccessToken() : "0");
|
|
||||||
vars.put("auth_uuid", options.getUuid() != null ? options.getUuid() : "00000000-0000-0000-0000-000000000000");
|
|
||||||
vars.put("auth_xuid", "0");
|
|
||||||
vars.put("user_type", "legacy");
|
|
||||||
vars.put("version_type", "release");
|
|
||||||
vars.put("natives_directory", nativesDir.toString());
|
|
||||||
vars.put("library_directory", librariesDir.toString());
|
|
||||||
vars.put("launcher_name", "ZernMC");
|
|
||||||
vars.put("launcher_version", "1.0");
|
|
||||||
vars.put("classpath_separator", System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":");
|
|
||||||
vars.put("resolution_width", String.valueOf(options.getWidth() > 0 ? options.getWidth() : 1920));
|
|
||||||
vars.put("resolution_height", String.valueOf(options.getHeight() > 0 ? options.getHeight() : 1080));
|
|
||||||
vars.put("game_directory", gameDir.toString());
|
|
||||||
|
|
||||||
String loaderType = instance.getLoaderType().toLowerCase();
|
|
||||||
if ("forge".equals(loaderType)) {
|
|
||||||
vars.put("forge_version", instance.getLoaderVersion() != null ? instance.getLoaderVersion() : "");
|
|
||||||
} else if ("neoforge".equals(loaderType)) {
|
|
||||||
vars.put("neoforge_version", instance.getLoaderVersion() != null ? instance.getLoaderVersion() : "");
|
|
||||||
vars.put("fml.neoForgeVersion", instance.getLoaderVersion() != null ? instance.getLoaderVersion() : "");
|
|
||||||
vars.put("fml.neoForgeGroup", "net.neoforged");
|
|
||||||
}
|
|
||||||
|
|
||||||
return vars;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveVariable(String raw, Map<String, String> vars) {
|
|
||||||
if (!raw.contains("${")) return raw;
|
|
||||||
String result = raw;
|
|
||||||
for (Map.Entry<String, String> entry : vars.entrySet()) {
|
|
||||||
result = result.replace("${" + entry.getKey() + "}", entry.getValue());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildClasspathFromManifest(VersionManifest manifest) throws Exception {
|
|
||||||
List<String> paths = new ArrayList<>();
|
|
||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
|
||||||
|
|
||||||
for (VersionManifest.Library lib : manifest.getLibraries()) {
|
|
||||||
Path libPath = librariesDir.resolve(lib.artifactPath);
|
|
||||||
if (Files.exists(libPath)) {
|
|
||||||
paths.add(libPath.toAbsolutePath().toString());
|
|
||||||
} else {
|
|
||||||
String mavenPath = mavenToPath(lib.name);
|
|
||||||
Path fallbackPath = librariesDir.resolve(mavenPath);
|
|
||||||
if (Files.exists(fallbackPath)) {
|
|
||||||
paths.add(fallbackPath.toAbsolutePath().toString());
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.yellow(" Библиотека не найдена: " + lib.name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Path versionJar = findVersionJar();
|
|
||||||
if (versionJar != null) {
|
|
||||||
paths.add(0, versionJar.toAbsolutePath().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
String separator = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
|
|
||||||
return String.join(separator, paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildVanillaClasspath() throws Exception {
|
|
||||||
List<String> paths = new ArrayList<>();
|
|
||||||
String versionId = getVersionId();
|
|
||||||
Path versionJar = instance.getPath()
|
|
||||||
.resolve("versions")
|
|
||||||
.resolve(versionId)
|
|
||||||
.resolve(versionId + ".jar");
|
|
||||||
|
|
||||||
if (Files.exists(versionJar)) {
|
|
||||||
paths.add(versionJar.toAbsolutePath().toString());
|
|
||||||
} else {
|
|
||||||
String mcVersion = instance.getMinecraftVersion();
|
|
||||||
Path fallbackJar = instance.getPath()
|
|
||||||
.resolve("versions")
|
|
||||||
.resolve(mcVersion)
|
|
||||||
.resolve(mcVersion + ".jar");
|
|
||||||
if (Files.exists(fallbackJar)) {
|
|
||||||
paths.add(fallbackJar.toAbsolutePath().toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
|
||||||
if (Files.exists(librariesDir)) {
|
|
||||||
try (var stream = Files.walk(librariesDir)) {
|
|
||||||
stream.filter(p -> p.toString().endsWith(".jar"))
|
|
||||||
.map(p -> p.toAbsolutePath().toString())
|
|
||||||
.forEach(paths::add);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String separator = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
|
|
||||||
return String.join(separator, paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path findVersionJar() {
|
|
||||||
String versionId = getVersionId();
|
|
||||||
Path versionsDir = instance.getPath().resolve("versions");
|
|
||||||
|
|
||||||
Path[] candidates = {
|
|
||||||
versionsDir.resolve(versionId).resolve(versionId + ".jar"),
|
|
||||||
versionsDir.resolve(instance.getMinecraftVersion()).resolve(instance.getMinecraftVersion() + ".jar")
|
|
||||||
};
|
|
||||||
|
|
||||||
for (Path candidate : candidates) {
|
|
||||||
if (Files.exists(candidate)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Files.exists(versionsDir)) {
|
|
||||||
try (var stream = Files.list(versionsDir)) {
|
|
||||||
return stream
|
|
||||||
.filter(Files::isDirectory)
|
|
||||||
.filter(dir -> dir.getFileName().toString().contains("forge") ||
|
|
||||||
dir.getFileName().toString().contains("neoforge"))
|
|
||||||
.filter(dir -> dir.getFileName().toString().contains(instance.getMinecraftVersion()))
|
|
||||||
.findFirst()
|
|
||||||
.map(dir -> dir.resolve(dir.getFileName().toString() + ".jar"))
|
|
||||||
.filter(Files::exists)
|
|
||||||
.orElse(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String mavenToPath(String mavenName) {
|
|
||||||
String[] parts = mavenName.split(":");
|
|
||||||
if (parts.length < 3) return mavenName;
|
|
||||||
|
|
||||||
String group = parts[0].replace('.', '/');
|
|
||||||
String artifact = parts[1];
|
|
||||||
String version = parts[2];
|
|
||||||
|
|
||||||
if (parts.length == 4) {
|
|
||||||
String classifier = parts[3];
|
|
||||||
return group + "/" + artifact + "/" + version + "/" + artifact + "-" + version + "-" + classifier + ".jar";
|
|
||||||
}
|
|
||||||
|
|
||||||
return group + "/" + artifact + "/" + version + "/" + artifact + "-" + version + ".jar";
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getJvmArguments(LaunchOptions options) {
|
|
||||||
List<String> jvmArgs = new ArrayList<>();
|
|
||||||
|
|
||||||
int ramMB = options.getMaxMemory() > 0 ? options.getMaxMemory() : 4096;
|
|
||||||
jvmArgs.add("-Xmx" + ramMB + "M");
|
|
||||||
jvmArgs.add("-Xms" + Math.max(512, ramMB / 2) + "M");
|
|
||||||
|
|
||||||
jvmArgs.add("-XX:+UseG1GC");
|
|
||||||
jvmArgs.add("-XX:+UnlockExperimentalVMOptions");
|
|
||||||
jvmArgs.add("-XX:G1NewSizePercent=20");
|
|
||||||
jvmArgs.add("-XX:G1ReservePercent=20");
|
|
||||||
jvmArgs.add("-XX:MaxGCPauseMillis=50");
|
|
||||||
jvmArgs.add("-XX:G1HeapRegionSize=32M");
|
|
||||||
|
|
||||||
String loaderType = instance.getLoaderType().toLowerCase();
|
|
||||||
|
|
||||||
if ("fabric".equals(loaderType)) {
|
|
||||||
jvmArgs.add("--add-modules=ALL-MODULE-PATH");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.io=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.util=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.lang=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.net=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED");
|
|
||||||
} else if ("forge".equals(loaderType)) {
|
|
||||||
jvmArgs.add("--add-modules=ALL-MODULE-PATH");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.util.jar=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.io=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.net=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.util=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED");
|
|
||||||
} else if ("neoforge".equals(loaderType)) {
|
|
||||||
jvmArgs.add("--add-modules=ALL-MODULE-PATH");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.util.jar=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.lang.invoke=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.io=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.net=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/java.util=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=java.base/sun.nio.ch=ALL-UNNAMED");
|
|
||||||
jvmArgs.add("--add-opens=jdk.unsupported/sun.misc=ALL-UNNAMED");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.getExtraJvmArgs() != null && !options.getExtraJvmArgs().isEmpty()) {
|
|
||||||
jvmArgs.addAll(options.getExtraJvmArgs());
|
|
||||||
}
|
|
||||||
|
|
||||||
return jvmArgs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-165
@@ -1,165 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft.launch;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class VersionManifest {
|
|
||||||
|
|
||||||
private final String id;
|
|
||||||
private final String mainClass;
|
|
||||||
private final String assetIndexId;
|
|
||||||
private final List<String> jvmArguments;
|
|
||||||
private final List<String> gameArguments;
|
|
||||||
private final List<Library> libraries;
|
|
||||||
|
|
||||||
public VersionManifest(JSONObject json) {
|
|
||||||
this.id = json.getString("id");
|
|
||||||
this.mainClass = json.getString("mainClass");
|
|
||||||
|
|
||||||
if (json.has("assetIndex")) {
|
|
||||||
JSONObject ai = json.getJSONObject("assetIndex");
|
|
||||||
this.assetIndexId = ai.has("id") ? ai.getString("id") : "unknown";
|
|
||||||
} else {
|
|
||||||
this.assetIndexId = "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
this.jvmArguments = parseArguments(json, "jvm");
|
|
||||||
this.gameArguments = parseArguments(json, "game");
|
|
||||||
this.libraries = parseLibraries(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId() { return id; }
|
|
||||||
public String getMainClass() { return mainClass; }
|
|
||||||
public String getAssetIndexId() { return assetIndexId; }
|
|
||||||
public List<String> getJvmArguments() { return jvmArguments; }
|
|
||||||
public List<String> getGameArguments() { return gameArguments; }
|
|
||||||
public List<Library> getLibraries() { return libraries; }
|
|
||||||
|
|
||||||
private List<String> parseArguments(JSONObject json, String type) {
|
|
||||||
List<String> args = new ArrayList<>();
|
|
||||||
if (!json.has("arguments")) return args;
|
|
||||||
|
|
||||||
JSONObject arguments = json.getJSONObject("arguments");
|
|
||||||
if (!arguments.has(type)) return args;
|
|
||||||
|
|
||||||
JSONArray arr = arguments.getJSONArray(type);
|
|
||||||
for (int i = 0; i < arr.length(); i++) {
|
|
||||||
Object item = arr.get(i);
|
|
||||||
if (item instanceof String) {
|
|
||||||
args.add((String) item);
|
|
||||||
} else if (item instanceof JSONObject) {
|
|
||||||
JSONObject ruleObj = (JSONObject) item;
|
|
||||||
if (ruleMatches(ruleObj)) {
|
|
||||||
Object value = ruleObj.get("value");
|
|
||||||
if (value instanceof String) {
|
|
||||||
args.add((String) value);
|
|
||||||
} else if (value instanceof JSONArray) {
|
|
||||||
JSONArray valArr = (JSONArray) value;
|
|
||||||
for (int j = 0; j < valArr.length(); j++) {
|
|
||||||
args.add(valArr.getString(j));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean ruleMatches(JSONObject ruleObj) {
|
|
||||||
JSONArray rules = ruleObj.getJSONArray("rules");
|
|
||||||
boolean result = false;
|
|
||||||
for (int i = 0; i < rules.length(); i++) {
|
|
||||||
JSONObject rule = rules.getJSONObject(i);
|
|
||||||
String action = rule.getString("action");
|
|
||||||
boolean matches = true;
|
|
||||||
|
|
||||||
if (rule.has("os")) {
|
|
||||||
JSONObject os = rule.getJSONObject("os");
|
|
||||||
String osName = System.getProperty("os.name").toLowerCase();
|
|
||||||
if (os.has("name")) {
|
|
||||||
String reqName = os.getString("name").toLowerCase();
|
|
||||||
if (reqName.equals("windows") && !osName.contains("win")) matches = false;
|
|
||||||
else if (reqName.equals("linux") && !osName.contains("linux") && !osName.contains("nix")) matches = false;
|
|
||||||
else if (reqName.equals("osx") && !osName.contains("mac")) matches = false;
|
|
||||||
}
|
|
||||||
if (os.has("arch")) {
|
|
||||||
String reqArch = os.getString("arch");
|
|
||||||
String osArch = System.getProperty("os.arch");
|
|
||||||
if (!reqArch.equals(osArch)) matches = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rule.has("features")) {
|
|
||||||
JSONObject features = rule.getJSONObject("features");
|
|
||||||
for (String key : features.keySet()) {
|
|
||||||
if (key.startsWith("is_demo_user") || key.startsWith("has_custom_resolution")) continue;
|
|
||||||
matches = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("allow".equals(action) && matches) {
|
|
||||||
result = true;
|
|
||||||
} else if ("disallow".equals(action) && matches) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Library> parseLibraries(JSONObject json) {
|
|
||||||
List<Library> libs = new ArrayList<>();
|
|
||||||
if (!json.has("libraries")) return libs;
|
|
||||||
|
|
||||||
JSONArray arr = json.getJSONArray("libraries");
|
|
||||||
for (int i = 0; i < arr.length(); i++) {
|
|
||||||
JSONObject libJson = arr.getJSONObject(i);
|
|
||||||
if (libJson.has("downloads") && libJson.getJSONObject("downloads").has("artifact")) {
|
|
||||||
String name = libJson.getString("name");
|
|
||||||
String artifactPath = libJson.getJSONObject("downloads").getJSONObject("artifact").getString("path");
|
|
||||||
Library lib = new Library(name, artifactPath);
|
|
||||||
|
|
||||||
if (libJson.has("natives")) {
|
|
||||||
JSONObject natives = libJson.getJSONObject("natives");
|
|
||||||
for (String key : natives.keySet()) {
|
|
||||||
String osKey = key.toLowerCase();
|
|
||||||
lib.natives.put(osKey, natives.getString(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (libJson.has("rules")) {
|
|
||||||
JSONObject dummyObj = new JSONObject();
|
|
||||||
dummyObj.put("rules", libJson.getJSONArray("rules"));
|
|
||||||
dummyObj.put("value", "");
|
|
||||||
if (ruleMatches(dummyObj)) {
|
|
||||||
libs.add(lib);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
libs.add(lib);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return libs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Library {
|
|
||||||
public final String name;
|
|
||||||
public final String artifactPath;
|
|
||||||
public final Map<String, String> natives = new HashMap<>();
|
|
||||||
|
|
||||||
public Library(String name, String artifactPath) {
|
|
||||||
this.name = name;
|
|
||||||
this.artifactPath = artifactPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSimpleName() {
|
|
||||||
return name.substring(name.indexOf(':') + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-41
@@ -1,41 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft.model;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class LaunchOptions {
|
|
||||||
private String username = "Player";
|
|
||||||
private String uuid = "00000000-0000-0000-0000-000000000000";
|
|
||||||
private String accessToken = "token";
|
|
||||||
private int maxMemory = 4096;
|
|
||||||
private boolean fullscreen = false;
|
|
||||||
private String javaPath = "java";
|
|
||||||
private List<String> extraJvmArgs = new ArrayList<>();
|
|
||||||
private int width = 854;
|
|
||||||
private int height = 480;
|
|
||||||
|
|
||||||
// Геттеры и сеттеры
|
|
||||||
public String getUsername() { return username; }
|
|
||||||
public void setUsername(String username) { this.username = username; }
|
|
||||||
|
|
||||||
public String getUuid() { return uuid; }
|
|
||||||
public void setUuid(String uuid) { this.uuid = uuid; }
|
|
||||||
|
|
||||||
public String getAccessToken() { return accessToken; }
|
|
||||||
public void setAccessToken(String accessToken) { this.accessToken = accessToken; }
|
|
||||||
|
|
||||||
public int getMaxMemory() { return maxMemory; }
|
|
||||||
public void setMaxMemory(int maxMemory) { this.maxMemory = maxMemory; }
|
|
||||||
|
|
||||||
public boolean isFullscreen() { return fullscreen; }
|
|
||||||
public void setFullscreen(boolean fullscreen) { this.fullscreen = fullscreen; }
|
|
||||||
|
|
||||||
public String getJavaPath() { return javaPath; }
|
|
||||||
public void setJavaPath(String javaPath) { this.javaPath = javaPath; }
|
|
||||||
|
|
||||||
public List<String> getExtraJvmArgs() { return extraJvmArgs; }
|
|
||||||
public void setExtraJvmArgs(List<String> extraJvmArgs) { this.extraJvmArgs = extraJvmArgs; }
|
|
||||||
|
|
||||||
public int getWidth() { return width; }
|
|
||||||
public int getHeight() { return height; }
|
|
||||||
}
|
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.minecraft.model;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
public class MinecraftVersion {
|
|
||||||
private final String id;
|
|
||||||
private final String type; // release, snapshot, old_beta, old_alpha
|
|
||||||
private final LocalDateTime releaseTime;
|
|
||||||
private final String url;
|
|
||||||
|
|
||||||
public MinecraftVersion(String id, String type, LocalDateTime releaseTime, String url) {
|
|
||||||
this.id = id;
|
|
||||||
this.type = type;
|
|
||||||
this.releaseTime = releaseTime;
|
|
||||||
this.url = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getId() { return id; }
|
|
||||||
public String getType() { return type; }
|
|
||||||
public LocalDateTime getReleaseTime() { return releaseTime; }
|
|
||||||
public String getUrl() { return url; }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return id + " (" + type + ")";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.ui;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
|
||||||
import org.jline.terminal.Terminal;
|
|
||||||
import org.jline.terminal.TerminalBuilder;
|
|
||||||
import org.jline.utils.InfoCmp;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class ArrowMenu {
|
|
||||||
|
|
||||||
private final String title;
|
|
||||||
private final List<String> options;
|
|
||||||
private int selected = 0;
|
|
||||||
private final Terminal terminal;
|
|
||||||
|
|
||||||
private static final int VISIBLE_ITEMS = 7; // сколько строк показывать в списке
|
|
||||||
|
|
||||||
public ArrowMenu(String title, List<String> options) throws IOException {
|
|
||||||
this.title = title;
|
|
||||||
this.options = options;
|
|
||||||
this.terminal = TerminalBuilder.builder()
|
|
||||||
.system(true)
|
|
||||||
.jna(true)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int show() throws IOException {
|
|
||||||
terminal.enterRawMode();
|
|
||||||
terminal.puts(InfoCmp.Capability.clear_screen);
|
|
||||||
terminal.puts(InfoCmp.Capability.cursor_invisible);
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
printPagedMenu();
|
|
||||||
int key = terminal.reader().read();
|
|
||||||
|
|
||||||
if (key == 'w' || key == 'W' || key == 'ц' || key == 'Ц'
|
|
||||||
|| key == 'k' || key == 'K' || key == 'л' || key == 'Л') { // Up / Arrow Up
|
|
||||||
selected = (selected - 1 + options.size()) % options.size();
|
|
||||||
}
|
|
||||||
else if (key == 's' || key == 'S' || key == 'ы' || key == 'Ы'
|
|
||||||
|| key == 'j' || key == 'J' || key == 'о' || key == 'О') { // Down / Arrow Down
|
|
||||||
selected = (selected + 1) % options.size();
|
|
||||||
}
|
|
||||||
else if (key == 13 || key == 10) { // Enter
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
else if (key == 27) { // Esc or arrow escape seq
|
|
||||||
int next = terminal.reader().read(50);
|
|
||||||
if (next == 91) { // '[' — start of arrow escape sequence
|
|
||||||
int arrow = terminal.reader().read(50);
|
|
||||||
if (arrow == 65) { // 'A' — Up arrow
|
|
||||||
selected = (selected - 1 + options.size()) % options.size();
|
|
||||||
} else if (arrow == 66) { // 'B' — Down arrow
|
|
||||||
selected = (selected + 1) % options.size();
|
|
||||||
}
|
|
||||||
// else — unknown escape seq, ignore
|
|
||||||
} else {
|
|
||||||
return -1; // genuine Esc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
terminal.puts(InfoCmp.Capability.cursor_visible);
|
|
||||||
terminal.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void printPagedMenu() {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
sb.append("\033[H\033[2J");
|
|
||||||
|
|
||||||
// Заголовок (фиксированный)
|
|
||||||
sb.append(ZAnsi.header("=== ZernMC Launcher ===")).append("\n\n");
|
|
||||||
sb.append(ZAnsi.yellow(title)).append("\n\n");
|
|
||||||
|
|
||||||
// Вычисляем диапазон отображаемых элементов
|
|
||||||
int start = Math.max(0, selected - (VISIBLE_ITEMS / 2));
|
|
||||||
int end = Math.min(options.size(), start + VISIBLE_ITEMS);
|
|
||||||
|
|
||||||
// Если в конце списка — подтягиваем вверх
|
|
||||||
if (end - start < VISIBLE_ITEMS && start > 0) {
|
|
||||||
start = Math.max(0, end - VISIBLE_ITEMS);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = start; i < end; i++) {
|
|
||||||
String line = options.get(i);
|
|
||||||
if (i == selected) {
|
|
||||||
sb.append(ZAnsi.selected(line)).append("\n");
|
|
||||||
} else {
|
|
||||||
sb.append(ZAnsi.white(" " + line)).append("\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Подсказка внизу (фиксированная)
|
|
||||||
sb.append("\n")
|
|
||||||
.append(ZAnsi.white("W/S (Ц/Ы) или ↑/↓ - перемещение | Enter - выбрать | Esc - назад"));
|
|
||||||
|
|
||||||
System.out.print(sb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.ui.jcef;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.sun.net.httpserver.HttpServer;
|
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
|
||||||
import com.sun.net.httpserver.Headers;
|
|
||||||
import me.sashegdev.zernmc.launcher.api.ApiResponse;
|
|
||||||
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.Instance;
|
|
||||||
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
|
|
||||||
public class LaunchServer {
|
|
||||||
private static final int PORT = 8080;
|
|
||||||
private final LauncherAPI api;
|
|
||||||
private final UIBridge bridge;
|
|
||||||
private HttpServer server;
|
|
||||||
private final Gson gson = new Gson();
|
|
||||||
|
|
||||||
public LaunchServer(UIBridge bridge) {
|
|
||||||
this.api = new LauncherAPI();
|
|
||||||
this.bridge = bridge;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() throws IOException {
|
|
||||||
server = HttpServer.create(new InetSocketAddress("localhost", PORT), 0);
|
|
||||||
|
|
||||||
server.createContext("/api/login", this::handleLogin);
|
|
||||||
server.createContext("/api/account", this::handleAccount);
|
|
||||||
server.createContext("/api/instances", this::handleInstances);
|
|
||||||
server.createContext("/api/launch", this::handleLaunch);
|
|
||||||
server.createContext("/api/install", this::handleInstall);
|
|
||||||
server.createContext("/api/logs", this::handleLogs);
|
|
||||||
server.createContext("/api/exit", this::handleExit);
|
|
||||||
server.createContext("/ui/", this::handleStatic);
|
|
||||||
|
|
||||||
server.setExecutor(Executors.newCachedThreadPool());
|
|
||||||
server.start();
|
|
||||||
|
|
||||||
bridge.log("HTTP сервер запущен на порту " + PORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (server != null) {
|
|
||||||
server.stop(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleLogin(HttpExchange exchange) throws IOException {
|
|
||||||
if (!"POST".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<String, String> body = parseJson(exchange.getRequestBody());
|
|
||||||
String username = body.get("username");
|
|
||||||
String password = body.get("password");
|
|
||||||
|
|
||||||
var result = api.login(username, password);
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
data.put("username", result.getData().getUsername());
|
|
||||||
data.put("token", result.getData().getToken());
|
|
||||||
sendJson(exchange, ApiResponse.success(data));
|
|
||||||
bridge.log("Пользователь вошел: " + username);
|
|
||||||
} else {
|
|
||||||
sendJson(exchange, ApiResponse.error(result.getError()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Ошибка: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleAccount(HttpExchange exchange) throws IOException {
|
|
||||||
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!api.isLoggedIn()) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Не авторизован"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
data.put("username", api.getCurrentUsername());
|
|
||||||
data.put("passActive", AuthManager.hasActivePass());
|
|
||||||
sendJson(exchange, ApiResponse.success(data));
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, ApiResponse.error(e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleInstances(HttpExchange exchange) throws IOException {
|
|
||||||
if (!"GET".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var result = api.getAllInstances();
|
|
||||||
sendJson(exchange, result);
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, ApiResponse.error(e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleLaunch(HttpExchange exchange) throws IOException {
|
|
||||||
if (!"POST".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!api.isLoggedIn()) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Не авторизован"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<String, String> body = parseJson(exchange.getRequestBody());
|
|
||||||
String name = body.get("name");
|
|
||||||
|
|
||||||
var result = api.launch(name);
|
|
||||||
if (result.isSuccess()) {
|
|
||||||
Map<String, Object> data = new HashMap<>();
|
|
||||||
data.put("pid", result.getData().getPid());
|
|
||||||
data.put("status", result.getData().getStatus());
|
|
||||||
sendJson(exchange, ApiResponse.success(data));
|
|
||||||
bridge.log("Запущена сборка: " + name);
|
|
||||||
} else {
|
|
||||||
sendJson(exchange, ApiResponse.error(result.getError()));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Ошибка запуска: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleInstall(HttpExchange exchange) throws IOException {
|
|
||||||
if (!"POST".equals(exchange.getRequestMethod())) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Метод не поддерживается"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!api.isLoggedIn()) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Не авторизован"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<String, String> body = parseJson(exchange.getRequestBody());
|
|
||||||
String name = body.get("name");
|
|
||||||
String version = body.get("version");
|
|
||||||
String loader = body.get("loader");
|
|
||||||
|
|
||||||
bridge.log("Установка сборки: " + name + " " + version + " " + loader);
|
|
||||||
|
|
||||||
var createResult = api.instances().createInstance(name);
|
|
||||||
if (!createResult.isSuccess()) {
|
|
||||||
sendJson(exchange, ApiResponse.error(createResult.getError()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Instance instance = InstanceManager.getInstance(name);
|
|
||||||
if (instance != null) {
|
|
||||||
instance.setMinecraftVersion(version);
|
|
||||||
instance.setLoaderType(loader);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendJson(exchange, ApiResponse.success(true));
|
|
||||||
bridge.log("Сборка установлена: " + name);
|
|
||||||
} catch (Exception e) {
|
|
||||||
sendJson(exchange, ApiResponse.error("Ошибка установки: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleLogs(HttpExchange exchange) throws IOException {
|
|
||||||
String logs = bridge.getLogs();
|
|
||||||
sendJson(exchange, ApiResponse.success(logs));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleExit(HttpExchange exchange) throws IOException {
|
|
||||||
bridge.log("Завершение работы...");
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleStatic(HttpExchange exchange) throws IOException {
|
|
||||||
String path = exchange.getRequestURI().getPath();
|
|
||||||
if (path.equals("/ui/") || path.equals("/ui")) {
|
|
||||||
path = "/ui/index.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
var resource = getClass().getResource(path);
|
|
||||||
|
|
||||||
if (resource == null) {
|
|
||||||
exchange.sendResponseHeaders(404, 0);
|
|
||||||
exchange.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
byte[] content = resource.openStream().readAllBytes();
|
|
||||||
String contentType = getContentType(path);
|
|
||||||
|
|
||||||
exchange.getResponseHeaders().set("Content-Type", contentType);
|
|
||||||
exchange.sendResponseHeaders(200, content.length);
|
|
||||||
|
|
||||||
OutputStream os = exchange.getResponseBody();
|
|
||||||
os.write(content);
|
|
||||||
os.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getContentType(String path) {
|
|
||||||
if (path.endsWith(".html")) return "text/html; charset=utf-8";
|
|
||||||
if (path.endsWith(".css")) return "text/css; charset=utf-8";
|
|
||||||
if (path.endsWith(".js")) return "application/javascript; charset=utf-8";
|
|
||||||
return "text/plain";
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Map<String, String> parseJson(InputStream body) {
|
|
||||||
try {
|
|
||||||
String json = new String(body.readAllBytes(), StandardCharsets.UTF_8);
|
|
||||||
return gson.fromJson(json, Map.class);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return new HashMap<>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendJson(HttpExchange exchange, ApiResponse response) {
|
|
||||||
try {
|
|
||||||
String json = gson.toJson(response);
|
|
||||||
byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
|
||||||
exchange.sendResponseHeaders(200, bytes.length);
|
|
||||||
|
|
||||||
OutputStream os = exchange.getResponseBody();
|
|
||||||
os.write(bytes);
|
|
||||||
os.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.ui.jcef;
|
|
||||||
|
|
||||||
public class UIBridge {
|
|
||||||
public void log(String message) {
|
|
||||||
System.out.println("[UI] " + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLogs() {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.ui.jcef;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.net.URI;
|
|
||||||
|
|
||||||
public class UILauncher {
|
|
||||||
private static final String APP_TITLE = "ZernMC Launcher";
|
|
||||||
private final LauncherAPI api;
|
|
||||||
private final UIBridge bridge;
|
|
||||||
private LaunchServer server;
|
|
||||||
|
|
||||||
public UILauncher() {
|
|
||||||
this.api = new LauncherAPI();
|
|
||||||
this.bridge = new UIBridge();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void launch() throws Exception {
|
|
||||||
redirectSystemLogs();
|
|
||||||
bridge.log("Запуск UI...");
|
|
||||||
|
|
||||||
server = new LaunchServer(bridge);
|
|
||||||
server.start();
|
|
||||||
|
|
||||||
openBrowser();
|
|
||||||
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
|
||||||
bridge.log("Выключение...");
|
|
||||||
if (server != null) server.stop();
|
|
||||||
}));
|
|
||||||
|
|
||||||
Thread.currentThread().join();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openBrowser() {
|
|
||||||
String url = "http://localhost:8080/ui/";
|
|
||||||
bridge.log("Открытие браузера: " + url);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
|
||||||
Desktop.getDesktop().browse(URI.create(url));
|
|
||||||
bridge.log("Браузер открыт");
|
|
||||||
} else {
|
|
||||||
bridge.log("Desktop browsing not supported");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
bridge.log("Ошибка открытия браузера: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void redirectSystemLogs() {
|
|
||||||
PrintStream originalOut = System.out;
|
|
||||||
PrintStream originalErr = System.err;
|
|
||||||
|
|
||||||
System.setOut(new PrintStream(new ByteArrayOutputStream() {
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) {
|
|
||||||
String line = new String(b, off, len).trim();
|
|
||||||
if (!line.isEmpty()) {
|
|
||||||
bridge.log(line);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
originalOut.write(b, off, len);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(int b) {
|
|
||||||
try {
|
|
||||||
originalOut.write(b);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
System.setErr(new PrintStream(new ByteArrayOutputStream() {
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) {
|
|
||||||
String line = new String(b, off, len).trim();
|
|
||||||
if (!line.isEmpty()) {
|
|
||||||
bridge.log("[ERROR] " + line);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
originalErr.write(b, off, len);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
try {
|
|
||||||
UILauncher launcher = new UILauncher();
|
|
||||||
launcher.launch();
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("UI launch failed: " + e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.utils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
public class Config {
|
|
||||||
|
|
||||||
private static final Path CONFIG_DIR = Path.of(System.getProperty("user.home"), ".zernmc");
|
|
||||||
private static final Path CONFIG_FILE = CONFIG_DIR.resolve("launcher.properties");
|
|
||||||
|
|
||||||
private static final String BUILD_PROFILE = System.getProperty("build.profile", "global");
|
|
||||||
|
|
||||||
private static final Properties props = new Properties();
|
|
||||||
|
|
||||||
// Настройки
|
|
||||||
private static int maxMemory = 4096; // будет перезаписано умной логикой
|
|
||||||
private static String serverUrl = "http://87.120.187.36:1582";
|
|
||||||
private static String lastUsername = "Player";
|
|
||||||
|
|
||||||
static {
|
|
||||||
load();
|
|
||||||
applySmartRamRecommendation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void load() {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(CONFIG_DIR);
|
|
||||||
if (Files.exists(CONFIG_FILE)) {
|
|
||||||
try (var is = Files.newInputStream(CONFIG_FILE)) {
|
|
||||||
props.load(is);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
maxMemory = Integer.parseInt(props.getProperty("maxMemory", "4096"));
|
|
||||||
serverUrl = props.getProperty("serverUrl", serverUrl);
|
|
||||||
lastUsername = props.getProperty("lastUsername", lastUsername);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println(ZAnsi.brightRed("Не удалось загрузить конфиг: ") + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void save() {
|
|
||||||
try {
|
|
||||||
props.setProperty("maxMemory", String.valueOf(maxMemory));
|
|
||||||
props.setProperty("serverUrl", serverUrl);
|
|
||||||
props.setProperty("lastUsername", lastUsername);
|
|
||||||
|
|
||||||
try (var os = Files.newOutputStream(CONFIG_FILE)) {
|
|
||||||
props.store(os, "ZernMC Launcher Configuration");
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
System.err.println(ZAnsi.brightRed("Не удалось сохранить конфиг: ") + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Умная рекомендация RAM:
|
|
||||||
* - минимум 1.5 GB
|
|
||||||
* - рекомендуется totalRAM - 30%
|
|
||||||
* - максимум 70% от доступной RAM
|
|
||||||
*/
|
|
||||||
private static void applySmartRamRecommendation() {
|
|
||||||
long totalRamMB = Runtime.getRuntime().maxMemory() / (1024 * 1024); // в MB
|
|
||||||
|
|
||||||
// Рекомендуемое значение = total - 30%
|
|
||||||
long recommended = (long) (totalRamMB * 0.70); // 70% от доступной
|
|
||||||
|
|
||||||
// Ограничения
|
|
||||||
recommended = Math.max(1536, recommended); // минимум 1.5 GB
|
|
||||||
recommended = Math.min(recommended, totalRamMB - 1024); // оставляем минимум 1 GB системе
|
|
||||||
|
|
||||||
// Если текущее значение сильно отличается от рекомендуемого — корректируем
|
|
||||||
if (Math.abs(maxMemory - recommended) > 1024) { // разница больше 1 GB
|
|
||||||
maxMemory = (int) recommended;
|
|
||||||
save(); // сохраняем умную рекомендацию
|
|
||||||
System.out.println(ZAnsi.cyan("Автоматически рекомендовано RAM: " + maxMemory + " MB"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters & Setters
|
|
||||||
public static int getMaxMemory() {
|
|
||||||
return maxMemory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isZernMCBuild() {
|
|
||||||
return "zernmc".equalsIgnoreCase(BUILD_PROFILE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isGlobalBuild() {
|
|
||||||
return !isZernMCBuild();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setMaxMemory(int memory) {
|
|
||||||
// Защита от слишком маленьких/больших значений
|
|
||||||
if (memory < 1024) memory = 1536;
|
|
||||||
if (memory > 32768) memory = 32768;
|
|
||||||
|
|
||||||
maxMemory = memory;
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getServerUrl() {
|
|
||||||
return serverUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getLastUsername() {
|
|
||||||
return lastUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setLastUsername(String username) {
|
|
||||||
lastUsername = username;
|
|
||||||
save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Path getInstancesDir() {
|
|
||||||
return CONFIG_DIR.resolve("instances");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Path getJreDir() {
|
|
||||||
return CONFIG_DIR.resolve("jre");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Path getConfigDir() {
|
|
||||||
return CONFIG_DIR;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Полезная информация для пользователя
|
|
||||||
*/
|
|
||||||
public static String getRamInfo() {
|
|
||||||
long totalMB = Runtime.getRuntime().maxMemory() / (1024 * 1024);
|
|
||||||
return "Доступно RAM: " + totalMB + " MB | Рекомендуется: " + maxMemory + " MB";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.utils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class ConsoleUtils {
|
|
||||||
|
|
||||||
public static void clearScreen() {
|
|
||||||
System.out.print("\033[H\033[2J");
|
|
||||||
System.out.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void pause() {
|
|
||||||
System.out.print(ZAnsi.white("\nНажмите Enter для продолжения..."));
|
|
||||||
try {
|
|
||||||
System.in.read();
|
|
||||||
// Очищаем буфер ввода
|
|
||||||
while (System.in.available() > 0) {
|
|
||||||
System.in.read();
|
|
||||||
}
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void printHeader(String subtitle) {
|
|
||||||
clearScreen();
|
|
||||||
System.out.println(ZAnsi.header("=== ZernMC Launcher ==="));
|
|
||||||
if (subtitle != null && !subtitle.isEmpty()) {
|
|
||||||
System.out.println(ZAnsi.yellow(subtitle));
|
|
||||||
}
|
|
||||||
System.out.println();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void printHeader() {
|
|
||||||
printHeader(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void separator() {
|
|
||||||
System.out.println(ZAnsi.white("────────────────────────────────────────────────────────────"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.utils;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.ui.ArrowMenu;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Scanner;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Улучшенный Input с поддержкой кириллицы и confirm через ArrowMenu
|
|
||||||
*/
|
|
||||||
public class Input {
|
|
||||||
|
|
||||||
// Используем UTF-8 явно — это помогает на Windows
|
|
||||||
private static final Scanner scanner = new Scanner(System.in, "UTF-8");
|
|
||||||
|
|
||||||
public static String readLine() {
|
|
||||||
return scanner.nextLine().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String readLine(String prompt) {
|
|
||||||
flushInput(); // Очищаем буфер
|
|
||||||
System.out.print(prompt);
|
|
||||||
return scanner.nextLine().trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int readInt(String prompt) {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
System.out.print(prompt);
|
|
||||||
return Integer.parseInt(scanner.nextLine().trim());
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Некорректное число. Попробуйте ещё раз."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int readInt(String prompt, int min, int max) {
|
|
||||||
while (true) {
|
|
||||||
int value = readInt(prompt);
|
|
||||||
if (value >= min && value <= max) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
System.out.println(ZAnsi.brightRed("Значение должно быть от " + min + " до " + max + "."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Новый confirm через ArrowMenu
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public static boolean confirm(String question) throws IOException {
|
|
||||||
ConsoleUtils.clearScreen(); // опционально, можно убрать
|
|
||||||
|
|
||||||
List<String> options = List.of(
|
|
||||||
"Да",
|
|
||||||
"Нет"
|
|
||||||
);
|
|
||||||
|
|
||||||
ArrowMenu menu = new ArrowMenu(question, options);
|
|
||||||
int choice = menu.show();
|
|
||||||
|
|
||||||
return choice == 0; // 0 = "Да"
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Альтернативный confirm без очистки экрана
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public static boolean confirmInline(String question) throws IOException {
|
|
||||||
List<String> options = List.of("Да", "Нет");
|
|
||||||
ArrowMenu menu = new ArrowMenu(question, options);
|
|
||||||
int choice = menu.show();
|
|
||||||
return choice == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Закрытие сканнера (вызывать при выходе из программы, если нужно)
|
|
||||||
*/
|
|
||||||
public static void close() {
|
|
||||||
scanner.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Очищает буфер ввода от оставшихся символов
|
|
||||||
*/
|
|
||||||
public static void flushInput() {
|
|
||||||
try {
|
|
||||||
while (System.in.available() > 0) {
|
|
||||||
System.in.read();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Игнорируем
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.utils;
|
|
||||||
|
|
||||||
import java.text.DecimalFormat;
|
|
||||||
|
|
||||||
public class ProgressBar {
|
|
||||||
|
|
||||||
private static final int BAR_LENGTH = 40;
|
|
||||||
private static final DecimalFormat DF = new DecimalFormat("#.##");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Прогресс по количеству файлов (для библиотек и общего прогресса)
|
|
||||||
*/
|
|
||||||
public static void show(String label, long current, long total, String unit) {
|
|
||||||
if (total <= 0) {
|
|
||||||
System.out.print("\r" + ZAnsi.cyan(label) + " ...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
double progress = (double) current / total;
|
|
||||||
int filled = (int) (progress * BAR_LENGTH);
|
|
||||||
String bar = "█".repeat(filled) + "░".repeat(BAR_LENGTH - filled);
|
|
||||||
int percent = (int) (progress * 100);
|
|
||||||
|
|
||||||
String text = String.format("%s [%s] %3d%% (%d/%d %s)",
|
|
||||||
ZAnsi.cyan(label), bar, percent, current, total, unit);
|
|
||||||
|
|
||||||
System.out.print("\r" + text);
|
|
||||||
System.out.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Прогресс по байтам для одного файла (реальный прогресс)
|
|
||||||
*/
|
|
||||||
public static void showDownload(String label, long downloaded, long totalBytes) {
|
|
||||||
if (totalBytes <= 0) {
|
|
||||||
System.out.print("\r" + ZAnsi.cyan(label) + " ...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
double progress = (double) downloaded / totalBytes;
|
|
||||||
int filled = (int) (progress * BAR_LENGTH);
|
|
||||||
String bar = "█".repeat(filled) + "░".repeat(BAR_LENGTH - filled);
|
|
||||||
String percent = DF.format(progress * 100);
|
|
||||||
|
|
||||||
String text = String.format("%s [%s] %6s%% %s / %s",
|
|
||||||
ZAnsi.cyan(label),
|
|
||||||
bar,
|
|
||||||
percent,
|
|
||||||
formatBytes(downloaded),
|
|
||||||
formatBytes(totalBytes));
|
|
||||||
|
|
||||||
System.out.print("\r" + text);
|
|
||||||
System.out.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void showAnimated(String label, long current, long total, String unit) {
|
|
||||||
if (total <= 0) {
|
|
||||||
// Анимация для неизвестного размера
|
|
||||||
char[] spinner = {'|', '/', '-', '\\'};
|
|
||||||
int idx = (int) (current / 1024) % 4;
|
|
||||||
System.out.print("\r" + label + " [" + spinner[idx] + "] " + formatBytes(current));
|
|
||||||
} else {
|
|
||||||
show(label, (int) ((current * 100) / total), 100, unit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void finish(String message) {
|
|
||||||
System.out.println("\r" + ZAnsi.brightGreen(message + " завершено ✓"));
|
|
||||||
System.out.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void clearLine() {
|
|
||||||
System.out.print("\r" + " ".repeat(110) + "\r");
|
|
||||||
System.out.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String formatBytes(long bytes) {
|
|
||||||
if (bytes < 1024) return bytes + " B";
|
|
||||||
if (bytes < 1024 * 1024) return DF.format(bytes / 1024.0) + " KB";
|
|
||||||
return DF.format(bytes / (1024.0 * 1024)) + " MB";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.utils;
|
|
||||||
|
|
||||||
import org.fusesource.jansi.Ansi;
|
|
||||||
import org.fusesource.jansi.AnsiConsole;
|
|
||||||
|
|
||||||
public class ZAnsi {
|
|
||||||
|
|
||||||
//поддержка ANSI епта
|
|
||||||
public static void install() {
|
|
||||||
AnsiConsole.systemInstall();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void uninstall() {
|
|
||||||
AnsiConsole.systemUninstall();
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Основные цвета ===
|
|
||||||
public static String green(String text) {
|
|
||||||
return Ansi.ansi().fg(Ansi.Color.GREEN).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String brightGreen(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.GREEN).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String cyan(String text) {
|
|
||||||
return Ansi.ansi().fg(Ansi.Color.CYAN).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String brightCyan(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.CYAN).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String yellow(String text) {
|
|
||||||
return Ansi.ansi().fg(Ansi.Color.YELLOW).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String brightYellow(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.YELLOW).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String red(String text) {
|
|
||||||
return Ansi.ansi().fg(Ansi.Color.RED).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String brightRed(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.RED).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String blue(String text) {
|
|
||||||
return Ansi.ansi().fg(Ansi.Color.BLUE).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String brightBlue(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.BLUE).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String magenta(String text) {
|
|
||||||
return Ansi.ansi().fg(Ansi.Color.MAGENTA).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String brightMagenta(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.MAGENTA).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Пурпурный как brightPurple (используем magenta)
|
|
||||||
public static String purple(String text) {
|
|
||||||
return brightMagenta(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String brightPurple(String text) {
|
|
||||||
return brightMagenta(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String white(String text) {
|
|
||||||
return Ansi.ansi().fg(Ansi.Color.WHITE).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String brightWhite(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.WHITE).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String black(String text) {
|
|
||||||
return Ansi.ansi().fg(Ansi.Color.BLACK).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Фоновые цвета ===
|
|
||||||
public static String bgGreen(String text) {
|
|
||||||
return Ansi.ansi().bg(Ansi.Color.GREEN).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String bgRed(String text) {
|
|
||||||
return Ansi.ansi().bg(Ansi.Color.RED).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String bgYellow(String text) {
|
|
||||||
return Ansi.ansi().bg(Ansi.Color.YELLOW).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String bgBlue(String text) {
|
|
||||||
return Ansi.ansi().bg(Ansi.Color.BLUE).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Стили ===
|
|
||||||
public static String bold(String text) {
|
|
||||||
return Ansi.ansi().bold().a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String reset() {
|
|
||||||
return Ansi.ansi().reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Комбинированные удобные методы ===
|
|
||||||
public static String header(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.CYAN).bold().a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String success(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.GREEN).bold().a("[✓] " + text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String error(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.RED).bold().a("[✗] " + text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String warning(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.YELLOW).bold().a("[!] " + text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String info(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.CYAN).bold().a("[i] " + text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String selected(String text) {
|
|
||||||
return Ansi.ansi()
|
|
||||||
.bgBright(Ansi.Color.WHITE)
|
|
||||||
.fg(Ansi.Color.BLACK)
|
|
||||||
.bold()
|
|
||||||
.a(" > " + text + " ")
|
|
||||||
.reset()
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String dim(String text) {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.BLACK).a(text).reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Цветной текст для ролей ===
|
|
||||||
public static String roleUser(String text) {
|
|
||||||
return white(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String rolePassHolder(String text) {
|
|
||||||
return brightGreen(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String roleModerator(String text) {
|
|
||||||
return brightBlue(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String roleElder(String text) {
|
|
||||||
return brightPurple(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String roleCreator(String text) {
|
|
||||||
return brightRed(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Очистка экрана ===
|
|
||||||
public static String clearScreen() {
|
|
||||||
return Ansi.ansi().eraseScreen().cursor(1, 1).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Прогресс бар символы ===
|
|
||||||
public static String progressChar() {
|
|
||||||
return Ansi.ansi().fgBright(Ansi.Color.CYAN).a("█").reset().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String progressEmpty() {
|
|
||||||
return Ansi.ansi().fg(Ansi.Color.BLACK).a("░").reset().toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,550 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.utils;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
public class ZHttpClient {
|
|
||||||
|
|
||||||
private static final HttpClient client = HttpClient.newBuilder()
|
|
||||||
.connectTimeout(Duration.ofSeconds(15))
|
|
||||||
.version(HttpClient.Version.HTTP_1_1)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
private static String BASE_URL = "http://87.120.187.36:1582";
|
|
||||||
|
|
||||||
// Глобальный прокси режим (для обратной совместимости)
|
|
||||||
private static final AtomicBoolean useProxyMode = new AtomicBoolean(false);
|
|
||||||
private static final AtomicBoolean proxyTested = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Переопределить URL сервера (для тестов).
|
|
||||||
* Внимание: не потокобезопасно, использовать только в тестах.
|
|
||||||
*/
|
|
||||||
public static void setBaseUrl(String url) {
|
|
||||||
BASE_URL = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getBaseUrl() {
|
|
||||||
return BASE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Умное проксирование по сервисам
|
|
||||||
public enum ServiceType {
|
|
||||||
ZERN_SERVER("http://87.120.187.36:1582", true),
|
|
||||||
FABRIC_META("https://meta.fabricmc.net", false),
|
|
||||||
FABRIC_MAVEN("https://maven.fabricmc.net", false),
|
|
||||||
MOJANG_META("https://piston-meta.mojang.com", false),
|
|
||||||
MOJANG_RESOURCES("https://resources.download.minecraft.net", false),
|
|
||||||
FORGE_MAVEN("https://maven.minecraftforge.net", false),
|
|
||||||
NEOFORGE_MAVEN("https://maven.neoforged.net", false),
|
|
||||||
GOOGLE("https://google.com", false),
|
|
||||||
CLOUDFLARE("https://cloudflare.com", false);
|
|
||||||
|
|
||||||
private final String baseUrl;
|
|
||||||
private final boolean alwaysDirect;
|
|
||||||
|
|
||||||
ServiceType(String baseUrl, boolean alwaysDirect) {
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.alwaysDirect = alwaysDirect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getBaseUrl() { return baseUrl; }
|
|
||||||
public boolean isAlwaysDirect() { return alwaysDirect; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Статусы сервисов
|
|
||||||
private static final Map<ServiceType, Boolean> serviceProxyMode = new ConcurrentHashMap<>();
|
|
||||||
private static final Map<ServiceType, Integer> serviceFailCount = new ConcurrentHashMap<>();
|
|
||||||
private static final Map<ServiceType, Long> serviceLastCheckTime = new ConcurrentHashMap<>();
|
|
||||||
private static final Map<ServiceType, Boolean> serviceHealthy = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private static final int MAX_FAILS_BEFORE_PROXY = 2;
|
|
||||||
private static final long HEALTH_CHECK_INTERVAL_MS = 60000; // 1 минута
|
|
||||||
private static final long CHECK_TIMEOUT_MS = 7000; // 7 секунд на проверку
|
|
||||||
|
|
||||||
// Статистика
|
|
||||||
private static int directSuccessCount = 0;
|
|
||||||
private static int proxySuccessCount = 0;
|
|
||||||
private static int directFailCount = 0;
|
|
||||||
|
|
||||||
static {
|
|
||||||
for (ServiceType type : ServiceType.values()) {
|
|
||||||
serviceProxyMode.put(type, false);
|
|
||||||
serviceFailCount.put(type, 0);
|
|
||||||
serviceHealthy.put(type, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Вызывать один раз при запуске лаунчера
|
|
||||||
*/
|
|
||||||
public static void checkAllServicesOnStartup() {
|
|
||||||
if (proxyTested.get()) return;
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Проверка доступности сервисов..."));
|
|
||||||
|
|
||||||
List<ServiceType> servicesToCheck = List.of(
|
|
||||||
ServiceType.ZERN_SERVER,
|
|
||||||
ServiceType.GOOGLE,
|
|
||||||
ServiceType.FABRIC_META,
|
|
||||||
ServiceType.FABRIC_MAVEN,
|
|
||||||
ServiceType.MOJANG_META,
|
|
||||||
ServiceType.MOJANG_RESOURCES,
|
|
||||||
ServiceType.FORGE_MAVEN,
|
|
||||||
ServiceType.NEOFORGE_MAVEN
|
|
||||||
);
|
|
||||||
|
|
||||||
for (ServiceType service : servicesToCheck) {
|
|
||||||
boolean isHealthy = checkServiceHealth(service);
|
|
||||||
serviceHealthy.put(service, isHealthy);
|
|
||||||
|
|
||||||
if (service.isAlwaysDirect()) {
|
|
||||||
System.out.println(isHealthy ?
|
|
||||||
ZAnsi.green(" " + service.name() + " - OK") :
|
|
||||||
ZAnsi.red(" " + service.name() + " - НЕ ДОСТУПЕН (критично!)"));
|
|
||||||
} else {
|
|
||||||
if (isHealthy) {
|
|
||||||
System.out.println(ZAnsi.green(" " + service.name() + " - прямое подключение работает"));
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.yellow(" " + service.name() + " - НЕ ДОСТУПЕН, будет использован прокси"));
|
|
||||||
serviceProxyMode.put(service, true);
|
|
||||||
serviceFailCount.put(service, MAX_FAILS_BEFORE_PROXY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!serviceHealthy.get(ServiceType.ZERN_SERVER)) {
|
|
||||||
System.out.println(ZAnsi.brightRed("Критическая ошибка: Zern сервер недоступен!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyTested.set(true);
|
|
||||||
startHealthCheckThread();
|
|
||||||
printStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Принудительная проверка Mojang-сервисов (рекомендуется вызывать перед установкой сборки)
|
|
||||||
*/
|
|
||||||
public static void forceCheckMojangServices() {
|
|
||||||
System.out.println(ZAnsi.cyan("Принудительная проверка Mojang сервисов..."));
|
|
||||||
|
|
||||||
for (ServiceType service : List.of(ServiceType.MOJANG_META, ServiceType.MOJANG_RESOURCES)) {
|
|
||||||
boolean healthy = checkServiceHealth(service);
|
|
||||||
serviceHealthy.put(service, healthy);
|
|
||||||
|
|
||||||
if (healthy) {
|
|
||||||
System.out.println(ZAnsi.green(" " + service.name() + " доступен напрямую"));
|
|
||||||
serviceProxyMode.put(service, false);
|
|
||||||
serviceFailCount.put(service, 0);
|
|
||||||
} else {
|
|
||||||
System.out.println(ZAnsi.yellow(" " + service.name() + " недоступен → прокси режим активирован"));
|
|
||||||
serviceProxyMode.put(service, true);
|
|
||||||
serviceFailCount.put(service, MAX_FAILS_BEFORE_PROXY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean checkServiceHealth(ServiceType service) {
|
|
||||||
return checkDirectConnection(service.getBaseUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Улучшенная проверка прямого подключения
|
|
||||||
*/
|
|
||||||
private static boolean checkDirectConnection(String baseUrl) {
|
|
||||||
String testUrl = baseUrl;
|
|
||||||
|
|
||||||
if (baseUrl.contains("piston-meta.mojang.com") || baseUrl.contains("launchermeta.mojang.com")) {
|
|
||||||
testUrl = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
|
|
||||||
} else if (baseUrl.contains("resources.download.minecraft.net")) {
|
|
||||||
testUrl = "https://resources.download.minecraft.net/00/0000000000000000000000000000000000000000";
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(testUrl))
|
|
||||||
.timeout(Duration.ofMillis(CHECK_TIMEOUT_MS))
|
|
||||||
.GET()
|
|
||||||
.header("User-Agent", "ZernMC-Launcher/HealthCheck")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
int code = response.statusCode();
|
|
||||||
return code == 200 || code == 404; // 404 для ресурсов — нормально
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void startHealthCheckThread() {
|
|
||||||
Thread healthThread = new Thread(() -> {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(HEALTH_CHECK_INTERVAL_MS);
|
|
||||||
performHealthCheck();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
healthThread.setDaemon(true);
|
|
||||||
healthThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void performHealthCheck() {
|
|
||||||
for (ServiceType service : ServiceType.values()) {
|
|
||||||
if (service.isAlwaysDirect()) continue;
|
|
||||||
|
|
||||||
boolean isHealthy = checkServiceHealth(service);
|
|
||||||
serviceHealthy.put(service, isHealthy);
|
|
||||||
|
|
||||||
if (isHealthy && serviceProxyMode.get(service)) {
|
|
||||||
serviceProxyMode.put(service, false);
|
|
||||||
serviceFailCount.put(service, 0);
|
|
||||||
System.out.println(ZAnsi.green("[NET] " + service.name() + " восстановлен, переключен на прямое подключение"));
|
|
||||||
} else if (!isHealthy && !serviceProxyMode.get(service)) {
|
|
||||||
int fails = serviceFailCount.getOrDefault(service, 0) + 1;
|
|
||||||
serviceFailCount.put(service, fails);
|
|
||||||
serviceLastCheckTime.put(service, System.currentTimeMillis());
|
|
||||||
|
|
||||||
if (fails >= MAX_FAILS_BEFORE_PROXY) {
|
|
||||||
serviceProxyMode.put(service, true);
|
|
||||||
System.out.println(ZAnsi.yellow("[NET] " + service.name() + " недоступен, включен прокси режим"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ServiceType detectService(String url) {
|
|
||||||
if (url.contains("meta.fabricmc.net")) return ServiceType.FABRIC_META;
|
|
||||||
if (url.contains("maven.fabricmc.net")) return ServiceType.FABRIC_MAVEN;
|
|
||||||
if (url.contains("piston-meta.mojang.com") || url.contains("launchermeta.mojang.com"))
|
|
||||||
return ServiceType.MOJANG_META;
|
|
||||||
if (url.contains("resources.download.minecraft.net")) return ServiceType.MOJANG_RESOURCES;
|
|
||||||
if (url.contains("maven.minecraftforge.net")) return ServiceType.FORGE_MAVEN;
|
|
||||||
if (url.contains("maven.neoforged.net")) return ServiceType.NEOFORGE_MAVEN;
|
|
||||||
if (url.contains("google.com")) return ServiceType.GOOGLE;
|
|
||||||
if (url.contains("cloudflare.com")) return ServiceType.CLOUDFLARE;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean shouldUseProxyForUrl(String url) {
|
|
||||||
if (useProxyMode.get()) return true;
|
|
||||||
|
|
||||||
ServiceType service = detectService(url);
|
|
||||||
if (service == null || service.isAlwaysDirect()) return false;
|
|
||||||
|
|
||||||
return serviceProxyMode.getOrDefault(service, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isConnectionError(Throwable e) {
|
|
||||||
Throwable cause = e.getCause() != null ? e.getCause() : e;
|
|
||||||
String msg = cause.getMessage() != null ? cause.getMessage().toLowerCase() : "";
|
|
||||||
|
|
||||||
return cause instanceof java.net.ConnectException ||
|
|
||||||
cause instanceof java.net.UnknownHostException ||
|
|
||||||
cause instanceof java.nio.channels.ClosedChannelException ||
|
|
||||||
msg.contains("connection") ||
|
|
||||||
msg.contains("timeout") ||
|
|
||||||
msg.contains("refused") ||
|
|
||||||
msg.contains("closed");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void markServiceAsBlocked(String url) {
|
|
||||||
ServiceType service = detectService(url);
|
|
||||||
if (service == null || service.isAlwaysDirect()) return;
|
|
||||||
|
|
||||||
int fails = serviceFailCount.getOrDefault(service, 0) + 1;
|
|
||||||
serviceFailCount.put(service, fails);
|
|
||||||
serviceLastCheckTime.put(service, System.currentTimeMillis());
|
|
||||||
|
|
||||||
if (fails >= MAX_FAILS_BEFORE_PROXY && !serviceProxyMode.get(service)) {
|
|
||||||
serviceProxyMode.put(service, true);
|
|
||||||
System.out.println(ZAnsi.yellow("[NET] " + service.name() + " заблокирован, переключаемся на прокси"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Универсальный GET с умным прокси + автоматическим fallback
|
|
||||||
*/
|
|
||||||
public static String getWithSmartProxy(String url) throws IOException, InterruptedException {
|
|
||||||
// Попытка прямого подключения
|
|
||||||
if (!shouldUseProxyForUrl(url)) {
|
|
||||||
try {
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(url))
|
|
||||||
.timeout(Duration.ofSeconds(25))
|
|
||||||
.header("User-Agent", "ZernMC-Launcher/1.0")
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
directSuccessCount++;
|
|
||||||
return response.body();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode() >= 400) {
|
|
||||||
throw new IOException("HTTP " + response.statusCode());
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (isConnectionError(e)) {
|
|
||||||
directFailCount++;
|
|
||||||
markServiceAsBlocked(url);
|
|
||||||
}
|
|
||||||
// Если ошибка соединения — пробуем через прокси
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Через прокси
|
|
||||||
try {
|
|
||||||
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
|
|
||||||
String proxyUrl = BASE_URL + "/download?url=" + encodedUrl;
|
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(proxyUrl))
|
|
||||||
.timeout(Duration.ofSeconds(40))
|
|
||||||
.header("User-Agent", "ZernMC-Launcher/1.0")
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
throw new IOException("Proxy HTTP " + response.statusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
proxySuccessCount++;
|
|
||||||
return response.body();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new IOException("Не удалось получить данные ни напрямую, ни через прокси: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Скачивание файла с умным прокси + fallback
|
|
||||||
*/
|
|
||||||
public static void downloadFileWithSmartProxy(String url, Path target) throws Exception {
|
|
||||||
if (!shouldUseProxyForUrl(url)) {
|
|
||||||
try {
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(url))
|
|
||||||
.timeout(Duration.ofSeconds(40))
|
|
||||||
.header("User-Agent", "ZernMC-Launcher/1.0")
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
|
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
directSuccessCount++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (isConnectionError(e)) {
|
|
||||||
directFailCount++;
|
|
||||||
markServiceAsBlocked(url);
|
|
||||||
}
|
|
||||||
// fallback на прокси ниже
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Скачивание через прокси
|
|
||||||
String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
|
|
||||||
String proxyUrl = BASE_URL + "/proxy/download?url=" + encodedUrl;
|
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(proxyUrl))
|
|
||||||
.timeout(Duration.ofMinutes(5))
|
|
||||||
.header("User-Agent", "ZernMC-Launcher/1.0")
|
|
||||||
.GET()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<Path> response = client.send(request, HttpResponse.BodyHandlers.ofFile(target));
|
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
throw new IOException("Proxy download failed: HTTP " + response.statusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
proxySuccessCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== СТАРЫЕ МЕТОДЫ (обновлённые) ======================
|
|
||||||
|
|
||||||
public static String get(String endpoint) throws IOException, InterruptedException {
|
|
||||||
checkAllServicesOnStartup();
|
|
||||||
|
|
||||||
if (useProxyMode.get()) {
|
|
||||||
return proxyGet(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(BASE_URL + endpoint))
|
|
||||||
.timeout(Duration.ofSeconds(15))
|
|
||||||
.header("User-Agent", "ZernMC-Launcher/1.0")
|
|
||||||
.GET();
|
|
||||||
|
|
||||||
// ===== ДОБАВИТЬ ТОКЕН АВТОРИЗАЦИИ =====
|
|
||||||
String accessToken = AuthManager.getAccessToken();
|
|
||||||
if (accessToken != null && !accessToken.equals("0")) {
|
|
||||||
requestBuilder.header("Authorization", "Bearer " + accessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpRequest request = requestBuilder.build();
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
throw new IOException("HTTP " + response.statusCode());
|
|
||||||
}
|
|
||||||
return response.body();
|
|
||||||
} catch (Exception e) {
|
|
||||||
directFailCount++;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String proxyGet(String endpoint) throws IOException {
|
|
||||||
try {
|
|
||||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(BASE_URL + "/proxy" + endpoint))
|
|
||||||
.timeout(Duration.ofSeconds(30))
|
|
||||||
.header("User-Agent", "ZernMC-Launcher/1.0")
|
|
||||||
.GET();
|
|
||||||
|
|
||||||
// ===== ДОБАВИТЬ ТОКЕН АВТОРИЗАЦИИ =====
|
|
||||||
String accessToken = AuthManager.getAccessToken();
|
|
||||||
if (accessToken != null && !accessToken.equals("0")) {
|
|
||||||
requestBuilder.header("Authorization", "Bearer " + accessToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
HttpRequest request = requestBuilder.build();
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
|
||||||
throw new IOException("HTTP " + response.statusCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
proxySuccessCount++;
|
|
||||||
return response.body();
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new IOException("Ошибка прокси: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== МЕТОДЫ ДЛЯ EXTERNAL РЕСУРСОВ ======================
|
|
||||||
|
|
||||||
public static List<String> getFabricLoaderVersions() throws IOException, InterruptedException {
|
|
||||||
String url = "https://meta.fabricmc.net/v2/versions/loader";
|
|
||||||
return parseFabricVersionsFromJson(getWithSmartProxy(url));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JSONObject getMojangVersionManifest() throws IOException, InterruptedException {
|
|
||||||
String url = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
|
|
||||||
String response = getWithSmartProxy(url);
|
|
||||||
return new JSONObject(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static JSONObject getMojangVersionJson(String versionId) throws IOException, InterruptedException {
|
|
||||||
JSONObject manifest = getMojangVersionManifest();
|
|
||||||
JSONArray versions = manifest.getJSONArray("versions");
|
|
||||||
|
|
||||||
for (int i = 0; i < versions.length(); i++) {
|
|
||||||
JSONObject v = versions.getJSONObject(i);
|
|
||||||
if (v.getString("id").equals(versionId)) {
|
|
||||||
return new JSONObject(getWithSmartProxy(v.getString("url")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new IOException("Version " + versionId + " not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getForgeVersionsXml() throws IOException, InterruptedException {
|
|
||||||
String url = "https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml";
|
|
||||||
return getWithSmartProxy(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void downloadFile(String url, Path target) throws Exception {
|
|
||||||
downloadFileWithSmartProxy(url, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void downloadAsset(String hash, Path target) throws Exception {
|
|
||||||
String url = "https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash;
|
|
||||||
downloadFileWithSmartProxy(url, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String downloadString(String url) throws IOException, InterruptedException {
|
|
||||||
return getWithSmartProxy(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<String> parseFabricVersionsFromJson(String json) {
|
|
||||||
JSONArray array = new JSONArray(json);
|
|
||||||
List<String> versions = new ArrayList<>();
|
|
||||||
for (int i = 0; i < array.length(); i++) {
|
|
||||||
JSONObject obj = array.getJSONObject(i);
|
|
||||||
if (obj.has("version")) {
|
|
||||||
versions.add(obj.getString("version"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return versions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ====================== ВСПОМОГАТЕЛЬНЫЕ ======================
|
|
||||||
|
|
||||||
public static String getLauncherVersionInfo() throws IOException, InterruptedException {
|
|
||||||
return get("/launcher/version");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void forceProxyMode() {
|
|
||||||
useProxyMode.set(true);
|
|
||||||
System.out.println(ZAnsi.yellow("Принудительно включен глобальный прокси режим"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void disableProxyMode() {
|
|
||||||
useProxyMode.set(false);
|
|
||||||
for (ServiceType type : ServiceType.values()) {
|
|
||||||
if (!type.isAlwaysDirect()) {
|
|
||||||
serviceProxyMode.put(type, false);
|
|
||||||
serviceFailCount.put(type, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
System.out.println(ZAnsi.green("Режим прокси выключен"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isProxyMode() {
|
|
||||||
return useProxyMode.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void printStats() {
|
|
||||||
System.out.println(ZAnsi.cyan("\n=== Статистика сети ==="));
|
|
||||||
System.out.println(ZAnsi.white("Глобальный прокси: ") + (useProxyMode.get() ? "ВКЛ" : "ВЫКЛ"));
|
|
||||||
System.out.println(ZAnsi.white("Прямых успехов: ") + directSuccessCount);
|
|
||||||
System.out.println(ZAnsi.white("Прямых неудач: ") + directFailCount);
|
|
||||||
System.out.println(ZAnsi.white("Прокси успехов: ") + proxySuccessCount);
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("\nСтатус сервисов:"));
|
|
||||||
for (ServiceType type : ServiceType.values()) {
|
|
||||||
if (type.isAlwaysDirect()) continue;
|
|
||||||
String status = serviceProxyMode.get(type) ? ZAnsi.red("ПРОКСИ") : ZAnsi.green("ПРЯМО");
|
|
||||||
String health = serviceHealthy.get(type) ? ZAnsi.green("[+]") : ZAnsi.red("[-]");
|
|
||||||
System.out.println(ZAnsi.white(" " + type.name() + ": ") + status + " " + health);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.auth;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for AuthManager error extraction and response parsing.
|
|
||||||
* Tests the contract between server error responses and Java client parsing.
|
|
||||||
*/
|
|
||||||
class AuthManagerParsingTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void extractError_simpleStringDetail() {
|
|
||||||
// Server: raise HTTPException(401, "Неверное имя пользователя или пароль")
|
|
||||||
String body = "{\"detail\":\"Неверное имя пользователя или пароль\"}";
|
|
||||||
String error = extractError(body);
|
|
||||||
assertEquals("Неверное имя пользователя или пароль", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void extractError_validationErrorArray() {
|
|
||||||
// FastAPI 422: {"detail": [{"loc": ["body", "username"], "msg": "...", "type": "..."}]}
|
|
||||||
String body = "{" +
|
|
||||||
"\"detail\":[" +
|
|
||||||
"{\"loc\":[\"body\",\"username\"],\"msg\":\"String should have at least 3 characters\",\"type\":\"string_too_short\"}" +
|
|
||||||
"]" +
|
|
||||||
"}";
|
|
||||||
String error = extractError(body);
|
|
||||||
assertEquals("String should have at least 3 characters", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void extractError_multipleValidationErrors_returnsFirst() {
|
|
||||||
String body = "{" +
|
|
||||||
"\"detail\":[" +
|
|
||||||
"{\"loc\":[\"body\",\"username\"],\"msg\":\"Username error\",\"type\":\"value_error\"}," +
|
|
||||||
"{\"loc\":[\"body\",\"password\"],\"msg\":\"Password error\",\"type\":\"value_error\"}" +
|
|
||||||
"]" +
|
|
||||||
"}";
|
|
||||||
String error = extractError(body);
|
|
||||||
assertEquals("Username error", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void extractError_plainTextBody() {
|
|
||||||
// Non-JSON error body
|
|
||||||
String body = "Internal Server Error";
|
|
||||||
String error = extractError(body);
|
|
||||||
assertEquals("Internal Server Error", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void extractError_longBody_truncated() {
|
|
||||||
String longBody = "A".repeat(300);
|
|
||||||
String error = extractError(longBody);
|
|
||||||
assertEquals(203, error.length()); // 200 + "..."
|
|
||||||
assertTrue(error.endsWith("..."));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void extractError_emptyDetail() {
|
|
||||||
String body = "{\"detail\":\"\"}";
|
|
||||||
String error = extractError(body);
|
|
||||||
assertEquals("", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void extractError_noDetailField_returnsBody() {
|
|
||||||
String body = "{\"error\":\"something went wrong\"}";
|
|
||||||
String error = extractError(body);
|
|
||||||
assertEquals("{\"error\":\"something went wrong\"}", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replicates AuthManager.extractError() logic for testing.
|
|
||||||
* If this passes, the real method in AuthManager works correctly.
|
|
||||||
*/
|
|
||||||
private static String extractError(String body) {
|
|
||||||
try {
|
|
||||||
com.google.gson.JsonObject json = com.google.gson.JsonParser.parseString(body).getAsJsonObject();
|
|
||||||
if (json.has("detail")) {
|
|
||||||
if (json.get("detail").isJsonArray()) {
|
|
||||||
return json.getAsJsonArray("detail").get(0).getAsJsonObject().get("msg").getAsString();
|
|
||||||
}
|
|
||||||
return json.get("detail").getAsString();
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
return body.length() > 200 ? body.substring(0, 200) + "..." : body;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-469
@@ -1,469 +0,0 @@
|
|||||||
package me.sashegdev.zernmc.launcher.integration;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.*;
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.GsonBuilder;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
|
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Integration tests: real Java client ↔ real Python server.
|
|
||||||
*
|
|
||||||
* These tests:
|
|
||||||
* 1. Start the FastAPI test server via Python subprocess
|
|
||||||
* 2. Use actual Java HTTP client code to make requests
|
|
||||||
* 3. Verify JSON parsing and response handling
|
|
||||||
*
|
|
||||||
* Requires: Python 3, pytest, and the server/.venv to be available.
|
|
||||||
*/
|
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
|
||||||
class ServerIntegrationTest {
|
|
||||||
|
|
||||||
private static Process serverProcess;
|
|
||||||
private static String serverBaseUrl;
|
|
||||||
private static Path testDir;
|
|
||||||
private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
|
||||||
|
|
||||||
@BeforeAll
|
|
||||||
static void startTestServer() throws Exception {
|
|
||||||
// Create temp directory for test data
|
|
||||||
testDir = Files.createTempDirectory("zern_integration_test_");
|
|
||||||
|
|
||||||
// Find the server directory
|
|
||||||
String serverDir = findServerDir();
|
|
||||||
if (serverDir == null) {
|
|
||||||
System.out.println("WARNING: Server directory not found, skipping integration tests");
|
|
||||||
serverBaseUrl = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the test server on a random port
|
|
||||||
int port = findFreePort();
|
|
||||||
serverBaseUrl = "http://127.0.0.1:" + port;
|
|
||||||
|
|
||||||
System.out.println("Starting test server on " + serverBaseUrl);
|
|
||||||
System.out.println("Server directory: " + serverDir);
|
|
||||||
|
|
||||||
// Find Python executable (prefer venv python)
|
|
||||||
String pythonPath = findPythonPath(serverDir);
|
|
||||||
if (pythonPath == null) {
|
|
||||||
System.out.println("WARNING: Python not found, skipping integration tests");
|
|
||||||
serverBaseUrl = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a Python startup script that properly sets up paths
|
|
||||||
String startupScript =
|
|
||||||
"import sys, os, tempfile\n" +
|
|
||||||
"from pathlib import Path\n" +
|
|
||||||
"sys.path.insert(0, '" + serverDir + "')\n" +
|
|
||||||
"os.chdir('" + serverDir + "')\n" +
|
|
||||||
"import auth\n" +
|
|
||||||
"db_dir = tempfile.mkdtemp()\n" +
|
|
||||||
"auth.AUTH_DB = Path(db_dir) / 'auth.db'\n" +
|
|
||||||
"auth.SECRET_KEY = Path(db_dir) / '.secret_key'\n" +
|
|
||||||
"auth.init_db()\n" +
|
|
||||||
"import uvicorn\n" +
|
|
||||||
"import main\n" +
|
|
||||||
"uvicorn.run(main.app, host='127.0.0.1', port=" + port + ", log_level='error')\n";
|
|
||||||
|
|
||||||
ProcessBuilder pb = new ProcessBuilder(pythonPath, "-c", startupScript);
|
|
||||||
pb.directory(new File(serverDir));
|
|
||||||
pb.redirectErrorStream(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
serverProcess = pb.start();
|
|
||||||
System.out.println("Server process started, PID: " + serverProcess.pid());
|
|
||||||
} catch (IOException e) {
|
|
||||||
System.out.println("WARNING: Could not start server process: " + e.getMessage());
|
|
||||||
System.out.println("Skipping integration tests");
|
|
||||||
serverBaseUrl = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for server to start
|
|
||||||
Thread.sleep(4000);
|
|
||||||
|
|
||||||
// Verify server is running
|
|
||||||
try {
|
|
||||||
URL url = new URL(serverBaseUrl + "/health");
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setConnectTimeout(5000);
|
|
||||||
conn.connect();
|
|
||||||
if (conn.getResponseCode() != 200) {
|
|
||||||
System.out.println("WARNING: Server health check failed: " + conn.getResponseCode());
|
|
||||||
System.out.println("Skipping integration tests");
|
|
||||||
serverBaseUrl = null;
|
|
||||||
if (serverProcess != null) serverProcess.destroy();
|
|
||||||
conn.disconnect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
conn.disconnect();
|
|
||||||
System.out.println("Test server started successfully");
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println("WARNING: Server failed to start: " + e.getMessage());
|
|
||||||
System.out.println("Skipping integration tests");
|
|
||||||
serverBaseUrl = null;
|
|
||||||
if (serverProcess != null) serverProcess.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterAll
|
|
||||||
static void stopTestServer() {
|
|
||||||
if (serverProcess != null) {
|
|
||||||
serverProcess.destroy();
|
|
||||||
try {
|
|
||||||
serverProcess.waitFor(5000, java.util.concurrent.TimeUnit.MILLISECONDS);
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
// Cleanup temp dir
|
|
||||||
if (testDir != null) {
|
|
||||||
try {
|
|
||||||
Files.walk(testDir)
|
|
||||||
.sorted(java.util.Comparator.reverseOrder())
|
|
||||||
.forEach(path -> {
|
|
||||||
try { Files.delete(path); } catch (IOException ignored) {}
|
|
||||||
});
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
if (serverBaseUrl != null) {
|
|
||||||
ZHttpClient.setBaseUrl(serverBaseUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Auth flow tests =====
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(1)
|
|
||||||
void testRegister() throws Exception {
|
|
||||||
assumeServerRunning();
|
|
||||||
|
|
||||||
String response = httpPost("/auth/register", "{" +
|
|
||||||
"\"username\":\"integration_test_user\"," +
|
|
||||||
"\"password\":\"IntegrationTest123\"" +
|
|
||||||
"}");
|
|
||||||
|
|
||||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
|
||||||
assertTrue(json.has("access_token"));
|
|
||||||
assertTrue(json.has("refresh_token"));
|
|
||||||
assertTrue(json.has("expires_in"));
|
|
||||||
assertTrue(json.has("uuid"));
|
|
||||||
assertEquals("integration_test_user", json.get("username").getAsString());
|
|
||||||
assertTrue(json.has("role"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(2)
|
|
||||||
void testLogin() throws Exception {
|
|
||||||
assumeServerRunning();
|
|
||||||
|
|
||||||
String response = httpPost("/auth/login", "{" +
|
|
||||||
"\"username\":\"integration_test_user\"," +
|
|
||||||
"\"password\":\"IntegrationTest123\"" +
|
|
||||||
"}");
|
|
||||||
|
|
||||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
|
||||||
assertTrue(json.has("access_token"));
|
|
||||||
assertTrue(json.has("refresh_token"));
|
|
||||||
assertEquals("integration_test_user", json.get("username").getAsString());
|
|
||||||
assertTrue(json.has("role"));
|
|
||||||
assertTrue(json.has("uuid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(3)
|
|
||||||
void testDuplicateRegistration() throws Exception {
|
|
||||||
assumeServerRunning();
|
|
||||||
|
|
||||||
try {
|
|
||||||
httpPost("/auth/register", "{" +
|
|
||||||
"\"username\":\"integration_test_user\"," +
|
|
||||||
"\"password\":\"AnotherPassword123\"" +
|
|
||||||
"}");
|
|
||||||
fail("Should have thrown IOException for duplicate registration");
|
|
||||||
} catch (IOException e) {
|
|
||||||
assertTrue(e.getMessage().contains("409") || e.getMessage().contains("409"),
|
|
||||||
"Expected 409 conflict, got: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(4)
|
|
||||||
void testLoginWrongPassword() throws Exception {
|
|
||||||
assumeServerRunning();
|
|
||||||
|
|
||||||
try {
|
|
||||||
httpPost("/auth/login", "{" +
|
|
||||||
"\"username\":\"integration_test_user\"," +
|
|
||||||
"\"password\":\"WrongPassword\"" +
|
|
||||||
"}");
|
|
||||||
fail("Should have thrown IOException for wrong password");
|
|
||||||
} catch (IOException e) {
|
|
||||||
assertTrue(e.getMessage().contains("401"),
|
|
||||||
"Expected 401, got: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(5)
|
|
||||||
void testGetAdminMe() throws Exception {
|
|
||||||
assumeServerRunning();
|
|
||||||
|
|
||||||
// Login to get token
|
|
||||||
String loginResp = httpPost("/auth/login", "{" +
|
|
||||||
"\"username\":\"integration_test_user\"," +
|
|
||||||
"\"password\":\"IntegrationTest123\"" +
|
|
||||||
"}");
|
|
||||||
JsonObject loginJson = JsonParser.parseString(loginResp).getAsJsonObject();
|
|
||||||
String token = loginJson.get("access_token").getAsString();
|
|
||||||
|
|
||||||
// Get user info
|
|
||||||
String response = httpGet("/admin/me", token);
|
|
||||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
|
||||||
|
|
||||||
assertTrue(json.has("id"));
|
|
||||||
assertEquals("integration_test_user", json.get("username").getAsString());
|
|
||||||
assertTrue(json.has("uuid"));
|
|
||||||
assertTrue(json.has("role"));
|
|
||||||
assertTrue(json.has("role_name"));
|
|
||||||
assertTrue(json.has("has_pass"));
|
|
||||||
assertTrue(json.has("permissions"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(6)
|
|
||||||
void testValidateToken() throws Exception {
|
|
||||||
assumeServerRunning();
|
|
||||||
|
|
||||||
String loginResp = httpPost("/auth/login", "{" +
|
|
||||||
"\"username\":\"integration_test_user\"," +
|
|
||||||
"\"password\":\"IntegrationTest123\"" +
|
|
||||||
"}");
|
|
||||||
JsonObject loginJson = JsonParser.parseString(loginResp).getAsJsonObject();
|
|
||||||
String token = loginJson.get("access_token").getAsString();
|
|
||||||
String uuid = loginJson.get("uuid").getAsString();
|
|
||||||
|
|
||||||
// Validate
|
|
||||||
String response = httpPost("/auth/validate",
|
|
||||||
"{\"username\":\"integration_test_user\",\"uuid\":\"" + uuid + "\"}",
|
|
||||||
token);
|
|
||||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
|
||||||
|
|
||||||
assertTrue(json.has("valid"));
|
|
||||||
assertTrue(json.get("valid").getAsBoolean());
|
|
||||||
assertEquals("integration_test_user", json.get("username").getAsString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(7)
|
|
||||||
void testRefreshToken() throws Exception {
|
|
||||||
assumeServerRunning();
|
|
||||||
|
|
||||||
String loginResp = httpPost("/auth/login", "{" +
|
|
||||||
"\"username\":\"integration_test_user\"," +
|
|
||||||
"\"password\":\"IntegrationTest123\"" +
|
|
||||||
"}");
|
|
||||||
JsonObject loginJson = JsonParser.parseString(loginResp).getAsJsonObject();
|
|
||||||
String refreshToken = loginJson.get("refresh_token").getAsString();
|
|
||||||
|
|
||||||
// Refresh
|
|
||||||
String response = httpPost("/auth/refresh",
|
|
||||||
"{\"refresh_token\":\"" + refreshToken + "\"}");
|
|
||||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
|
||||||
|
|
||||||
assertTrue(json.has("access_token"));
|
|
||||||
assertTrue(json.has("refresh_token"));
|
|
||||||
assertTrue(json.has("expires_in"));
|
|
||||||
assertEquals("integration_test_user", json.get("username").getAsString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Pack endpoint tests =====
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(8)
|
|
||||||
void testPacksNoAuth() throws Exception {
|
|
||||||
assumeServerRunning();
|
|
||||||
|
|
||||||
try {
|
|
||||||
httpGet("/packs");
|
|
||||||
fail("Should have thrown IOException for unauthenticated access");
|
|
||||||
} catch (IOException e) {
|
|
||||||
assertTrue(e.getMessage().contains("401") || e.getMessage().contains("403"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(9)
|
|
||||||
void testPackManifestPublic() throws Exception {
|
|
||||||
assumeServerRunning();
|
|
||||||
|
|
||||||
// /pack/{name} is public
|
|
||||||
try {
|
|
||||||
String response = httpGet("/pack/nonexistent-pack");
|
|
||||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
|
||||||
fail("Should have thrown IOException for non-existent pack");
|
|
||||||
} catch (IOException e) {
|
|
||||||
assertTrue(e.getMessage().contains("404"),
|
|
||||||
"Expected 404, got: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Order(10)
|
|
||||||
void testLauncherVersion() throws Exception {
|
|
||||||
assumeServerRunning();
|
|
||||||
|
|
||||||
String response = httpGet("/launcher/version");
|
|
||||||
JsonObject json = JsonParser.parseString(response).getAsJsonObject();
|
|
||||||
assertTrue(json.has("version") || json.has("latest"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Helper methods =====
|
|
||||||
|
|
||||||
private static void assumeServerRunning() {
|
|
||||||
org.junit.jupiter.api.Assumptions.assumeTrue(
|
|
||||||
serverBaseUrl != null && serverProcess != null && serverProcess.isAlive(),
|
|
||||||
"Test server is not running"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String httpPost(String endpoint, String body) throws IOException {
|
|
||||||
return httpPost(endpoint, body, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String httpPost(String endpoint, String body, String token) throws IOException {
|
|
||||||
URL url = new URL(serverBaseUrl + endpoint);
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
conn.setRequestProperty("Content-Type", "application/json");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
if (token != null) {
|
|
||||||
conn.setRequestProperty("Authorization", "Bearer " + token);
|
|
||||||
}
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
conn.setConnectTimeout(10000);
|
|
||||||
conn.setReadTimeout(10000);
|
|
||||||
|
|
||||||
byte[] input = body.getBytes(StandardCharsets.UTF_8);
|
|
||||||
conn.setFixedLengthStreamingMode(input.length);
|
|
||||||
try (var os = conn.getOutputStream()) {
|
|
||||||
os.write(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
int code = conn.getResponseCode();
|
|
||||||
String response = readResponse(conn, code);
|
|
||||||
|
|
||||||
if (code >= 400) {
|
|
||||||
throw new IOException("HTTP " + code + ": " + response);
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.disconnect();
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String httpGet(String endpoint) throws IOException {
|
|
||||||
return httpGet(endpoint, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String httpGet(String endpoint, String token) throws IOException {
|
|
||||||
URL url = new URL(serverBaseUrl + endpoint);
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
conn.setRequestProperty("Accept", "application/json");
|
|
||||||
if (token != null) {
|
|
||||||
conn.setRequestProperty("Authorization", "Bearer " + token);
|
|
||||||
}
|
|
||||||
conn.setConnectTimeout(10000);
|
|
||||||
conn.setReadTimeout(10000);
|
|
||||||
|
|
||||||
int code = conn.getResponseCode();
|
|
||||||
String response = readResponse(conn, code);
|
|
||||||
|
|
||||||
if (code >= 400) {
|
|
||||||
throw new IOException("HTTP " + code + ": " + response);
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.disconnect();
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String readResponse(HttpURLConnection conn, int code) throws IOException {
|
|
||||||
var is = (code >= 200 && code < 300) ? conn.getInputStream() : conn.getErrorStream();
|
|
||||||
if (is == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
try (var scanner = new java.util.Scanner(is, StandardCharsets.UTF_8.name())) {
|
|
||||||
return scanner.useDelimiter("\\A").hasNext() ? scanner.next() : "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String findPythonPath(String serverDir) {
|
|
||||||
String[] paths = {
|
|
||||||
serverDir + "/.venv/bin/python3",
|
|
||||||
serverDir + "/.venv/bin/python",
|
|
||||||
"python3",
|
|
||||||
"python"
|
|
||||||
};
|
|
||||||
for (String path : paths) {
|
|
||||||
File f = new File(path);
|
|
||||||
if (f.exists() && f.canExecute()) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
// Try which command
|
|
||||||
try {
|
|
||||||
Process p = new ProcessBuilder(path, "--version").start();
|
|
||||||
int exit = p.waitFor();
|
|
||||||
if (exit == 0) return path;
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String findServerDir() {
|
|
||||||
String[] paths = {
|
|
||||||
"../server",
|
|
||||||
"server",
|
|
||||||
System.getenv("SERVER_DIR")
|
|
||||||
};
|
|
||||||
for (String path : paths) {
|
|
||||||
if (path != null && new File(path).exists() && new File(path, "main.py").exists()) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int findFreePort() throws IOException {
|
|
||||||
try (java.net.ServerSocket socket = new java.net.ServerSocket(0)) {
|
|
||||||
return socket.getLocalPort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String readProcessOutput() throws IOException {
|
|
||||||
if (serverProcess == null) return "";
|
|
||||||
try (BufferedReader reader = new BufferedReader(
|
|
||||||
new InputStreamReader(serverProcess.getInputStream(), StandardCharsets.UTF_8))) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
String line;
|
|
||||||
while ((line = reader.readLine()) != null) {
|
|
||||||
sb.append(line).append("\n");
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-287
@@ -1,287 +0,0 @@
|
|||||||
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.JsonObject;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unit tests for PackDownloader JSON parsing.
|
|
||||||
* Tests that the Java client correctly parses server JSON responses.
|
|
||||||
*/
|
|
||||||
class PackDownloaderParsingTest {
|
|
||||||
|
|
||||||
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
|
||||||
|
|
||||||
// ===== /packs response parsing =====
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parsePacksResponse_singlePack() {
|
|
||||||
String body = "{" +
|
|
||||||
"\"packs\":[" +
|
|
||||||
"{" +
|
|
||||||
"\"name\":\"test-modpack\"," +
|
|
||||||
"\"version\":3," +
|
|
||||||
"\"files_count\":15," +
|
|
||||||
"\"updated_at\":\"2024-01-15T10:30:00\"," +
|
|
||||||
"\"minecraft_version\":\"1.20.4\"," +
|
|
||||||
"\"loader_type\":\"fabric\"," +
|
|
||||||
"\"loader_version\":\"0.15.6\"" +
|
|
||||||
"}" +
|
|
||||||
"]" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
List<ServerPack> packs = parsePacksResponse(body);
|
|
||||||
assertEquals(1, packs.size());
|
|
||||||
|
|
||||||
ServerPack pack = packs.get(0);
|
|
||||||
assertEquals("test-modpack", pack.getName());
|
|
||||||
assertEquals(3, pack.getVersion());
|
|
||||||
assertEquals(15, pack.getFilesCount());
|
|
||||||
assertEquals("1.20.4", pack.getMinecraftVersion());
|
|
||||||
assertEquals("fabric", pack.getLoaderType());
|
|
||||||
assertEquals("0.15.6", pack.getLoaderVersion());
|
|
||||||
assertNotNull(pack.getUpdatedAt());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parsePacksResponse_multiplePacks() {
|
|
||||||
String body = "{" +
|
|
||||||
"\"packs\":[" +
|
|
||||||
"{\"name\":\"survival\",\"version\":1,\"files_count\":5,\"minecraft_version\":\"1.20.1\",\"loader_type\":\"vanilla\",\"loader_version\":null,\"updated_at\":null}," +
|
|
||||||
"{\"name\":\"pvp\",\"version\":10,\"files_count\":50,\"minecraft_version\":\"1.20.4\",\"loader_type\":\"fabric\",\"loader_version\":\"0.15.6\",\"updated_at\":\"2024-02-01T00:00:00\"}" +
|
|
||||||
"]" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
List<ServerPack> packs = parsePacksResponse(body);
|
|
||||||
assertEquals(2, packs.size());
|
|
||||||
assertEquals("survival", packs.get(0).getName());
|
|
||||||
assertEquals("pvp", packs.get(1).getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parsePacksResponse_skipsErroredPacks() {
|
|
||||||
String body = "{" +
|
|
||||||
"\"packs\":[" +
|
|
||||||
"{\"name\":\"good-pack\",\"version\":1,\"files_count\":1,\"minecraft_version\":\"1.20.1\",\"loader_type\":\"vanilla\",\"loader_version\":null,\"updated_at\":null}," +
|
|
||||||
"{\"name\":\"bad-pack\",\"error\":\"scan failed\"}," +
|
|
||||||
"{\"name\":\"not-scanned\",\"status\":\"not_scanned\"}" +
|
|
||||||
"]" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
List<ServerPack> packs = parsePacksResponse(body);
|
|
||||||
assertEquals(1, packs.size());
|
|
||||||
assertEquals("good-pack", packs.get(0).getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parsePacksResponse_missingFields_defaults() {
|
|
||||||
String body = "{" +
|
|
||||||
"\"packs\":[" +
|
|
||||||
"{\"name\":\"minimal-pack\"}" +
|
|
||||||
"]" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
List<ServerPack> packs = parsePacksResponse(body);
|
|
||||||
assertEquals(1, packs.size());
|
|
||||||
|
|
||||||
ServerPack pack = packs.get(0);
|
|
||||||
assertEquals("minimal-pack", pack.getName());
|
|
||||||
assertEquals(0, pack.getVersion()); // default
|
|
||||||
assertEquals("unknown", pack.getMinecraftVersion()); // default
|
|
||||||
assertEquals("vanilla", pack.getLoaderType()); // default
|
|
||||||
assertEquals("", pack.getLoaderVersion()); // default
|
|
||||||
assertEquals(0, pack.getFilesCount()); // default
|
|
||||||
assertNull(pack.getUpdatedAt()); // default
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parsePacksResponse_emptyList() {
|
|
||||||
String body = "{\"packs\":[]}";
|
|
||||||
List<ServerPack> packs = parsePacksResponse(body);
|
|
||||||
assertTrue(packs.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== PackManifest parsing =====
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parsePackManifest_withFiles() {
|
|
||||||
String body = "{" +
|
|
||||||
"\"pack_name\":\"my-pack\"," +
|
|
||||||
"\"version\":5," +
|
|
||||||
"\"minecraft_version\":\"1.20.4\"," +
|
|
||||||
"\"loader_type\":\"fabric\"," +
|
|
||||||
"\"loader_version\":\"0.15.6\"," +
|
|
||||||
"\"asset_index\":\"1.20.4\"," +
|
|
||||||
"\"files\":{" +
|
|
||||||
"\"mods/sodium.jar\":{\"path\":\"mods/sodium.jar\",\"url\":\"/pack/my-pack/file/mods/sodium.jar\",\"size\":1024000,\"hash\":\"abc123\"}," +
|
|
||||||
"\"mods/fabric-api.jar\":{\"path\":\"mods/fabric-api.jar\",\"url\":\"/pack/my-pack/file/mods/fabric-api.jar\",\"size\":2048000,\"hash\":\"def456\"}" +
|
|
||||||
"}" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
PackDownloader.PackManifest manifest = gson.fromJson(body, PackDownloader.PackManifest.class);
|
|
||||||
|
|
||||||
assertEquals("my-pack", manifest.getPackName());
|
|
||||||
assertEquals(5, manifest.getVersion());
|
|
||||||
assertEquals("1.20.4", manifest.getMinecraftVersion());
|
|
||||||
assertEquals("fabric", manifest.getLoaderType());
|
|
||||||
assertEquals("0.15.6", manifest.getLoaderVersion());
|
|
||||||
assertEquals("1.20.4", manifest.getAssetIndex());
|
|
||||||
assertFalse(manifest.isEmpty());
|
|
||||||
assertEquals(2, manifest.getFiles().size());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parsePackManifest_nullAssetIndex_defaultsToMinecraftVersion() {
|
|
||||||
String body = "{" +
|
|
||||||
"\"pack_name\":\"no-asset\"," +
|
|
||||||
"\"version\":1," +
|
|
||||||
"\"minecraft_version\":\"1.19.4\"," +
|
|
||||||
"\"loader_type\":\"vanilla\"," +
|
|
||||||
"\"loader_version\":null" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
PackDownloader.PackManifest manifest = gson.fromJson(body, PackDownloader.PackManifest.class);
|
|
||||||
assertEquals("1.19.4", manifest.getAssetIndex()); // defaults to minecraft_version
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parsePackManifest_noFiles_isEmpty() {
|
|
||||||
String body = "{" +
|
|
||||||
"\"pack_name\":\"empty-pack\"," +
|
|
||||||
"\"version\":1," +
|
|
||||||
"\"minecraft_version\":\"1.20.1\"," +
|
|
||||||
"\"loader_type\":\"vanilla\"," +
|
|
||||||
"\"loader_version\":null" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
PackDownloader.PackManifest manifest = gson.fromJson(body, PackDownloader.PackManifest.class);
|
|
||||||
assertTrue(manifest.isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== DiffResponse parsing =====
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parseDiffResponse_allFields() {
|
|
||||||
String body = "{" +
|
|
||||||
"\"version\":6," +
|
|
||||||
"\"to_download\":[" +
|
|
||||||
"{\"path\":\"mods/new-mod.jar\",\"url\":\"/pack/test/file/mods/new-mod.jar\",\"size\":512000,\"hash\":\"aaa111\"}" +
|
|
||||||
"]," +
|
|
||||||
"\"to_delete\":[\"mods/old-mod.jar\"]," +
|
|
||||||
"\"to_update\":[\"mods/updated-mod.jar\"]" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
PackDownloader.DiffResponse diff = gson.fromJson(body, PackDownloader.DiffResponse.class);
|
|
||||||
|
|
||||||
assertEquals(6, diff.getVersion());
|
|
||||||
assertEquals(1, diff.getToDownload().size());
|
|
||||||
assertEquals(1, diff.getToDelete().size());
|
|
||||||
assertEquals(1, diff.getToUpdate().size());
|
|
||||||
|
|
||||||
PackDownloader.FileInfo fileInfo = diff.getToDownload().get(0);
|
|
||||||
assertEquals("mods/new-mod.jar", fileInfo.getPath());
|
|
||||||
assertEquals("/pack/test/file/mods/new-mod.jar", fileInfo.getUrl());
|
|
||||||
assertEquals(512000, fileInfo.getSize());
|
|
||||||
assertEquals("aaa111", fileInfo.getHash());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parseDiffResponse_emptyArrays() {
|
|
||||||
String body = "{" +
|
|
||||||
"\"version\":1," +
|
|
||||||
"\"to_download\":[]," +
|
|
||||||
"\"to_delete\":[]," +
|
|
||||||
"\"to_update\":[]" +
|
|
||||||
"}";
|
|
||||||
|
|
||||||
PackDownloader.DiffResponse diff = gson.fromJson(body, PackDownloader.DiffResponse.class);
|
|
||||||
assertTrue(diff.getToDownload().isEmpty());
|
|
||||||
assertTrue(diff.getToDelete().isEmpty());
|
|
||||||
assertTrue(diff.getToUpdate().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void parseDiffResponse_nullArrays_returnsEmpty() {
|
|
||||||
String body = "{\"version\":1}";
|
|
||||||
|
|
||||||
PackDownloader.DiffResponse diff = gson.fromJson(body, PackDownloader.DiffResponse.class);
|
|
||||||
assertNotNull(diff.getToDownload());
|
|
||||||
assertNotNull(diff.getToDelete());
|
|
||||||
assertNotNull(diff.getToUpdate());
|
|
||||||
assertTrue(diff.getToDownload().isEmpty());
|
|
||||||
assertTrue(diff.getToDelete().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== ServerPack toString =====
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void serverPack_toString_withDate() {
|
|
||||||
java.time.LocalDateTime date = java.time.LocalDateTime.of(2024, 3, 15, 12, 0);
|
|
||||||
ServerPack pack = new ServerPack("my-pack", 2, "1.20.4", "fabric", "0.15.6", date, 25);
|
|
||||||
|
|
||||||
String str = pack.toString();
|
|
||||||
assertTrue(str.contains("my-pack"));
|
|
||||||
assertTrue(str.contains("1.20.4"));
|
|
||||||
assertTrue(str.contains("fabric"));
|
|
||||||
assertTrue(str.contains("25 файлов"));
|
|
||||||
assertTrue(str.contains("15.03.2024"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void serverPack_toString_withoutDate() {
|
|
||||||
ServerPack pack = new ServerPack("my-pack", 2, "1.20.4", "fabric", "0.15.6", null, 25);
|
|
||||||
|
|
||||||
String str = pack.toString();
|
|
||||||
assertTrue(str.contains("my-pack"));
|
|
||||||
assertTrue(str.contains("25 файлов"));
|
|
||||||
assertFalse(str.contains("обновлен"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Helper: replicates PackDownloader.parsePacksResponse() =====
|
|
||||||
|
|
||||||
private static List<ServerPack> parsePacksResponse(String responseBody) {
|
|
||||||
JsonObject root = com.google.gson.JsonParser.parseString(responseBody).getAsJsonObject();
|
|
||||||
JsonArray packsArray = root.getAsJsonArray("packs");
|
|
||||||
List<ServerPack> result = new ArrayList<>();
|
|
||||||
|
|
||||||
for (var elem : packsArray) {
|
|
||||||
JsonObject pack = elem.getAsJsonObject();
|
|
||||||
|
|
||||||
if (pack.has("error") || (pack.has("status") && "not_scanned".equals(pack.get("status").getAsString()))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String name = pack.get("name").getAsString();
|
|
||||||
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;
|
|
||||||
|
|
||||||
java.time.LocalDateTime updatedAt = null;
|
|
||||||
if (pack.has("updated_at") && !pack.get("updated_at").isJsonNull()) {
|
|
||||||
try {
|
|
||||||
updatedAt = java.time.LocalDateTime.parse(pack.get("updated_at").getAsString(),
|
|
||||||
java.time.format.DateTimeFormatter.ISO_DATE_TIME);
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
result.add(new ServerPack(name, version, minecraftVersion, loaderType,
|
|
||||||
loaderVersion, updatedAt, filesCount));
|
|
||||||
} catch (Exception e) {
|
|
||||||
System.err.println("Ошибка парсинга пака: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user