58 Commits

Author SHA1 Message Date
SashegDev e021d499a9 feat(ui): add Web UI with JavaFX, install service, and new tests
- Add JavaFX WebView for native window UI (fallback to TUI on headless)
- Create WebServer with Javalin HTTP server
- Add webapp with dark theme and grid animation
- Create InstallService for ZernMC pack installation
- Integrate CLI installation logic via PackDownloader
- Add verifyHashes() using /pack/{name}/diff endpoint
- Add API endpoints: /instances/zernmc/install, /instances/{name}/updates, /instances/{name}/verify, /instances/{name}/playtime
- Add 14 new tests (WebServerTest, HeadlessDetectionTest, InstanceServiceTest)
- Total 44 tests now passing
2026-05-05 06:48:27 +00:00
SashegDev 896b58472f feat(api): add internal API foundation for UI
- Create api package with AuthService, InstanceService, LaunchService
- Add ApiResponse<T> model for consistent responses
- Create LauncherAPI central facade for all services
- Update Main.java to use new API for session checking
- All services compile successfully
2026-05-05 04:12:39 +00:00
SashegDev a501dd1d99 refactor(launch): dynamic version JSON parsing for Forge/NeoForge compatibility
- Replace hardcoded Forge/NeoForge args with version.json parsing
- Add VersionManifest.java — parses mainClass, arguments, libraries from JSON
- Implement rule matching for OS-specific library/argument filtering
- Build classpath dynamically from manifest libraries with fallback resolution
- Resolve game args with variable substitution (${version_name}, ${game_directory}, etc.)
- Auto-discover version.json path with multiple candidate formats
- Support all Forge versions (1.12.2 through 1.21+) and NeoForge out of the box
2026-05-04 22:58:49 +00:00
SashegDev 37e7c990a7 feat: add NeoForge support, fix Forge installPack bug, update server proxy
- Fix MinecraftLib.installPack() returning false for Forge (was dead code)
- Add NeoForgeInstaller.java with installer download and execution
- Update LaunchCommandBuilder with NeoForge JVM args, classpath, launch args
- Update LaunchMenu with NeoForge option, version selector, support check
- Update Instance.java loader type comment (vanilla, fabric, forge, neoforge)
- Update PackDownloader to handle neoforge loader type
- Update ZHttpClient with NEOFORGE_MAVEN service type and detection
- Add NeoForge proxy endpoints (/proxy/neoforge/versions, /proxy/neoforge/maven)
- Add maven.neoforged.net to proxy allowed_domains
- Add asset_index to PackMeta model and pack_manager scanning
- Include asset_index in /packs list endpoint response
2026-05-04 22:53:22 +00:00
SashegDev 14da6c1a45 test(client): add JUnit 5 tests (30 tests) — unit + integration
- Add JUnit 5 dependency to pom.xml with surefire plugin
- Add setBaseUrl() to ZHttpClient for test server override
- AuthManagerParsingTest (7 tests): error extraction from JSON responses
  (simple detail, validation array, multiple errors, plain text, truncation)
- PackDownloaderParsingTest (13 tests): JSON contract for packs, manifests,
  diffs, file info, ServerPack toString
- ServerIntegrationTest (10 tests): real Java client ↔ real FastAPI server
  (register, login, duplicate, wrong password, /admin/me, validate token,
  refresh, packs auth, pack manifest public, launcher version)
- Integration tests auto-start test server via venv python3 subprocess
  on random port with isolated temp DB, graceful skip if unavailable

