Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 454d3389b6 | |||
| 012d1635dc | |||
| 70c4815032 | |||
| a46ef3e834 | |||
| cba8259e59 | |||
| 08417efe2f | |||
| 991252130d | |||
| 6fa97b7fda | |||
| 83abc600f3 | |||
| 300ce4b60b | |||
| 82391e10ea | |||
| 1934199ba8 | |||
| 526a24a16a | |||
| 96baeeea68 | |||
| 28995adce8 | |||
| 3f2cb6662a | |||
| 11513fbf13 |
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
- Графического интерфейса (GUI) — только TUI
|
- Графического интерфейса (GUI) — только TUI
|
||||||
- Нормальных настроек (пока доступна только настройка Java и выделенной оперативной памяти)
|
- Нормальных настроек (пока доступна только настройка Java и выделенной оперативной памяти)
|
||||||
- Поддержки **Forge** (в разработке)
|
- Поддержки **Forge** (в разработке) (технически уже есть вместе с NeoForge (science PR№4))
|
||||||
- Поддержки Quilt, LabyMod, NeoForge и других лоадеров
|
- Поддержки Quilt, LabyMod, NeoForge и других лоадеров
|
||||||
- Раздела новостей об обновлениях Minecraft и лаунчера
|
- Раздела новостей об обновлениях Minecraft и лаунчера
|
||||||
- Выбора готовых пресетов оптимизации JVM
|
- Выбора готовых пресетов оптимизации JVM
|
||||||
|
|||||||
@@ -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,11 +28,54 @@
|
|||||||
<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>
|
||||||
</transformers>
|
</transformers>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
<filter>
|
||||||
|
<artifact>org.openjfx:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>**/*</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
<dependencySet>
|
||||||
|
<outputDirectory>/</outputDirectory>
|
||||||
|
<useProjectArtifact>false</useProjectArtifact>
|
||||||
|
<unpack>true</unpack>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
<excludes>
|
||||||
|
<exclude>org.openjfx:*</exclude>
|
||||||
|
</excludes>
|
||||||
|
</dependencySet>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>copy-javafx</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>copy-dependencies</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<outputDirectory>${project.build.directory}/lib-javafx</outputDirectory>
|
||||||
|
<includeScope>runtime</includeScope>
|
||||||
|
<includeGroupIds>org.openjfx</includeGroupIds>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
@@ -45,28 +92,36 @@
|
|||||||
<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>gui</headerType>
|
||||||
<dontWrapJar>false</dontWrapJar>
|
<dontWrapJar>false</dontWrapJar>
|
||||||
<jre>
|
<jre>
|
||||||
<path>jre21</path>
|
<path>jre21</path>
|
||||||
<minVersion>21</minVersion>
|
<minVersion>21</minVersion>
|
||||||
|
<opts>
|
||||||
|
<opt>--module-path=lib-javafx</opt>
|
||||||
|
<opt>--add-modules=javafx.controls,javafx.web</opt>
|
||||||
|
<opt>--add-reads=javafx.graphics=ALL-UNNAMED</opt>
|
||||||
|
</opts>
|
||||||
</jre>
|
</jre>
|
||||||
<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>
|
||||||
</executions>
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<skip>${skip.launch4j}</skip>
|
||||||
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-antrun-plugin</artifactId>
|
<artifactId>maven-antrun-plugin</artifactId>
|
||||||
@@ -83,6 +138,9 @@
|
|||||||
<copy>
|
<copy>
|
||||||
<fileset />
|
<fileset />
|
||||||
</copy>
|
</copy>
|
||||||
|
<copy>
|
||||||
|
<fileset />
|
||||||
|
</copy>
|
||||||
<zip />
|
<zip />
|
||||||
</target>
|
</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -108,11 +166,55 @@
|
|||||||
<server.url>http://87.120.187.36:1582</server.url>
|
<server.url>http://87.120.187.36:1582</server.url>
|
||||||
</properties>
|
</properties>
|
||||||
</profile>
|
</profile>
|
||||||
|
<profile>
|
||||||
|
<id>win</id>
|
||||||
|
<properties>
|
||||||
|
<os.suffix>win</os.suffix>
|
||||||
|
<javafx.classifier>win</javafx.classifier>
|
||||||
|
<skip.launch4j>false</skip.launch4j>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
<profile>
|
||||||
|
<id>linux</id>
|
||||||
|
<properties>
|
||||||
|
<os.suffix>linux</os.suffix>
|
||||||
|
<javafx.classifier>linux</javafx.classifier>
|
||||||
|
<skip.launch4j>true</skip.launch4j>
|
||||||
|
</properties>
|
||||||
|
</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>
|
||||||
|
<javafx.classifier>win</javafx.classifier>
|
||||||
|
<skip.launch4j>false</skip.launch4j>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<os.suffix>win</os.suffix>
|
||||||
<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>
|
||||||
|
|||||||
Binary file not shown.
+141
-6
@@ -17,6 +17,9 @@
|
|||||||
<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 - just a minimalistic launcher by SashegDev</project.description>
|
||||||
<mainClass>me.sashegdev.zernmc.launcher.Main</mainClass>
|
<mainClass>me.sashegdev.zernmc.launcher.Main</mainClass>
|
||||||
|
<javafx.classifier>win</javafx.classifier>
|
||||||
|
<os.suffix>win</os.suffix>
|
||||||
|
<skip.launch4j>false</skip.launch4j>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -60,6 +63,42 @@
|
|||||||
<artifactId>commons-io</artifactId>
|
<artifactId>commons-io</artifactId>
|
||||||
<version>2.15.1</version>
|
<version>2.15.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.javalin</groupId>
|
||||||
|
<artifactId>javalin</artifactId>
|
||||||
|
<version>6.1.3</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<version>2.0.11</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-controls</artifactId>
|
||||||
|
<version>21.0.2</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-web</artifactId>
|
||||||
|
<version>21.0.2</version>
|
||||||
|
<classifier>win</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-controls</artifactId>
|
||||||
|
<version>21.0.2</version>
|
||||||
|
<classifier>linux</classifier>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openjfx</groupId>
|
||||||
|
<artifactId>javafx-web</artifactId>
|
||||||
|
<version>21.0.2</version>
|
||||||
|
<classifier>linux</classifier>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
@@ -101,16 +140,66 @@
|
|||||||
</manifestEntries>
|
</manifestEntries>
|
||||||
</transformer>
|
</transformer>
|
||||||
</transformers>
|
</transformers>
|
||||||
|
<filters>
|
||||||
|
<filter>
|
||||||
|
<artifact>*:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>META-INF/*.SF</exclude>
|
||||||
|
<exclude>META-INF/*.DSA</exclude>
|
||||||
|
<exclude>META-INF/*.RSA</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
<!-- Исключаем JavaFX из shade полностью (он будет в lib-javafx) -->
|
||||||
|
<filter>
|
||||||
|
<artifact>org.openjfx:*</artifact>
|
||||||
|
<excludes>
|
||||||
|
<exclude>**/*</exclude>
|
||||||
|
</excludes>
|
||||||
|
</filter>
|
||||||
|
</filters>
|
||||||
|
<dependencySet>
|
||||||
|
<outputDirectory>/</outputDirectory>
|
||||||
|
<useProjectArtifact>false</useProjectArtifact>
|
||||||
|
<unpack>true</unpack>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
<excludes>
|
||||||
|
<exclude>org.openjfx:*</exclude>
|
||||||
|
</excludes>
|
||||||
|
</dependencySet>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<!-- Launch4j для создания .exe -->
|
<!-- Copy JavaFX dependencies -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>copy-javafx</id>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>copy-dependencies</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<outputDirectory>${project.build.directory}/lib-javafx</outputDirectory>
|
||||||
|
<includeScope>runtime</includeScope>
|
||||||
|
<includeGroupIds>org.openjfx</includeGroupIds>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<!-- Launch4j для создания .exe (только для Windows) -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>com.akathist.maven.plugins.launch4j</groupId>
|
<groupId>com.akathist.maven.plugins.launch4j</groupId>
|
||||||
<artifactId>launch4j-maven-plugin</artifactId>
|
<artifactId>launch4j-maven-plugin</artifactId>
|
||||||
<version>2.5.0</version>
|
<version>2.5.0</version>
|
||||||
|
<configuration>
|
||||||
|
<skip>${skip.launch4j}</skip>
|
||||||
|
</configuration>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<id>l4j</id>
|
<id>l4j</id>
|
||||||
@@ -121,11 +210,16 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<outfile>../server/builds/ZernMCLauncher-${project.version}.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>gui</headerType>
|
||||||
<dontWrapJar>false</dontWrapJar>
|
<dontWrapJar>false</dontWrapJar>
|
||||||
<jre>
|
<jre>
|
||||||
<path>jre21</path>
|
<path>jre21</path>
|
||||||
<minVersion>21</minVersion>
|
<minVersion>21</minVersion>
|
||||||
|
<opts>
|
||||||
|
<opt>--module-path=lib-javafx</opt>
|
||||||
|
<opt>--add-modules=javafx.controls,javafx.web</opt>
|
||||||
|
<opt>--add-reads=javafx.graphics=ALL-UNNAMED</opt>
|
||||||
|
</opts>
|
||||||
</jre>
|
</jre>
|
||||||
<versionInfo>
|
<versionInfo>
|
||||||
<fileVersion>${project.version}.0</fileVersion>
|
<fileVersion>${project.version}.0</fileVersion>
|
||||||
@@ -161,11 +255,22 @@
|
|||||||
<fileset dir="${user.home}/launcher/jre/jre21"/>
|
<fileset dir="${user.home}/launcher/jre/jre21"/>
|
||||||
</copy>
|
</copy>
|
||||||
|
|
||||||
<!-- Создаём zip только с .exe и jre21 (без .jar и build.version) -->
|
<!-- Копируем JavaFX JAR в builds -->
|
||||||
<zip destfile="../server/builds/ZernMCLauncher-${project.version}.zip"
|
<copy todir="../server/builds/lib-javafx" overwrite="true">
|
||||||
|
<fileset dir="${project.build.directory}/lib-javafx"/>
|
||||||
|
</copy>
|
||||||
|
|
||||||
|
<!-- Копируем shell script для Linux -->
|
||||||
|
<copy file="${project.basedir}/src/main/resources/launcher.sh"
|
||||||
|
todir="../server/builds"
|
||||||
|
overwrite="true"/>
|
||||||
|
<chmod file="../server/builds/launcher.sh" perm="+x"/>
|
||||||
|
|
||||||
|
<!-- Создаём zip с .exe, jre21, lib-javafx и launcher.sh (без .jar и build.version) -->
|
||||||
|
<zip destfile="../server/builds/ZernMCLauncher-${project.version}-${os.suffix}.zip"
|
||||||
basedir="../server/builds"
|
basedir="../server/builds"
|
||||||
includes="ZernMCLauncher.exe,jre21/**"
|
includes="ZernMCLauncher.exe,ZernMCLauncher.jar,jre21/**,lib-javafx/**,launcher.sh"
|
||||||
excludes="*.jar,build.version"/>
|
excludes="build.version"/>
|
||||||
</target>
|
</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
@@ -197,5 +302,35 @@
|
|||||||
<server.url>http://87.120.187.36:1582</server.url>
|
<server.url>http://87.120.187.36:1582</server.url>
|
||||||
</properties>
|
</properties>
|
||||||
</profile>
|
</profile>
|
||||||
|
|
||||||
|
<!-- ==================== WINDOWS BUILD ==================== -->
|
||||||
|
<profile>
|
||||||
|
<id>win</id>
|
||||||
|
<activation>
|
||||||
|
<os>
|
||||||
|
<family>windows</family>
|
||||||
|
</os>
|
||||||
|
</activation>
|
||||||
|
<properties>
|
||||||
|
<javafx.classifier>win</javafx.classifier>
|
||||||
|
<os.suffix>win</os.suffix>
|
||||||
|
<skip.launch4j>false</skip.launch4j>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
|
||||||
|
<!-- ==================== LINUX BUILD ==================== -->
|
||||||
|
<profile>
|
||||||
|
<id>linux</id>
|
||||||
|
<activation>
|
||||||
|
<os>
|
||||||
|
<family>unix</family>
|
||||||
|
</os>
|
||||||
|
</activation>
|
||||||
|
<properties>
|
||||||
|
<javafx.classifier>linux</javafx.classifier>
|
||||||
|
<os.suffix>linux</os.suffix>
|
||||||
|
<skip.launch4j>true</skip.launch4j>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
</project>
|
</project>
|
||||||
@@ -34,6 +34,7 @@ public class Main {
|
|||||||
startWebUI(args);
|
startWebUI(args);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println(ZAnsi.red("UI не запустился: " + e.getMessage()));
|
System.err.println(ZAnsi.red("UI не запустился: " + e.getMessage()));
|
||||||
|
e.printStackTrace();
|
||||||
System.out.println(ZAnsi.yellow("Переключаюсь на режим TUI..."));
|
System.out.println(ZAnsi.yellow("Переключаюсь на режим TUI..."));
|
||||||
runTUI(args);
|
runTUI(args);
|
||||||
}
|
}
|
||||||
@@ -70,14 +71,21 @@ public class Main {
|
|||||||
// Даем серверу время запуститься
|
// Даем серверу время запуститься
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
|
|
||||||
// Проверяем headless перед запуском JavaFX
|
// Проверяем headless перед запуском JavaFX (только для не-Windows систем)
|
||||||
if (java.awt.GraphicsEnvironment.isHeadless()) {
|
if (!System.getProperty("os.name").toLowerCase().contains("win")) {
|
||||||
System.out.println(ZAnsi.yellow("Дисплей недоступен, переключаюсь на TUI..."));
|
boolean isHeadless = java.awt.GraphicsEnvironment.isHeadless();
|
||||||
WebServer.stop();
|
String display = System.getenv("DISPLAY");
|
||||||
runTUI(args);
|
if (isHeadless && (display == null || display.isEmpty())) {
|
||||||
return;
|
System.out.println(ZAnsi.yellow("Дисплей недоступен, переключаюсь на TUI..."));
|
||||||
|
WebServer.stop();
|
||||||
|
runTUI(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Проверка обновлений лаунчера
|
||||||
|
checkAndAutoUpdateLauncher();
|
||||||
|
|
||||||
// Запускаем JavaFX окно
|
// Запускаем JavaFX окно
|
||||||
UIWindow.start(port);
|
UIWindow.start(port);
|
||||||
}
|
}
|
||||||
@@ -178,12 +186,20 @@ public class Main {
|
|||||||
try {
|
try {
|
||||||
String javaPath = System.getProperty("java.home") + "/bin/java";
|
String javaPath = System.getProperty("java.home") + "/bin/java";
|
||||||
String jarPath = getCurrentJarPath().toAbsolutePath().toString();
|
String jarPath = getCurrentJarPath().toAbsolutePath().toString();
|
||||||
|
String launcherDir = jarPath.substring(0, jarPath.lastIndexOf(java.io.File.separator));
|
||||||
|
String javafxPath = launcherDir + java.io.File.separator + "lib-javafx";
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("Перезапуск лаунчера с новой версией..."));
|
System.out.println(ZAnsi.brightGreen("Перезапуск лаунчера с новой версией..."));
|
||||||
|
|
||||||
new ProcessBuilder(javaPath, "-jar", jarPath)
|
ProcessBuilder pb = new ProcessBuilder(
|
||||||
.inheritIO()
|
javaPath,
|
||||||
.start();
|
"--module-path=" + javafxPath,
|
||||||
|
"--add-modules=javafx.controls,javafx.web",
|
||||||
|
"--add-reads=javafx.graphics=ALL-UNNAMED",
|
||||||
|
"-jar", jarPath
|
||||||
|
);
|
||||||
|
pb.inheritIO();
|
||||||
|
pb.start();
|
||||||
|
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
+12
-2
@@ -13,6 +13,12 @@ import java.util.Map;
|
|||||||
|
|
||||||
public class InstallService {
|
public class InstallService {
|
||||||
|
|
||||||
|
private PackDownloader.ProgressCallback progressCallback;
|
||||||
|
|
||||||
|
public void setProgressCallback(PackDownloader.ProgressCallback callback) {
|
||||||
|
this.progressCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
public ApiResponse<InstallResult> installZernMCPack(String packName, String instanceName) {
|
public ApiResponse<InstallResult> installZernMCPack(String packName, String instanceName) {
|
||||||
try {
|
try {
|
||||||
boolean created = InstanceManager.createInstanceFolder(instanceName);
|
boolean created = InstanceManager.createInstanceFolder(instanceName);
|
||||||
@@ -26,6 +32,9 @@ public class InstallService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PackDownloader downloader = new PackDownloader(instance);
|
PackDownloader downloader = new PackDownloader(instance);
|
||||||
|
if (progressCallback != null) {
|
||||||
|
downloader.setProgressCallback(progressCallback);
|
||||||
|
}
|
||||||
|
|
||||||
// Получаем список доступных сборок
|
// Получаем список доступных сборок
|
||||||
List<ServerPack> availablePacks = downloader.getAvailablePacks();
|
List<ServerPack> availablePacks = downloader.getAvailablePacks();
|
||||||
@@ -65,13 +74,14 @@ public class InstallService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PackDownloader downloader = new PackDownloader(instance);
|
PackDownloader downloader = new PackDownloader(instance);
|
||||||
boolean hasUpdate = downloader.checkForUpdates(instance.getServerPackName());
|
int serverVersion = downloader.checkForUpdates(instance.getServerPackName());
|
||||||
|
boolean hasUpdate = serverVersion > 0;
|
||||||
|
|
||||||
return ApiResponse.success(new UpdateCheckResult(
|
return ApiResponse.success(new UpdateCheckResult(
|
||||||
hasUpdate,
|
hasUpdate,
|
||||||
true,
|
true,
|
||||||
instance.getServerVersion(),
|
instance.getServerVersion(),
|
||||||
hasUpdate ? instance.getServerVersion() + 1 : instance.getServerVersion()
|
serverVersion
|
||||||
));
|
));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return ApiResponse.error("Ошибка проверки обновлений: " + e.getMessage());
|
return ApiResponse.error("Ошибка проверки обновлений: " + e.getMessage());
|
||||||
|
|||||||
+11
-3
@@ -68,12 +68,14 @@ public class InstanceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private InstanceInfo toInstanceInfo(Instance instance) {
|
private InstanceInfo toInstanceInfo(Instance instance) {
|
||||||
return new InstanceInfo(
|
return new InstanceInfo(
|
||||||
instance.getName(),
|
instance.getName(),
|
||||||
instance.getPath().toString(),
|
instance.getPath().toString(),
|
||||||
instance.getMinecraftVersion(),
|
instance.getMinecraftVersion(),
|
||||||
instance.getLoaderType()
|
instance.getLoaderType(),
|
||||||
|
instance.isServerPack(),
|
||||||
|
instance.getServerPackName()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,17 +84,23 @@ public class InstanceService {
|
|||||||
private String path;
|
private String path;
|
||||||
private String version;
|
private String version;
|
||||||
private String loaderType;
|
private String loaderType;
|
||||||
|
private boolean isServerPack;
|
||||||
|
private String serverPackName;
|
||||||
|
|
||||||
public InstanceInfo(String name, String path, String version, String loaderType) {
|
public InstanceInfo(String name, String path, String version, String loaderType, boolean isServerPack, String serverPackName) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.version = version;
|
this.version = version;
|
||||||
this.loaderType = loaderType;
|
this.loaderType = loaderType;
|
||||||
|
this.isServerPack = isServerPack;
|
||||||
|
this.serverPackName = serverPackName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() { return name; }
|
public String getName() { return name; }
|
||||||
public String getPath() { return path; }
|
public String getPath() { return path; }
|
||||||
public String getVersion() { return version; }
|
public String getVersion() { return version; }
|
||||||
public String getLoaderType() { return loaderType; }
|
public String getLoaderType() { return loaderType; }
|
||||||
|
public boolean isServerPack() { return isServerPack; }
|
||||||
|
public String getServerPackName() { return serverPackName; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,17 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
public class LaunchMenu {
|
public class LaunchMenu {
|
||||||
|
|
||||||
|
public static class ExitToMainMenuException extends Exception {}
|
||||||
|
|
||||||
public void show() throws Exception {
|
public void show() throws Exception {
|
||||||
if (Config.isZernMCBuild()) {
|
try {
|
||||||
showZernMCOnly();
|
if (Config.isZernMCBuild()) {
|
||||||
} else {
|
showZernMCOnly();
|
||||||
showGlobal();
|
} else {
|
||||||
|
showGlobal();
|
||||||
|
}
|
||||||
|
} catch (ExitToMainMenuException e) {
|
||||||
|
// Возвращаемся в главное меню - ничего не делаем, просто выходим
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +288,15 @@ public class LaunchMenu {
|
|||||||
// ====================== manageInstance — полностью восстановлен ======================
|
// ====================== manageInstance — полностью восстановлен ======================
|
||||||
private void manageInstance(Instance instance) throws Exception {
|
private void manageInstance(Instance instance) throws Exception {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
// Проверяем, существует ли сборка (на случай если она была удалена вручную)
|
||||||
|
Instance currentInstance = InstanceManager.getInstance(instance.getName());
|
||||||
|
if (currentInstance == null) {
|
||||||
|
System.out.println(ZAnsi.yellow("Сборка была удалена или не существует."));
|
||||||
|
ConsoleUtils.pause();
|
||||||
|
throw new ExitToMainMenuException(); // Выходим в главное меню
|
||||||
|
}
|
||||||
|
instance = currentInstance; // Обновляем ссылку на актуальный объект
|
||||||
|
|
||||||
ConsoleUtils.clearScreen();
|
ConsoleUtils.clearScreen();
|
||||||
System.out.println(ZAnsi.header("Управление сборкой: " + instance.getName()));
|
System.out.println(ZAnsi.header("Управление сборкой: " + instance.getName()));
|
||||||
System.out.println(ZAnsi.white("Версия: " + instance.getMinecraftVersion()));
|
System.out.println(ZAnsi.white("Версия: " + instance.getMinecraftVersion()));
|
||||||
@@ -320,9 +335,13 @@ public class LaunchMenu {
|
|||||||
changeLoaderVersion(instance);
|
changeLoaderVersion(instance);
|
||||||
} else {
|
} else {
|
||||||
deleteInstance(instance);
|
deleteInstance(instance);
|
||||||
|
throw new ExitToMainMenuException(); // Выходим в главное меню
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 3 -> deleteInstance(instance);
|
case 3 -> {
|
||||||
|
deleteInstance(instance);
|
||||||
|
throw new ExitToMainMenuException(); // Выходим в главное меню после удаления
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,7 +351,8 @@ public class LaunchMenu {
|
|||||||
System.out.println(ZAnsi.cyan("Проверка обновлений для " + instance.getName()));
|
System.out.println(ZAnsi.cyan("Проверка обновлений для " + instance.getName()));
|
||||||
|
|
||||||
PackDownloader downloader = new PackDownloader(instance);
|
PackDownloader downloader = new PackDownloader(instance);
|
||||||
boolean hasUpdate = downloader.checkForUpdates(instance.getServerPackName());
|
int serverVersion = downloader.checkForUpdates(instance.getServerPackName());
|
||||||
|
boolean hasUpdate = serverVersion > 0;
|
||||||
|
|
||||||
if (!hasUpdate) {
|
if (!hasUpdate) {
|
||||||
System.out.println(ZAnsi.green("Сборка актуальна (v" + instance.getServerVersion() + ")"));
|
System.out.println(ZAnsi.green("Сборка актуальна (v" + instance.getServerVersion() + ")"));
|
||||||
@@ -423,14 +443,15 @@ public class LaunchMenu {
|
|||||||
boolean deleted = InstanceManager.deleteInstance(instance.getName());
|
boolean deleted = InstanceManager.deleteInstance(instance.getName());
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
System.out.println(ZAnsi.brightGreen("Сборка '" + instance.getName() + "' успешно удалена."));
|
System.out.println(ZAnsi.brightGreen("Сборка '" + instance.getName() + "' успешно удалена."));
|
||||||
|
// НЕ делаем pause(), сразу возвращаемся в manageInstance для выхода в меню сборок
|
||||||
} else {
|
} else {
|
||||||
System.out.println(ZAnsi.brightRed("Не удалось удалить сборку."));
|
System.out.println(ZAnsi.brightRed("Не удалось удалить сборку."));
|
||||||
|
ConsoleUtils.pause();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
System.out.println(ZAnsi.yellow("Удаление отменено."));
|
System.out.println(ZAnsi.yellow("Удаление отменено."));
|
||||||
|
ConsoleUtils.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
ConsoleUtils.pause();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void launchExistingInstance(Instance instance) {
|
private void launchExistingInstance(Instance instance) {
|
||||||
@@ -625,9 +646,8 @@ public class LaunchMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isNeoForgeSupported(String version) {
|
private boolean isNeoForgeSupported(String version) {
|
||||||
return version.matches("^1\\.20\\.[1-9].*") ||
|
// ВРЕМЕННО ОТКЛЮЧЕНО: в разработке
|
||||||
version.matches("^1\\.21.*") ||
|
return false;
|
||||||
version.matches("^\\d{2}\\..*");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String askFabricLoaderVersion() throws Exception {
|
private String askFabricLoaderVersion() throws Exception {
|
||||||
|
|||||||
@@ -64,9 +64,10 @@ public class UpdateMenu {
|
|||||||
|
|
||||||
for (Instance instance : serverInstances) {
|
for (Instance instance : serverInstances) {
|
||||||
PackDownloader downloader = new PackDownloader(instance);
|
PackDownloader downloader = new PackDownloader(instance);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
boolean hasUpdate = downloader.checkForUpdates(instance.getServerPackName());
|
int serverVersion = downloader.checkForUpdates(instance.getServerPackName());
|
||||||
|
boolean hasUpdate = serverVersion > 0;
|
||||||
if (hasUpdate) {
|
if (hasUpdate) {
|
||||||
System.out.println(ZAnsi.yellow(instance.getName() + " - Есть обновление!"));
|
System.out.println(ZAnsi.yellow(instance.getName() + " - Есть обновление!"));
|
||||||
updatableInstances.add(instance);
|
updatableInstances.add(instance);
|
||||||
|
|||||||
+101
-37
@@ -29,12 +29,55 @@ public class PackDownloader {
|
|||||||
private final Instance instance;
|
private final Instance instance;
|
||||||
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
|
||||||
private final HttpClient httpClient = HttpClient.newHttpClient();
|
private final HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
//private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
private ProgressCallback progressCallback;
|
||||||
|
|
||||||
|
public interface ProgressCallback {
|
||||||
|
void onProgress(ProgressInfo info);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ProgressInfo {
|
||||||
|
private String phase;
|
||||||
|
private int totalFiles;
|
||||||
|
private int downloadedFiles;
|
||||||
|
private String currentFile;
|
||||||
|
private long fileSize;
|
||||||
|
private long downloadedBytes;
|
||||||
|
private int filePercent;
|
||||||
|
private int totalPercent;
|
||||||
|
private String eta;
|
||||||
|
|
||||||
|
public ProgressInfo(String phase, int totalFiles, int downloadedFiles, String currentFile,
|
||||||
|
long fileSize, long downloadedBytes, int filePercent, int totalPercent, String eta) {
|
||||||
|
this.phase = phase;
|
||||||
|
this.totalFiles = totalFiles;
|
||||||
|
this.downloadedFiles = downloadedFiles;
|
||||||
|
this.currentFile = currentFile;
|
||||||
|
this.fileSize = fileSize;
|
||||||
|
this.downloadedBytes = downloadedBytes;
|
||||||
|
this.filePercent = filePercent;
|
||||||
|
this.totalPercent = totalPercent;
|
||||||
|
this.eta = eta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhase() { return phase; }
|
||||||
|
public int getTotalFiles() { return totalFiles; }
|
||||||
|
public int getDownloadedFiles() { return downloadedFiles; }
|
||||||
|
public String getCurrentFile() { return currentFile; }
|
||||||
|
public long getFileSize() { return fileSize; }
|
||||||
|
public long getDownloadedBytes() { return downloadedBytes; }
|
||||||
|
public int getFilePercent() { return filePercent; }
|
||||||
|
public int getTotalPercent() { return totalPercent; }
|
||||||
|
public String getEta() { return eta; }
|
||||||
|
}
|
||||||
|
|
||||||
public PackDownloader(Instance instance) {
|
public PackDownloader(Instance instance) {
|
||||||
this.instance = instance;
|
this.instance = instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setProgressCallback(ProgressCallback callback) {
|
||||||
|
this.progressCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить список доступных паков с сервера
|
* Получить список доступных паков с сервера
|
||||||
*/
|
*/
|
||||||
@@ -225,15 +268,16 @@ public class PackDownloader {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверить наличие обновлений для серверной сборки
|
* Проверить наличие обновлений для серверной сборки
|
||||||
|
* @return версия на сервере, или 0 если нет обновлений
|
||||||
*/
|
*/
|
||||||
public boolean checkForUpdates(String packName) throws Exception {
|
public int checkForUpdates(String packName) throws Exception {
|
||||||
if (!instance.isServerPack()) return false;
|
if (!instance.isServerPack()) return 0;
|
||||||
|
|
||||||
PackManifest manifest = getPackManifest(packName);
|
PackManifest manifest = getPackManifest(packName);
|
||||||
int serverVersion = manifest.getVersion();
|
int serverVersion = manifest.getVersion();
|
||||||
int localVersion = instance.getServerVersion();
|
int localVersion = instance.getServerVersion();
|
||||||
|
|
||||||
return serverVersion > localVersion;
|
return serverVersion > localVersion ? serverVersion : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -397,16 +441,18 @@ public class PackDownloader {
|
|||||||
System.out.println(ZAnsi.cyan("\nПрименение изменений:"));
|
System.out.println(ZAnsi.cyan("\nПрименение изменений:"));
|
||||||
System.out.println(" Загрузить: " + diff.getToDownload().size() + " файлов");
|
System.out.println(" Загрузить: " + diff.getToDownload().size() + " файлов");
|
||||||
System.out.println(" Удалить: " + diff.getToDelete().size() + " файлов");
|
System.out.println(" Удалить: " + diff.getToDelete().size() + " файлов");
|
||||||
|
|
||||||
// Создаем директории если нужно
|
if (progressCallback != null) {
|
||||||
|
progressCallback.onProgress(new ProgressInfo("starting", diff.getToDownload().size(), 0, "", 0, 0, 0, 0, ""));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(instance.getPath());
|
Files.createDirectories(instance.getPath());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.err.println(ZAnsi.red("Ошибка создания директорий: " + e.getMessage()));
|
System.err.println(ZAnsi.red("Ошибка создания директорий: " + e.getMessage()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем файлы
|
|
||||||
for (String filePath : diff.getToDelete()) {
|
for (String filePath : diff.getToDelete()) {
|
||||||
Path fullPath = instance.getPath().resolve(filePath);
|
Path fullPath = instance.getPath().resolve(filePath);
|
||||||
try {
|
try {
|
||||||
@@ -417,85 +463,103 @@ public class PackDownloader {
|
|||||||
System.err.println(ZAnsi.red(" Ошибка удаления " + filePath + ": " + e.getMessage()));
|
System.err.println(ZAnsi.red(" Ошибка удаления " + filePath + ": " + e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скачиваем файлы
|
|
||||||
AtomicInteger downloaded = new AtomicInteger(0);
|
AtomicInteger downloaded = new AtomicInteger(0);
|
||||||
int total = diff.getToDownload().size();
|
int total = diff.getToDownload().size();
|
||||||
|
|
||||||
for (FileInfo file : diff.getToDownload()) {
|
for (FileInfo file : diff.getToDownload()) {
|
||||||
String path = file.getPath();
|
String path = file.getPath();
|
||||||
Path fullPath = instance.getPath().resolve(path);
|
Path fullPath = instance.getPath().resolve(path);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Создаем директории
|
|
||||||
Files.createDirectories(fullPath.getParent());
|
Files.createDirectories(fullPath.getParent());
|
||||||
|
|
||||||
// Скачиваем файл
|
downloadFile(file, fullPath, progressCallback, downloaded.get(), total);
|
||||||
downloadFile(file, fullPath);
|
|
||||||
|
|
||||||
// Проверяем хеш
|
|
||||||
String actualHash = calculateHash(fullPath);
|
String actualHash = calculateHash(fullPath);
|
||||||
if (!actualHash.equals(file.getHash())) {
|
if (!actualHash.equals(file.getHash())) {
|
||||||
throw new IOException("Хеш не совпадает! Ожидался: " + file.getHash() +
|
throw new IOException("Хеш не совпадает! Ожидался: " + file.getHash() +
|
||||||
", получен: " + actualHash);
|
", получен: " + actualHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
downloaded.incrementAndGet();
|
downloaded.incrementAndGet();
|
||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
ProgressBar.show("Скачивание", downloaded.get(), total, "файлов");
|
ProgressBar.show("Скачивание", downloaded.get(), total, "файлов");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progressCallback != null) {
|
||||||
|
progressCallback.onProgress(new ProgressInfo("downloading", total, downloaded.get(), path,
|
||||||
|
file.getSize(), file.getSize(), 100, (downloaded.get() * 100) / total, ""));
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("\n" + ZAnsi.red(" Ошибка скачивания " + path + ": " + e.getMessage()));
|
System.err.println("\n" + ZAnsi.red(" Ошибка скачивания " + path + ": " + e.getMessage()));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (total > 0) {
|
if (total > 0) {
|
||||||
ProgressBar.finish("Скачивание");
|
ProgressBar.finish("Скачивание");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progressCallback != null) {
|
||||||
|
progressCallback.onProgress(new ProgressInfo("complete", total, total, "", 0, 0, 100, 100, ""));
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Скачать один файл с сервера
|
* Скачать один файл с сервера
|
||||||
*/
|
*/
|
||||||
private void downloadFile(FileInfo file, Path destination) throws Exception {
|
private void downloadFile(FileInfo file, Path destination) throws Exception {
|
||||||
|
downloadFile(file, destination, null, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void downloadFile(FileInfo file, Path destination, ProgressCallback callback, int downloadedFiles, int totalFiles) throws Exception {
|
||||||
String url = ZHttpClient.getBaseUrl() + file.getUrl();
|
String url = ZHttpClient.getBaseUrl() + file.getUrl();
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
.uri(java.net.URI.create(url))
|
.uri(java.net.URI.create(url))
|
||||||
.GET()
|
.GET()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<InputStream> response = httpClient.send(request,
|
HttpResponse<InputStream> response = httpClient.send(request,
|
||||||
HttpResponse.BodyHandlers.ofInputStream());
|
HttpResponse.BodyHandlers.ofInputStream());
|
||||||
|
|
||||||
if (response.statusCode() != 200) {
|
if (response.statusCode() != 200) {
|
||||||
throw new IOException("HTTP " + response.statusCode());
|
throw new IOException("HTTP " + response.statusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Скачиваем с прогрессом
|
|
||||||
try (InputStream in = response.body();
|
try (InputStream in = response.body();
|
||||||
FileOutputStream out = new FileOutputStream(destination.toFile())) {
|
FileOutputStream out = new FileOutputStream(destination.toFile())) {
|
||||||
|
|
||||||
byte[] buffer = new byte[8192];
|
byte[] buffer = new byte[8192];
|
||||||
int bytesRead;
|
int bytesRead;
|
||||||
long totalRead = 0;
|
long totalRead = 0;
|
||||||
long fileSize = file.getSize();
|
long fileSize = file.getSize();
|
||||||
|
long lastCallbackTime = 0;
|
||||||
|
|
||||||
while ((bytesRead = in.read(buffer)) != -1) {
|
while ((bytesRead = in.read(buffer)) != -1) {
|
||||||
out.write(buffer, 0, bytesRead);
|
out.write(buffer, 0, bytesRead);
|
||||||
totalRead += bytesRead;
|
totalRead += bytesRead;
|
||||||
|
|
||||||
if (fileSize > 0 && totalRead % 8192 == 0) {
|
if (fileSize > 0) {
|
||||||
ProgressBar.showDownload(" " + file.getPath(), totalRead, fileSize);
|
ProgressBar.showDownload(" " + file.getPath(), totalRead, fileSize);
|
||||||
|
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (callback != null && now - lastCallbackTime > 200) {
|
||||||
|
int filePercent = (int) ((totalRead * 100) / fileSize);
|
||||||
|
int totalPercent = totalFiles > 0 ? ((downloadedFiles * 100 + filePercent) / totalFiles) : 0;
|
||||||
|
callback.onProgress(new ProgressInfo("downloading", totalFiles, downloadedFiles, file.getPath(),
|
||||||
|
fileSize, totalRead, filePercent, totalPercent, ""));
|
||||||
|
lastCallbackTime = now;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProgressBar.clearLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgressBar.clearLine();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+113
-127
@@ -39,45 +39,41 @@ public class NeoForgeInstaller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
instance.setAssetIndex(assetIndex);
|
instance.setAssetIndex(assetIndex);
|
||||||
createLauncherProfile();
|
|
||||||
|
|
||||||
String mavenGroup = getMavenGroup(mcVersion);
|
String mavenGroup = getMavenGroup(mcVersion);
|
||||||
String mavenArtifact = getMavenArtifact(mcVersion);
|
String mavenArtifact = getMavenArtifact(mcVersion);
|
||||||
|
|
||||||
String installerUrl = "https://maven.neoforged.net/releases/"
|
// Формируем путь к версии
|
||||||
|
String versionName = mcVersion + "-" + neoForgeVersion;
|
||||||
|
Path versionDir = instance.getPath().resolve("versions").resolve(versionName);
|
||||||
|
Files.createDirectories(versionDir);
|
||||||
|
|
||||||
|
// Скачиваем universal.jar (это основной JAR NeoForge)
|
||||||
|
String baseMavenUrl = "https://maven.neoforged.net/releases/"
|
||||||
+ mavenGroup.replace('.', '/') + "/"
|
+ mavenGroup.replace('.', '/') + "/"
|
||||||
+ mavenArtifact + "/"
|
+ mavenArtifact + "/"
|
||||||
+ neoForgeVersion
|
+ neoForgeVersion + "/";
|
||||||
+ "/" + mavenArtifact + "-" + neoForgeVersion + "-installer.jar";
|
|
||||||
|
|
||||||
Path installerJar = instance.getPath().resolve("neoforge-installer.jar");
|
String universalJarUrl = baseMavenUrl + mavenArtifact + "-" + neoForgeVersion + "-universal.jar";
|
||||||
|
Path neoForgeJar = versionDir.resolve(versionName + ".jar");
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Скачивание NeoForge Installer..."));
|
System.out.println(ZAnsi.cyan("Скачивание NeoForge universal.jar..."));
|
||||||
downloadFileWithProgress(installerUrl, installerJar);
|
downloadFileDirect(universalJarUrl, neoForgeJar);
|
||||||
|
|
||||||
System.out.println(ZAnsi.cyan("Запуск NeoForge Installer..."));
|
// Создаем version.json вручную
|
||||||
System.out.println(ZAnsi.yellow("Это может занять несколько минут. Пожалуйста, подождите...\n"));
|
System.out.println(ZAnsi.cyan("Создание version.json..."));
|
||||||
|
createVersionJson(versionDir.resolve(versionName + ".json"), mcVersion, neoForgeVersion, mavenArtifact);
|
||||||
|
|
||||||
boolean success = runNeoForgeInstaller(installerJar);
|
// Скачиваем необходимые библиотеки
|
||||||
|
System.out.println(ZAnsi.cyan("Скачивание библиотек NeoForge..."));
|
||||||
|
downloadNeoForgeLibraries(mcVersion, neoForgeVersion, mavenGroup, mavenArtifact);
|
||||||
|
|
||||||
if (success) {
|
System.out.println(ZAnsi.brightGreen("\nNeoForge " + neoForgeVersion + " успешно установлен!"));
|
||||||
try {
|
instance.setMinecraftVersion(mcVersion);
|
||||||
downloadMissingLibraries(mcVersion, neoForgeVersion, mavenGroup, mavenArtifact);
|
instance.setLoaderType("neoforge");
|
||||||
} catch (Exception e) {
|
instance.setLoaderVersion(neoForgeVersion);
|
||||||
System.out.println(ZAnsi.yellow("Предупреждение: не удалось докачать некоторые библиотеки: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
|
|
||||||
System.out.println(ZAnsi.brightGreen("\nNeoForge " + neoForgeVersion + " успешно установлен!"));
|
return true;
|
||||||
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) {
|
private String getMavenGroup(String mcVersion) {
|
||||||
@@ -153,119 +149,109 @@ public class NeoForgeInstaller {
|
|||||||
ProgressBar.finish("NeoForge Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")");
|
ProgressBar.finish("NeoForge Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean runNeoForgeInstaller(Path installerJar) throws IOException, InterruptedException {
|
private void downloadFileDirect(String url, Path target) throws Exception {
|
||||||
int maxRetries = 3;
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
int attempt = 1;
|
.uri(URI.create(url))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
while (attempt <= maxRetries) {
|
HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target));
|
||||||
System.out.println(ZAnsi.cyan("Попытка " + attempt + " из " + maxRetries));
|
|
||||||
|
|
||||||
ProcessBuilder pb = new ProcessBuilder(
|
if (response.statusCode() != 200) {
|
||||||
"java",
|
throw new IOException("HTTP " + response.statusCode() + " for " + url);
|
||||||
"-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;
|
System.out.println(ZAnsi.green(" " + target.getFileName() + " завершено ✓"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void downloadMissingLibraries(String mcVersion, String neoForgeVersion, String mavenGroup, String mavenArtifact) throws Exception {
|
private void createVersionJson(Path jsonFile, String mcVersion, String neoForgeVersion, String mavenArtifact) throws IOException {
|
||||||
System.out.println(ZAnsi.cyan("Проверка и докачка отсутствующих библиотек..."));
|
// Создаем минимальный version.json для NeoForge
|
||||||
|
String versionName = mcVersion + "-" + neoForgeVersion;
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"id": "%s",
|
||||||
|
"type": "release",
|
||||||
|
"mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher",
|
||||||
|
"inheritsFrom": "%s",
|
||||||
|
"arguments": {
|
||||||
|
"--tweakClass": "cpw.mods.fml.relauncher.CoreModManager"
|
||||||
|
},
|
||||||
|
"libraries": [
|
||||||
|
{"name": "net.neoforged:neoforge:%s"},
|
||||||
|
{"name": "cpw.mods:bootstraplauncher:1.1.2"},
|
||||||
|
{"name": "net.minecraftforge:unsafe:0.2.0"},
|
||||||
|
{"name": "net.minecraftforge:srgutils:0.4.4"},
|
||||||
|
{"name": "net.minecraftforge:modlauncher:10.2.1"},
|
||||||
|
{"name": "net.minecraftforge:coremods:5.0.1"},
|
||||||
|
{"name": "net.minecraftforge:accesstransformers:8.8"},
|
||||||
|
{"name": "net.minecraftforge:eventbus:6.0.5"},
|
||||||
|
{"name": "net.minecraftforge:forgemin:0.1.1"},
|
||||||
|
{"name": "net.minecraftforge:scanner:1.2.2"},
|
||||||
|
{"name": "com.google.code.gson:gson:2.10.1"},
|
||||||
|
{"name": "com.google.guava:guava:32.1.3-jre"},
|
||||||
|
{"name": "org.apache.commons:commons-lang3:3.13.0"},
|
||||||
|
{"name": "org.jline:jline-reader:3.12.1"},
|
||||||
|
{"name": "org.jline:jline-terminal:3.12.1"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".formatted(versionName, mcVersion, neoForgeVersion);
|
||||||
|
|
||||||
Map<String, String> alternativeUrls = new HashMap<>();
|
Files.writeString(jsonFile, json);
|
||||||
alternativeUrls.put("org/ow2/asm/asm/9.6/asm-9.6.jar",
|
System.out.println(ZAnsi.green(" version.json создан ✓"));
|
||||||
"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");
|
private void downloadNeoForgeLibraries(String mcVersion, String neoForgeVersion, String mavenGroup, String mavenArtifact) throws Exception {
|
||||||
alternativeUrls.put("org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar",
|
System.out.println(ZAnsi.cyan("Скачивание библиотек NeoForge..."));
|
||||||
"https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar");
|
|
||||||
|
String baseMavenUrl = "https://maven.neoforged.net/releases/"
|
||||||
|
+ mavenGroup.replace('.', '/') + "/";
|
||||||
|
|
||||||
Path librariesDir = instance.getPath().resolve("libraries");
|
Path librariesDir = instance.getPath().resolve("libraries");
|
||||||
|
|
||||||
for (Map.Entry<String, String> entry : alternativeUrls.entrySet()) {
|
// Список основных библиотек NeoForge
|
||||||
Path target = librariesDir.resolve(entry.getKey());
|
String[][] libs = {
|
||||||
if (!Files.exists(target)) {
|
{mavenGroup, mavenArtifact, neoForgeVersion},
|
||||||
Files.createDirectories(target.getParent());
|
{"cpw.mods", "bootstraplauncher", "1.1.2"},
|
||||||
System.out.println(ZAnsi.yellow("Докачка: " + target.getFileName()));
|
{"net.minecraftforge", "unsafe", "0.2.0"},
|
||||||
|
{"net.minecraftforge", "srgutils", "0.4.4"},
|
||||||
|
{"net.minecraftforge", "modlauncher", "10.2.1"},
|
||||||
|
{"net.minecraftforge", "coremods", "5.0.1"},
|
||||||
|
{"net.minecraftforge", "accesstransformers", "8.8"},
|
||||||
|
{"net.minecraftforge", "eventbus", "6.0.5"},
|
||||||
|
{"net.minecraftforge", "forgemin", "0.1.1"},
|
||||||
|
{"net.minecraftforge", "scanner", "1.2.2"}
|
||||||
|
};
|
||||||
|
|
||||||
for (int attempt = 1; attempt <= 3; attempt++) {
|
for (String[] lib : libs) {
|
||||||
try {
|
String group = lib[0].replace('.', '/');
|
||||||
downloadFileWithProgress(entry.getValue(), target);
|
String artifact = lib[1];
|
||||||
break;
|
String version = lib[2];
|
||||||
} catch (Exception e) {
|
|
||||||
if (attempt == 3) throw e;
|
String jarName = artifact + "-" + version + ".jar";
|
||||||
System.out.println(ZAnsi.yellow("Повторная попытка " + attempt + "/3..."));
|
String mavenPath = group + "/" + artifact + "/" + version + "/" + jarName;
|
||||||
Thread.sleep(2000);
|
Path target = librariesDir.resolve(mavenPath);
|
||||||
}
|
|
||||||
|
if (Files.exists(target)) {
|
||||||
|
System.out.println(ZAnsi.green(" " + jarName + " уже есть ✓"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.createDirectories(target.getParent());
|
||||||
|
|
||||||
|
String url = baseMavenUrl + mavenPath;
|
||||||
|
try {
|
||||||
|
downloadFileDirect(url, target);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Пробуем Maven Central как fallback
|
||||||
|
try {
|
||||||
|
String centralUrl = "https://repo1.maven.org/maven2/" + mavenPath;
|
||||||
|
downloadFileDirect(centralUrl, target);
|
||||||
|
} catch (Exception e2) {
|
||||||
|
System.out.println(ZAnsi.yellow(" Предупреждение: не удалось скачать " + jarName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
System.out.println(ZAnsi.green("Библиотеки NeoForge обработаны ✓"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,6 @@ public class UIWindow extends Application {
|
|||||||
private static int port;
|
private static int port;
|
||||||
|
|
||||||
public static void start(int port) {
|
public static void start(int port) {
|
||||||
// Backup проверка headless
|
|
||||||
if (java.awt.GraphicsEnvironment.isHeadless()) {
|
|
||||||
throw new RuntimeException("Headless environment - no display available");
|
|
||||||
}
|
|
||||||
|
|
||||||
UIWindow.port = port;
|
UIWindow.port = port;
|
||||||
UIWindow.url = "http://localhost:" + port;
|
UIWindow.url = "http://localhost:" + port;
|
||||||
Application.launch(UIWindow.class);
|
Application.launch(UIWindow.class);
|
||||||
|
|||||||
@@ -5,13 +5,21 @@ import io.javalin.http.staticfiles.Location;
|
|||||||
import me.sashegdev.zernmc.launcher.api.ApiResponse;
|
import me.sashegdev.zernmc.launcher.api.ApiResponse;
|
||||||
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
import me.sashegdev.zernmc.launcher.api.LauncherAPI;
|
||||||
import me.sashegdev.zernmc.launcher.api.instance.InstanceService;
|
import me.sashegdev.zernmc.launcher.api.instance.InstanceService;
|
||||||
|
import me.sashegdev.zernmc.launcher.api.install.InstallService;
|
||||||
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
import me.sashegdev.zernmc.launcher.auth.AuthManager;
|
||||||
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
|
||||||
|
import me.sashegdev.zernmc.launcher.utils.ZHttpClient;
|
||||||
|
|
||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
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.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -182,6 +190,48 @@ public class WebServer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// SSE прогресс установки
|
||||||
|
app.get("/api/instances/{name}/install/stream", ctx -> {
|
||||||
|
ctx.header("Content-Type", "text/event-stream");
|
||||||
|
ctx.header("Cache-Control", "no-cache");
|
||||||
|
ctx.header("Connection", "keep-alive");
|
||||||
|
|
||||||
|
String instanceName = ctx.pathParam("name");
|
||||||
|
var instanceInfo = api.instances().getInstance(instanceName);
|
||||||
|
|
||||||
|
if (!instanceInfo.isSuccess() || instanceInfo.getData() == null) {
|
||||||
|
ctx.result("data: {\"phase\":\"error\",\"message\":\"Instance not found\"}\n\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var os = ctx.outputStream();
|
||||||
|
InstallService service = new InstallService();
|
||||||
|
service.setProgressCallback(info -> {
|
||||||
|
try {
|
||||||
|
String json = String.format(
|
||||||
|
"{\"phase\":\"%s\",\"totalFiles\":%d,\"downloadedFiles\":%d,\"currentFile\":\"%s\",\"fileSize\":%d,\"downloadedBytes\":%d,\"filePercent\":%d,\"totalPercent\":%d,\"eta\":\"%s\"}",
|
||||||
|
info.getPhase(), info.getTotalFiles(), info.getDownloadedFiles(),
|
||||||
|
info.getCurrentFile() != null ? info.getCurrentFile().replace("\"", "\\\"") : "",
|
||||||
|
info.getFileSize(), info.getDownloadedBytes(),
|
||||||
|
info.getFilePercent(), info.getTotalPercent(),
|
||||||
|
info.getEta() != null ? info.getEta() : ""
|
||||||
|
);
|
||||||
|
os.write(("data: " + json + "\n\n").getBytes());
|
||||||
|
os.flush();
|
||||||
|
} catch (Exception e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = service.installZernMCPack(instanceInfo.getData().getServerPackName(), instanceName);
|
||||||
|
try {
|
||||||
|
if (!result.isSuccess()) {
|
||||||
|
os.write(("data: {\"phase\":\"error\",\"message\":\"" + result.getError().replace("\"", "\\\"") + "\"}\n\n").getBytes());
|
||||||
|
} else {
|
||||||
|
os.write("data: {\"phase\":\"complete\"}\n\n".getBytes());
|
||||||
|
}
|
||||||
|
os.flush();
|
||||||
|
} catch (Exception e) {}
|
||||||
|
});
|
||||||
|
|
||||||
// Проверка обновлений
|
// Проверка обновлений
|
||||||
app.get("/api/instances/{name}/updates", ctx -> {
|
app.get("/api/instances/{name}/updates", ctx -> {
|
||||||
String name = ctx.pathParam("name");
|
String name = ctx.pathParam("name");
|
||||||
@@ -326,4 +376,88 @@ public class WebServer {
|
|||||||
app.stop();
|
app.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== LAUNCHER AUTO-UPDATE ====================
|
||||||
|
public static void checkLauncherUpdate() {
|
||||||
|
try {
|
||||||
|
String json = ZHttpClient.getLauncherVersionInfo();
|
||||||
|
String serverVersion = extractVersion(json);
|
||||||
|
String currentVersion = me.sashegdev.zernmc.launcher.utils.Version.getCurrentVersion();
|
||||||
|
|
||||||
|
if (me.sashegdev.zernmc.launcher.utils.Version.isNewer(currentVersion, serverVersion)) {
|
||||||
|
System.out.println(ZAnsi.brightYellow("\nДоступна новая версия лаунчера! (" + serverVersion + ")"));
|
||||||
|
System.out.println(ZAnsi.cyan("Начинается автоматическое обновление...\n"));
|
||||||
|
performLauncherUpdate(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 performLauncherUpdate(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(me.sashegdev.zernmc.launcher.Main.class.getProtectionDomain()
|
||||||
|
.getCodeSource()
|
||||||
|
.getLocation()
|
||||||
|
.toURI());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Path.of("zernmc-launcher.jar");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
JAVA_HOME="$SCRIPT_DIR/jre21"
|
||||||
|
JAVA="$JAVA_HOME/bin/java"
|
||||||
|
|
||||||
|
JAVAFX_PATH="$SCRIPT_DIR/lib-javafx"
|
||||||
|
|
||||||
|
exec "$JAVA" \
|
||||||
|
--module-path="$JAVAFX_PATH" \
|
||||||
|
--add-modules=javafx.controls,javafx.web \
|
||||||
|
--add-reads=javafx.graphics=ALL-UNNAMED \
|
||||||
|
-jar "$SCRIPT_DIR/ZernMCLauncher.jar" "$@"
|
||||||
@@ -484,6 +484,39 @@ body {
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-update {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px 30px;
|
||||||
|
background: linear-gradient(135deg, var(--warning), #f59e0b);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: #1a1a24;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: var(--transition-normal);
|
||||||
|
box-shadow: 0 4px 20px rgba(251, 191, 36, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update:hover {
|
||||||
|
transform: translateY(-4px) scale(1.02);
|
||||||
|
box-shadow: 0 8px 40px rgba(251, 191, 36, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-update:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== MODAL ==================== */
|
/* ==================== MODAL ==================== */
|
||||||
.modal {
|
.modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -653,6 +686,30 @@ body {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-file {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill.animated {
|
||||||
|
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary), var(--accent-primary));
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: progressShimmer 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progressShimmer {
|
||||||
|
0% { background-position: 200% 0; }
|
||||||
|
100% { background-position: -200% 0; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== LOADING ==================== */
|
/* ==================== LOADING ==================== */
|
||||||
.loading-overlay {
|
.loading-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ class App {
|
|||||||
this.instances = [];
|
this.instances = [];
|
||||||
this.zernmcPacks = [];
|
this.zernmcPacks = [];
|
||||||
this.mcVersions = [];
|
this.mcVersions = [];
|
||||||
|
this.hasUpdate = false;
|
||||||
|
this.hasMismatches = false;
|
||||||
|
this.isServerPack = false;
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +213,32 @@ class App {
|
|||||||
if (result.success && result.data && result.data.length > 0) {
|
if (result.success && result.data && result.data.length > 0) {
|
||||||
this.currentInstance = result.data[0];
|
this.currentInstance = result.data[0];
|
||||||
this.renderCurrentInstance(this.currentInstance);
|
this.renderCurrentInstance(this.currentInstance);
|
||||||
this.enablePlayButton(true);
|
|
||||||
|
this.isServerPack = this.currentInstance.isServerPack || false;
|
||||||
|
|
||||||
|
if (this.isServerPack) {
|
||||||
|
this.addLog('Проверка целостности файлов...', 'info');
|
||||||
|
|
||||||
|
const verifyResult = await this.request(`/instances/${this.currentInstance.name}/verify`);
|
||||||
|
if (verifyResult.success && verifyResult.data) {
|
||||||
|
this.hasMismatches = verifyResult.data.hasMismatches;
|
||||||
|
if (this.hasMismatches) {
|
||||||
|
this.addLog('Обнаружены изменённые файлы!', 'warning');
|
||||||
|
} else {
|
||||||
|
this.addLog('Файлы целы', 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateResult = await this.request(`/instances/${this.currentInstance.name}/updates`);
|
||||||
|
if (updateResult.success && updateResult.data) {
|
||||||
|
this.hasUpdate = updateResult.data.hasUpdate;
|
||||||
|
if (this.hasUpdate) {
|
||||||
|
this.addLog('Доступно обновление: v' + updateResult.data.currentVersion + ' → v' + updateResult.data.latestVersion, 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatePlayButton();
|
||||||
this.addLog('Сборка загружена: ' + this.currentInstance.name, 'success');
|
this.addLog('Сборка загружена: ' + this.currentInstance.name, 'success');
|
||||||
} else {
|
} else {
|
||||||
this.renderNoInstance();
|
this.renderNoInstance();
|
||||||
@@ -219,6 +247,26 @@ class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatePlayButton() {
|
||||||
|
const btn = document.getElementById('play-btn');
|
||||||
|
if (!this.currentInstance) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.className = 'btn-play';
|
||||||
|
btn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>ИГРАТЬ';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasUpdate || this.hasMismatches) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.className = 'btn-update';
|
||||||
|
btn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>ОБНОВИТЬ';
|
||||||
|
} else {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.className = 'btn-play';
|
||||||
|
btn.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>ИГРАТЬ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderCurrentInstance(instance) {
|
renderCurrentInstance(instance) {
|
||||||
const container = document.getElementById('current-instance');
|
const container = document.getElementById('current-instance');
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
@@ -247,6 +295,11 @@ class App {
|
|||||||
async launchInstance() {
|
async launchInstance() {
|
||||||
if (!this.currentInstance) return;
|
if (!this.currentInstance) return;
|
||||||
|
|
||||||
|
if (this.hasUpdate || this.hasMismatches) {
|
||||||
|
await this.updateInstance();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.addLog('Проверка целостности файлов...', 'info');
|
this.addLog('Проверка целостности файлов...', 'info');
|
||||||
this.enablePlayButton(false);
|
this.enablePlayButton(false);
|
||||||
|
|
||||||
@@ -262,6 +315,118 @@ class App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateInstance() {
|
||||||
|
if (!this.currentInstance || !this.isServerPack) return;
|
||||||
|
|
||||||
|
const packName = this.currentInstance.serverPackName;
|
||||||
|
if (!packName) {
|
||||||
|
this.addLog('Ошибка: неизвестная сборка', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addLog('Обновление сборки...', 'info');
|
||||||
|
this.enablePlayButton(false);
|
||||||
|
|
||||||
|
const progressContainer = this.showAnimatedProgress('Обновление сборки...');
|
||||||
|
|
||||||
|
let eventSource = null;
|
||||||
|
let progressData = { totalFiles: 0, downloadedFiles: 0 };
|
||||||
|
|
||||||
|
try {
|
||||||
|
eventSource = new EventSource(`/api/instances/${this.currentInstance.name}/install/stream`);
|
||||||
|
eventSource.onmessage = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
if (data.phase === 'starting') {
|
||||||
|
progressData.totalFiles = data.totalFiles || 0;
|
||||||
|
this.updateAnimatedProgress(progressContainer, `Загрузка: 0/${progressData.totalFiles} файлов`, 5);
|
||||||
|
} else if (data.phase === 'downloading') {
|
||||||
|
progressData.downloadedFiles = data.downloadedFiles || 0;
|
||||||
|
const total = data.totalFiles || progressData.totalFiles || 1;
|
||||||
|
const percent = Math.round((progressData.downloadedFiles / total) * 100);
|
||||||
|
const fileName = data.currentFile ? data.currentFile.split('/').pop() : '';
|
||||||
|
const filePercent = data.filePercent || 0;
|
||||||
|
this.updateAnimatedProgress(progressContainer,
|
||||||
|
`Файл ${progressData.downloadedFiles}/${total} (${percent}%)`,
|
||||||
|
percent,
|
||||||
|
fileName,
|
||||||
|
filePercent
|
||||||
|
);
|
||||||
|
} else if (data.phase === 'complete') {
|
||||||
|
this.updateAnimatedProgress(progressContainer, 'Готово!', 100);
|
||||||
|
} else if (data.phase === 'error') {
|
||||||
|
this.addLog('Ошибка: ' + (data.message || 'неизвестная ошибка'), 'error');
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.log('SSE not available, using fallback progress');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.request('/instances/zernmc/install', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
packName: packName,
|
||||||
|
instanceName: this.currentInstance.name
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (eventSource) {
|
||||||
|
eventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hideProgress();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.addLog('Сборка обновлена!', 'success');
|
||||||
|
|
||||||
|
this.addLog('Проверка после обновления...', 'info');
|
||||||
|
const verifyResult = await this.request(`/instances/${this.currentInstance.name}/verify`);
|
||||||
|
if (verifyResult.success && verifyResult.data) {
|
||||||
|
this.hasMismatches = verifyResult.data.hasMismatches;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateResult = await this.request(`/instances/${this.currentInstance.name}/updates`);
|
||||||
|
if (updateResult.success && updateResult.data) {
|
||||||
|
this.hasUpdate = updateResult.data.hasUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatePlayButton();
|
||||||
|
|
||||||
|
if (!this.hasUpdate && !this.hasMismatches) {
|
||||||
|
this.addLog('Готово к игре!', 'success');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.addLog('Ошибка обновления: ' + result.error, 'error');
|
||||||
|
this.updatePlayButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showAnimatedProgress(text) {
|
||||||
|
const progress = document.getElementById('download-progress');
|
||||||
|
const progressText = document.getElementById('progress-text');
|
||||||
|
const progressFill = document.getElementById('progress-fill');
|
||||||
|
|
||||||
|
progress.classList.remove('hidden');
|
||||||
|
progressText.innerHTML = `<div class="progress-label">${text}</div>
|
||||||
|
<div class="progress-file"></div>`;
|
||||||
|
progressFill.style.width = '5%';
|
||||||
|
progressFill.classList.add('animated');
|
||||||
|
|
||||||
|
return { container: progress, text: progressText, fill: progressFill };
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAnimatedProgress(progressContainer, text, percent, fileName = '', filePercent = 0) {
|
||||||
|
const { text: progressText, fill: progressFill } = progressContainer;
|
||||||
|
if (fileName) {
|
||||||
|
progressText.innerHTML = `<div class="progress-label">${text}</div>
|
||||||
|
<div class="progress-file">${fileName} (${filePercent}%)</div>`;
|
||||||
|
} else {
|
||||||
|
progressText.innerHTML = `<div class="progress-label">${text}</div>`;
|
||||||
|
}
|
||||||
|
progressFill.style.width = percent + '%';
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== DOWNLOAD MODAL ====================
|
// ==================== DOWNLOAD MODAL ====================
|
||||||
async showDownloadModal() {
|
async showDownloadModal() {
|
||||||
document.getElementById('download-modal').classList.remove('hidden');
|
document.getElementById('download-modal').classList.remove('hidden');
|
||||||
|
|||||||
Reference in New Issue
Block a user