All 30 tests pass, 0 failures
2026-05-04 22:40:10 +00:00
SashegDev a80c00f91c test(server): add client-facing endpoint tests (20 tests), fix pack contract assertions
- Add test_client.py with comprehensive client-server contract tests:
  - TestAuthFlowClient: full register → login → refresh → validate → /admin/me → logout lifecycle
  - TestPacksClientContract: /packs response fields matching ServerPack.java
  - TestPackManifestClientContract: /pack/{name} fields matching PackManifest.java
  - TestPackDiffClientContract: /pack/{name}/diff matching DiffResponse/FileInfo.java
    (all-new, no-changes, outdated-file, extra-local-file scenarios)
  - TestPackFileDownload: file serving, 404, path traversal security
  - TestPackPermissions: auth/pass requirements for /packs and /diff
  - TestLauncherVersion: /launcher/version endpoint
  - TestProxyEndpoints: /proxy/status, /proxy/fabric/versions/loader
- Add logged_in_user_with_pass fixture (role=1) for pack-related tests
- Add pack_fixture: creates temp pack with mod file, scans it, cleans up
- Fix manifest test: files don't have 'url' field (only in diff response)
- Fix /pack/{name} test: endpoint is public, no auth required

Total: 67 tests passing (47 existing + 20 new)
2026-05-04 22:28:12 +00:00
SashegDev ff921def7f test(server): add comprehensive test suite (47 tests), fix DB lock and schema bugs
- Add pytest test suite: test_auth.py, test_admin.py, test_pass.py,
  test_proxy.py, test_rate_limit.py, test_client_contract.py
- Fix SQLite 'database is locked' errors: moved log_audit() calls outside
  with get_db() blocks in register, login, logout, refresh, activate_pass
- Enable WAL mode and busy_timeout in get_db() for concurrent access
- Fix /admin/me: removed non-existent 'email' column from query
- Fix /admin/users list: disambiguated activated_at column in JOIN query
- Fix /auth/refresh: now returns refresh_token + expires_in + username/uuid/role
  to match AuthManager.AuthSession expectations; revokes old refresh token
- Fix conftest.py: unique usernames per test to avoid conflicts
- All 47 tests passing
2026-05-04 22:14:06 +00:00
SashegDev ba4eea8c4e fix(server,security): add ban check to validate_token, replace rate_limit DB with TTLCache 2026-05-04 21:12:35 +00:00
SashegDev 7daaedd327 chore(server): remove unused models, delete http_logger.py, rename viev_logs.py → view_logs.py 2026-05-04 21:10:11 +00:00
SashegDev 95e5338635 refactor(server): clean main.py — remove duplicate imports, dead code, unify logging, fix proxy lifecycle 2026-05-04 21:09:10 +00:00
SashegDev d78c6c27a5 feat(server): add /auth/pass/activate endpoint for pass code activation 2026-05-04 21:06:56 +00:00
SashegDev 466f155e0e feat(server): connect admin_router to FastAPI app 2026-05-04 21:06:02 +00:00
SashegDev 014ffc4fae fix(server): add role aliases in roles.py to fix broken admin_router imports 2026-05-04 21:04:44 +00:00
SashegDev ec8e4c05d2 fix(pom.xml): correct launch4j JAR path for exe build 2026-05-04 20:52:28 +00:00
SashegDev ae7532eec0 fix(TUI): proper arrow key handling — parse ESC sequences instead of treating as Esc 2026-05-04 20:39:29 +00:00
SashegDev 7b2a571180 just workin on the todo 2026-05-04 20:26:27 +00:00
SashegDev ce8237c0db last commit to uuuuh idl 2026-05-04 15:19:46 +00:00
SashegDev 75d87084e3 The fuck was hapanned тут 2026-04-22 12:54:57 +00:00
SashegDev 18919f7a8e Merge branch 'main' into alpha 2026-04-22 15:26:39 +03:00
SashegDev 1c2ae28511 Коммит, для того что бы если что роллбекать 2026-04-22 12:23:51 +00:00
SashegDev 9e003fe1a1 Фиксы проходок (нормально, в отличии от main ветки)
ОНО РАБОТАЕТ СУКАААА
2026-04-20 19:30:17 +00:00
SashegDev 6bd49217d0 Update issue templates 2026-04-20 19:59:07 +03:00
SashegDev 3afd832db9 Create LICENSE 2026-04-20 19:57:52 +03:00
SashegDev 81aebef6a5 test penis 2026-04-09 18:13:21 +00:00
SashegDev efff2d4ef6 fixes 2026-04-09 18:03:00 +00:00
SashegDev 46cafa5250 рефакторинг + новая система модерации 2026-04-09 17:28:48 +00:00
SashegDev 7ea88e0a27 СУКА ЛАСТ ФИКСЫ ДЛЯ ПРОХОДОК (логин работает) 2026-04-08 20:22:47 +00:00
SashegDev b29698a3a2 ДА БЛЯ Я ЗАБЕАЛСЯ ФИКСИТЬ ПОМОГИТЕ Я КОНЧЕННЫЫЫЙ 2026-04-08 20:16:51 +00:00
SashegDev c66a8d372a SuperMinor Fixes (надеюсь последние для аккаунтов) 2026-04-08 20:04:52 +00:00
SashegDev 8d24bb9a92 Minor fixes(важные блять) 2026-04-08 20:02:09 +00:00
SashegDev e6dacdcb61 Server Fixes 2026-04-08 19:56:38 +00:00
SashegDev e6e4d88aec 1.0.7 типоооо и фиксы 2026-04-08 19:45:15 +00:00
SashegDev 9657053e08 utf-8 рефактор чутка 2026-04-07 19:00:42 +00:00
SashegDev 27aafeaab3 небольшой рефактор 2026-04-07 18:54:16 +00:00
SashegDev af3183c6ef ВАЖНИ ФИКСЕС 2026-04-07 18:40:51 +00:00
SashegDev 5627c9cc6a Изменил версию и немного фиксов 2026-04-07 18:28:26 +00:00
SashegDev f0973e93df Readme модификейшин 2026-04-07 18:08:11 +00:00
SashegDev 02608a9856 Merge remote-tracking branch 'refs/remotes/origin/main' 2026-04-07 18:01:49 +00:00
SashegDev c5c119fcb6 Попытка добавления проходок, аккаунтов, а так же доработка прокси 2026-04-07 17:50:29 +00:00
SashegDev bce719bdc1 Update README.md 2026-04-07 18:11:54 +03:00
SashegDev 2509a9b4e9 Орфографическая правка 2026-04-07 12:24:06 +03:00
SashegDev 7696cfb9fb REAMDE.md update, мяу 2026-04-07 11:17:36 +03:00
SashegDev 27a0366fb8 ДОБАВЛЕНИЕ ПРОКСИ РЕЖИМА ЙОООУ 1.0.5 2026-04-06 19:57:32 +00:00
SashegDev f3a6bb21a0 Багфиксы ClassPath 2026-04-06 18:17:07 +00:00
SashegDev 911f4120dd Smol Fixes Yoooo 2026-04-06 17:00:39 +00:00
SashegDev 81c34b44e6 Both | БЛЯЯЯ ЗАГРУЗКА ПАКОВ С СЕРВЕРА СЮДААА 2026-04-06 00:32:36 +00:00
SashegDev 1c5beff21d Server BugFixes + убрал генерацию sevrer команды т.к это уже в клиенте лол 2026-04-05 22:25:43 +00:00
SashegDev ea7b77b549 Немного рефактора 2026-04-05 18:45:31 +00:00
SashegDev 7fb2cdc059 Попытка заставить работать Forge 2026-04-05 16:18:39 +00:00
SashegDev a10bc16d86 ФАБРИК ПОДДЕРЖКАААААААА 2026-04-05 15:43:16 +00:00
SashegDev 385d33ae5b Починил загрузку ассетов, добавлена оптимизация
запуск Vanilla версий работает
2026-04-05 14:56:01 +00:00
SashegDev 06a1b71ab8 uuuh почему бы и нет 2026-04-05 10:47:28 +00:00
SashegDev f5cbd2e649 Изменение получения self-version 2026-04-05 10:46:32 +00:00
SashegDev f1ab0a6cdf КЛИЕНТ ЛАУНЧЕРА ЙОООО 2026-04-05 00:18:57 +00:00
SashegDev 27dcd97c15 перенос readme т.к я криворукий 2026-04-04 14:59:00 +00:00
SashegDev ca522e229b server update 2026-04-04 14:57:15 +00:00
SashegDev 897637434b test 2026-04-04 14:55:18 +00:00
SashegDev b03bb845a6 first commit 2026-04-04 14:49:24 +00:00
16 changed files with 211 additions and 916 deletions
+1 -1
View File
@@ -19,7 +19,7 @@
- Графического интерфейса (GUI) — только TUI - Графического интерфейса (GUI) — только TUI
- Нормальных настроек (пока доступна только настройка Java и выделенной оперативной памяти) - Нормальных настроек (пока доступна только настройка Java и выделенной оперативной памяти)
- Поддержки **Forge** (в разработке) (технически уже есть вместе с NeoForge (science PR№4)) - Поддержки **Forge** (в разработке)
- Поддержки Quilt, LabyMod, NeoForge и других лоадеров - Поддержки Quilt, LabyMod, NeoForge и других лоадеров
- Раздела новостей об обновлениях Minecraft и лаунчера - Раздела новостей об обновлениях Minecraft и лаунчера
- Выбора готовых пресетов оптимизации JVM - Выбора готовых пресетов оптимизации JVM
+7 -109
View File
@@ -3,13 +3,9 @@
<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.8</version> <version>1.0.7</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>
@@ -28,54 +24,11 @@
<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>Samopisnui Minecraft-launcher. by SashegDev</Implementation-Description> <Implementation-Description>Полностью самописный Minecraft-лаунчер. Написанный 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>
@@ -92,36 +45,28 @@
<goal>launch4j</goal> <goal>launch4j</goal>
</goals> </goals>
<configuration> <configuration>
<outfile>../server/builds/ZernMCLauncher-${project.version}.exe</outfile> <outfile>../server/builds/ZernMCLauncher.exe</outfile>
<jar>../server/builds/ZernMCLauncher.jar</jar> <jar>../server/builds/ZernMCLauncher.jar</jar>
<headerType>gui</headerType> <headerType>console</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 — just a Minecraft launcher</fileDescription> <fileDescription>ZernMC Launcher — A Little 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-${project.version}.exe</originalFilename> <originalFilename>ZernMCLauncher.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>
@@ -138,9 +83,6 @@
<copy> <copy>
<fileset /> <fileset />
</copy> </copy>
<copy>
<fileset />
</copy>
<zip /> <zip />
</target> </target>
</configuration> </configuration>
@@ -166,55 +108,11 @@
<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>
<project.description>ZernMC Launcher - just a minimalistic launcher by SashegDev</project.description> <maven.compiler.target>21</maven.compiler.target>
<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>
+6 -141
View File
@@ -17,9 +17,6 @@
<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>
@@ -63,42 +60,6 @@
<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>
@@ -140,66 +101,16 @@
</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>
<!-- Copy JavaFX dependencies --> <!-- Launch4j для создания .exe -->
<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>
@@ -210,16 +121,11 @@
<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>gui</headerType> <headerType>console</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>
@@ -255,22 +161,11 @@
<fileset dir="${user.home}/launcher/jre/jre21"/> <fileset dir="${user.home}/launcher/jre/jre21"/>
</copy> </copy>
<!-- Копируем JavaFX JAR в builds --> <!-- Создаём zip только с .exe и jre21 (без .jar и build.version) -->
<copy todir="../server/builds/lib-javafx" overwrite="true"> <zip destfile="../server/builds/ZernMCLauncher-${project.version}.zip"
<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,ZernMCLauncher.jar,jre21/**,lib-javafx/**,launcher.sh" includes="ZernMCLauncher.exe,jre21/**"
excludes="build.version"/> excludes="*.jar,build.version"/>
</target> </target>
</configuration> </configuration>
</execution> </execution>
@@ -302,35 +197,5 @@
<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,7 +34,6 @@ 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);
} }
@@ -71,21 +70,14 @@ public class Main {
// Даем серверу время запуститься // Даем серверу время запуститься
Thread.sleep(1000); Thread.sleep(1000);
// Проверяем headless перед запуском JavaFX (только для не-Windows систем) // Проверяем headless перед запуском JavaFX
if (!System.getProperty("os.name").toLowerCase().contains("win")) { if (java.awt.GraphicsEnvironment.isHeadless()) {
boolean isHeadless = java.awt.GraphicsEnvironment.isHeadless(); System.out.println(ZAnsi.yellow("Дисплей недоступен, переключаюсь на TUI..."));
String display = System.getenv("DISPLAY"); WebServer.stop();
if (isHeadless && (display == null || display.isEmpty())) { runTUI(args);
System.out.println(ZAnsi.yellow("Дисплей недоступен, переключаюсь на TUI...")); return;
WebServer.stop();
runTUI(args);
return;
}
} }
// Проверка обновлений лаунчера
checkAndAutoUpdateLauncher();
// Запускаем JavaFX окно // Запускаем JavaFX окно
UIWindow.start(port); UIWindow.start(port);
} }
@@ -186,20 +178,12 @@ 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("Перезапуск лаунчера с новой версией..."));
ProcessBuilder pb = new ProcessBuilder( new ProcessBuilder(javaPath, "-jar", jarPath)
javaPath, .inheritIO()
"--module-path=" + javafxPath, .start();
"--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) {
@@ -13,12 +13,6 @@ 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);
@@ -32,9 +26,6 @@ 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();
@@ -74,14 +65,13 @@ public class InstallService {
} }
PackDownloader downloader = new PackDownloader(instance); PackDownloader downloader = new PackDownloader(instance);
int serverVersion = downloader.checkForUpdates(instance.getServerPackName()); boolean hasUpdate = 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(),
serverVersion hasUpdate ? instance.getServerVersion() + 1 : instance.getServerVersion()
)); ));
} catch (Exception e) { } catch (Exception e) {
return ApiResponse.error("Ошибка проверки обновлений: " + e.getMessage()); return ApiResponse.error("Ошибка проверки обновлений: " + e.getMessage());
@@ -68,14 +68,12 @@ 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()
); );
} }
@@ -84,23 +82,17 @@ private InstanceInfo toInstanceInfo(Instance instance) {
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, boolean isServerPack, String serverPackName) { public InstanceInfo(String name, String path, String version, String loaderType) {
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,17 +25,11 @@ 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 {
try { if (Config.isZernMCBuild()) {
if (Config.isZernMCBuild()) { showZernMCOnly();
showZernMCOnly(); } else {
} else { showGlobal();
showGlobal();
}
} catch (ExitToMainMenuException e) {
// Возвращаемся в главное меню - ничего не делаем, просто выходим
} }
} }
@@ -288,15 +282,6 @@ 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()));
@@ -335,13 +320,9 @@ public class LaunchMenu {
changeLoaderVersion(instance); changeLoaderVersion(instance);
} else { } else {
deleteInstance(instance); deleteInstance(instance);
throw new ExitToMainMenuException(); // Выходим в главное меню
} }
} }
case 3 -> { case 3 -> deleteInstance(instance);
deleteInstance(instance);
throw new ExitToMainMenuException(); // Выходим в главное меню после удаления
}
} }
} }
} }
@@ -351,8 +332,7 @@ 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);
int serverVersion = downloader.checkForUpdates(instance.getServerPackName()); boolean hasUpdate = 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() + ")"));
@@ -443,15 +423,14 @@ 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) {
@@ -646,8 +625,9 @@ public class LaunchMenu {
} }
private boolean isNeoForgeSupported(String version) { private boolean isNeoForgeSupported(String version) {
// ВРЕМЕННО ОТКЛЮЧЕНО: в разработке return version.matches("^1\\.20\\.[1-9].*") ||
return false; version.matches("^1\\.21.*") ||
version.matches("^\\d{2}\\..*");
} }
private String askFabricLoaderVersion() throws Exception { private String askFabricLoaderVersion() throws Exception {
@@ -64,10 +64,9 @@ public class UpdateMenu {
for (Instance instance : serverInstances) { for (Instance instance : serverInstances) {
PackDownloader downloader = new PackDownloader(instance); PackDownloader downloader = new PackDownloader(instance);
try { try {
int serverVersion = downloader.checkForUpdates(instance.getServerPackName()); boolean hasUpdate = 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);
@@ -29,55 +29,12 @@ 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 ProgressCallback progressCallback; //private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
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;
}
/** /**
* Получить список доступных паков с сервера * Получить список доступных паков с сервера
*/ */
@@ -268,16 +225,15 @@ public class PackDownloader {
/** /**
* Проверить наличие обновлений для серверной сборки * Проверить наличие обновлений для серверной сборки
* @return версия на сервере, или 0 если нет обновлений
*/ */
public int checkForUpdates(String packName) throws Exception { public boolean checkForUpdates(String packName) throws Exception {
if (!instance.isServerPack()) return 0; if (!instance.isServerPack()) return false;
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 ? serverVersion : 0; return serverVersion > localVersion;
} }
/** /**
@@ -441,18 +397,16 @@ 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 {
@@ -463,103 +417,85 @@ 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) { if (fileSize > 0 && totalRead % 8192 == 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();
} }
/** /**
@@ -39,41 +39,45 @@ 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";
String universalJarUrl = baseMavenUrl + mavenArtifact + "-" + neoForgeVersion + "-universal.jar"; Path installerJar = instance.getPath().resolve("neoforge-installer.jar");
Path neoForgeJar = versionDir.resolve(versionName + ".jar");
System.out.println(ZAnsi.cyan("Скачивание NeoForge universal.jar...")); System.out.println(ZAnsi.cyan("Скачивание NeoForge Installer..."));
downloadFileDirect(universalJarUrl, neoForgeJar); downloadFileWithProgress(installerUrl, installerJar);
// Создаем version.json вручную System.out.println(ZAnsi.cyan("Запуск NeoForge Installer..."));
System.out.println(ZAnsi.cyan("Создание version.json...")); System.out.println(ZAnsi.yellow("Это может занять несколько минут. Пожалуйста, подождите...\n"));
createVersionJson(versionDir.resolve(versionName + ".json"), mcVersion, neoForgeVersion, mavenArtifact);
// Скачиваем необходимые библиотеки boolean success = runNeoForgeInstaller(installerJar);
System.out.println(ZAnsi.cyan("Скачивание библиотек NeoForge..."));
downloadNeoForgeLibraries(mcVersion, neoForgeVersion, mavenGroup, mavenArtifact);
System.out.println(ZAnsi.brightGreen("\nNeoForge " + neoForgeVersion + " успешно установлен!")); if (success) {
instance.setMinecraftVersion(mcVersion); try {
instance.setLoaderType("neoforge"); downloadMissingLibraries(mcVersion, neoForgeVersion, mavenGroup, mavenArtifact);
instance.setLoaderVersion(neoForgeVersion); } catch (Exception e) {
System.out.println(ZAnsi.yellow("Предупреждение: не удалось докачать некоторые библиотеки: " + e.getMessage()));
}
return true; 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) { private String getMavenGroup(String mcVersion) {
@@ -149,109 +153,119 @@ public class NeoForgeInstaller {
ProgressBar.finish("NeoForge Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")"); ProgressBar.finish("NeoForge Installer (" + ProgressBar.formatBytes(Files.size(target)) + ")");
} }
private void downloadFileDirect(String url, Path target) throws Exception { private boolean runNeoForgeInstaller(Path installerJar) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder() int maxRetries = 3;
.uri(URI.create(url)) int attempt = 1;
.GET()
.build();
HttpResponse<Path> response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(target)); while (attempt <= maxRetries) {
System.out.println(ZAnsi.cyan("Попытка " + attempt + " из " + maxRetries));
if (response.statusCode() != 200) { ProcessBuilder pb = new ProcessBuilder(
throw new IOException("HTTP " + response.statusCode() + " for " + url); "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++;
} }
System.out.println(ZAnsi.green(" " + target.getFileName() + " завершено ✓")); return false;
} }
private void createVersionJson(Path jsonFile, String mcVersion, String neoForgeVersion, String mavenArtifact) throws IOException { private void downloadMissingLibraries(String mcVersion, String neoForgeVersion, String mavenGroup, String mavenArtifact) throws Exception {
// Создаем минимальный version.json для NeoForge System.out.println(ZAnsi.cyan("Проверка и докачка отсутствующих библиотек..."));
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);
Files.writeString(jsonFile, json); Map<String, String> alternativeUrls = new HashMap<>();
System.out.println(ZAnsi.green(" version.json создан ✓")); 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",
private void downloadNeoForgeLibraries(String mcVersion, String neoForgeVersion, String mavenGroup, String mavenArtifact) throws Exception { "https://repo1.maven.org/maven2/org/ow2/asm/asm-commons/9.6/asm-commons-9.6.jar");
System.out.println(ZAnsi.cyan("Скачивание библиотек NeoForge...")); 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");
String baseMavenUrl = "https://maven.neoforged.net/releases/"
+ mavenGroup.replace('.', '/') + "/";
Path librariesDir = instance.getPath().resolve("libraries"); Path librariesDir = instance.getPath().resolve("libraries");
// Список основных библиотек NeoForge for (Map.Entry<String, String> entry : alternativeUrls.entrySet()) {
String[][] libs = { Path target = librariesDir.resolve(entry.getKey());
{mavenGroup, mavenArtifact, neoForgeVersion}, if (!Files.exists(target)) {
{"cpw.mods", "bootstraplauncher", "1.1.2"}, Files.createDirectories(target.getParent());
{"net.minecraftforge", "unsafe", "0.2.0"}, System.out.println(ZAnsi.yellow("Докачка: " + target.getFileName()));
{"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 (String[] lib : libs) { for (int attempt = 1; attempt <= 3; attempt++) {
String group = lib[0].replace('.', '/'); try {
String artifact = lib[1]; downloadFileWithProgress(entry.getValue(), target);
String version = lib[2]; break;
} catch (Exception e) {
String jarName = artifact + "-" + version + ".jar"; if (attempt == 3) throw e;
String mavenPath = group + "/" + artifact + "/" + version + "/" + jarName; System.out.println(ZAnsi.yellow("Повторная попытка " + attempt + "/3..."));
Path target = librariesDir.resolve(mavenPath); Thread.sleep(2000);
}
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,6 +17,11 @@ 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,21 +5,13 @@ 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;
@@ -190,48 +182,6 @@ 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");
@@ -376,88 +326,4 @@ 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");
}
}
} }
-12
View File
@@ -1,12 +0,0 @@
#!/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,39 +484,6 @@ 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;
@@ -686,30 +653,6 @@ 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;
+1 -166
View File
@@ -8,9 +8,6 @@ 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();
} }
@@ -213,32 +210,7 @@ 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();
@@ -247,26 +219,6 @@ 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 = `
@@ -295,11 +247,6 @@ 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);
@@ -315,118 +262,6 @@ 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');