diff --git a/launcher/bootstrap/pom.xml b/launcher/bootstrap/pom.xml
index 20cc5ce..60792f9 100644
--- a/launcher/bootstrap/pom.xml
+++ b/launcher/bootstrap/pom.xml
@@ -7,7 +7,7 @@
me.sashegdev
ZernMCLauncher
- 1.0.8
+ 1.0.9
zernmc-bootstrap
diff --git a/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java b/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java
index 404ab90..6f451a7 100644
--- a/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java
+++ b/launcher/bootstrap/src/main/java/me/sashegdev/zernmc/launcher/Bootstrap.java
@@ -8,6 +8,8 @@ import java.security.MessageDigest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
public class Bootstrap {
private static final String VERSION_FILE = "build.version";
@@ -62,10 +64,26 @@ public class Bootstrap {
}
private static String readCurrentVersion() {
+ // Читаем версию из манифеста zernmclauncher.jar в папке bin/
+ Path launcherJar = baseDir.resolve("bin").resolve("zernmclauncher.jar");
+ try {
+ if (Files.exists(launcherJar)) {
+ try (FileInputStream fis = new FileInputStream(launcherJar.toFile())) {
+ Manifest manifest = new Manifest(fis);
+ String version = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+ if (version != null && !version.isBlank()) {
+ return version;
+ }
+ }
+ }
+ } catch (Exception ignored) {}
+
+ // Fallback: из build.version
Path f = baseDir.resolve(VERSION_FILE);
try {
if (Files.exists(f)) return Files.readString(f).trim();
} catch (Exception ignored) {}
+
return "0.0.0";
}
diff --git a/launcher/launcher/pom.xml b/launcher/launcher/pom.xml
index d5ae1ab..5af03f8 100644
--- a/launcher/launcher/pom.xml
+++ b/launcher/launcher/pom.xml
@@ -7,7 +7,7 @@
me.sashegdev
ZernMCLauncher
- 1.0.8
+ 1.0.9
zernmclauncher
@@ -223,11 +223,24 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ includes="zernmc.exe,bin/**,assets/**,lib/**"
+ excludes="build.version,*-${project.version}.*,zernmclauncher.jar,zernmc-bootstrap.jar"/>
diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/auth/AuthService.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/auth/AuthService.java
index 48097c0..b74c945 100644
--- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/auth/AuthService.java
+++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/auth/AuthService.java
@@ -37,7 +37,9 @@ public class AuthService {
SessionInfo info = new SessionInfo(
AuthManager.getUsername(),
AuthManager.getAccessToken(),
- AuthManager.hasActivePass()
+ AuthManager.hasActivePass(),
+ AuthManager.getRole(),
+ AuthManager.getRoleName()
);
return ApiResponse.success(info);
}
@@ -120,15 +122,21 @@ public class AuthService {
private String username;
private String token;
private boolean passActive;
+ private int role;
+ private String roleName;
- public SessionInfo(String username, String token, boolean passActive) {
+ public SessionInfo(String username, String token, boolean passActive, int role, String roleName) {
this.username = username;
this.token = token;
this.passActive = passActive;
+ this.role = role;
+ this.roleName = roleName;
}
public String getUsername() { return username; }
public String getToken() { return token; }
public boolean isPassActive() { return passActive; }
+ public int getRole() { return role; }
+ public String getRoleName() { return roleName; }
}
}
diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/launch/LaunchService.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/launch/LaunchService.java
index c203138..157a413 100644
--- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/launch/LaunchService.java
+++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/launch/LaunchService.java
@@ -1,12 +1,16 @@
package me.sashegdev.zernmc.launcher.api.launch;
import me.sashegdev.zernmc.launcher.api.ApiResponse;
+import me.sashegdev.zernmc.launcher.auth.AuthManager;
import me.sashegdev.zernmc.launcher.minecraft.Instance;
import me.sashegdev.zernmc.launcher.minecraft.InstanceManager;
import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder;
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
+import me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher;
+import java.io.BufferedReader;
import java.io.IOException;
+import java.io.InputStreamReader;
import java.nio.file.Path;
import java.util.List;
@@ -44,15 +48,47 @@ public class LaunchService {
LaunchCommandBuilder builder = new LaunchCommandBuilder(instance);
LaunchOptions options = new LaunchOptions();
+
+ // Set auth info
+ options.setUsername(AuthManager.getUsername());
+ options.setAccessToken(AuthManager.getAccessToken());
+ options.setUuid(AuthManager.getUuid());
List command = builder.build(options);
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.directory(instance.getPath().toFile());
- processBuilder.inheritIO();
Process process = processBuilder.start();
+ // Capture output
+ Thread outThread = new Thread(() -> {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ JFXLauncher.appendGameLog(line);
+ }
+ } catch (Exception e) {
+ JFXLauncher.appendGameLog("[Ошибка чтения вывода: " + e.getMessage() + "]");
+ }
+ });
+ outThread.setDaemon(true);
+ outThread.start();
+
+ // Capture errors
+ Thread errThread = new Thread(() -> {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ JFXLauncher.appendGameLog("[ERR] " + line);
+ }
+ } catch (Exception e) {
+ JFXLauncher.appendGameLog("[Ошибка чтения ошибок: " + e.getMessage() + "]");
+ }
+ });
+ errThread.setDaemon(true);
+ errThread.start();
+
ProcessInfo info = new ProcessInfo(
instanceName,
process.pid(),
diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/auth/AuthManager.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/auth/AuthManager.java
index d169b23..7e30a01 100644
--- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/auth/AuthManager.java
+++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/auth/AuthManager.java
@@ -213,6 +213,13 @@ public class AuthManager {
return session != null ? session.role : ROLE_USER;
}
+ public static String getRoleName() {
+ if (userInfo != null && userInfo.role_name != null) {
+ return userInfo.role_name;
+ }
+ return "USER";
+ }
+
// ====================== POST ======================
private static SimpleHttpResponse post(String endpoint, String jsonBody) throws Exception {
String fullUrl = ZHttpClient.getBaseUrl() + endpoint;
diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/menu/LoginMenu.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/menu/LoginMenu.java
index 6a106a6..2115e2a 100644
--- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/menu/LoginMenu.java
+++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/menu/LoginMenu.java
@@ -166,9 +166,9 @@ public class LoginMenu {
if (key == 27) {
// Escape sequence — consume remaining bytes (arrow keys, etc.)
- int next = passTerminal.reader().read(50);
+ int next = passTerminal.reader().read();
if (next == 91) { // '[' — arrow key sequence
- passTerminal.reader().read(50); // consume 'A'/'B'/'C'/'D'
+ passTerminal.reader().read(); // consume 'A'/'B'/'C'/'D'
}
continue;
}
diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java
index 9593357..8242c16 100644
--- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java
+++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java
@@ -6,10 +6,14 @@ import me.sashegdev.zernmc.launcher.minecraft.installer.NeoForgeInstaller;
import me.sashegdev.zernmc.launcher.minecraft.installer.VersionInstaller;
import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder;
import me.sashegdev.zernmc.launcher.minecraft.model.LaunchOptions;
+import me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher;
import me.sashegdev.zernmc.launcher.utils.ConsoleUtils;
import me.sashegdev.zernmc.launcher.utils.ZAnsi;
+import java.io.BufferedReader;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@@ -114,15 +118,43 @@ public class MinecraftLib {
ProcessBuilder pb = new ProcessBuilder(command);
pb.directory(instance.getPath().toFile());
- pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
- pb.redirectError(ProcessBuilder.Redirect.INHERIT);
- pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
System.out.println(ZAnsi.brightGreen("\nЗапускаем Minecraft...\n"));
ConsoleUtils.clearScreen();
Process process = pb.start();
+
+ // Capture output
+ Thread outThread = new Thread(() -> {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ JFXLauncher.appendGameLog(line);
+ }
+ } catch (Exception e) {
+ JFXLauncher.appendGameLog("[Ошибка чтения вывода: " + e.getMessage() + "]");
+ }
+ });
+ outThread.setDaemon(true);
+ outThread.start();
+
+ // Capture errors
+ Thread errThread = new Thread(() -> {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ JFXLauncher.appendGameLog("[ERR] " + line);
+ }
+ } catch (Exception e) {
+ JFXLauncher.appendGameLog("[Ошибка чтения ошибок: " + e.getMessage() + "]");
+ }
+ });
+ errThread.setDaemon(true);
+ errThread.start();
+
int exitCode = process.waitFor();
+ outThread.join(1000);
+ errThread.join(1000);
System.out.println(ZAnsi.yellow("\nMinecraft завершился с кодом: " + exitCode));
}
diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java
index 2fdea9c..74debf6 100644
--- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java
+++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/VersionManifest.java
@@ -99,8 +99,17 @@ public class VersionManifest {
if (rule.has("features")) {
JSONObject features = rule.getJSONObject("features");
for (String key : features.keySet()) {
- if (key.startsWith("is_demo_user") || key.startsWith("has_custom_resolution")) continue;
+ if (key.startsWith("has_custom_resolution")) {
+ continue; // Лаунчер сам обрабатывает разрешение
+ }
+ if (key.startsWith("is_demo_user")) {
+ // Лаунчер не использует demo режим, считаем фичу false
+ matches = false;
+ break;
+ }
+ // Неизвестная фича — считаем false
matches = false;
+ break;
}
}
diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/ArrowMenu.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/ArrowMenu.java
index 1a1720c..eaf4eb5 100644
--- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/ArrowMenu.java
+++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/ArrowMenu.java
@@ -48,9 +48,9 @@ public class ArrowMenu {
return selected;
}
else if (key == 27) { // Esc or arrow escape seq
- int next = terminal.reader().read(50);
+ int next = terminal.reader().read();
if (next == 91) { // '[' — start of arrow escape sequence
- int arrow = terminal.reader().read(50);
+ int arrow = terminal.reader().read();
if (arrow == 65) { // 'A' — Up arrow
selected = (selected - 1 + options.size()) % options.size();
} else if (arrow == 66) { // 'B' — Down arrow
diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java
index 1b60d4b..496fbd8 100644
--- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java
+++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXLauncher.java
@@ -16,6 +16,9 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
@@ -30,8 +33,21 @@ public class JFXLauncher extends Application {
private final Gson gson = new Gson();
private HttpServer server;
private StringBuilder logBuffer = new StringBuilder();
+ private static StringBuilder gameLogBuffer = new StringBuilder();
private Stage mainStage;
+ public static void appendGameLog(String log) {
+ synchronized (gameLogBuffer) {
+ gameLogBuffer.append(log).append("\n");
+ }
+ }
+
+ public static String getGameLogs() {
+ synchronized (gameLogBuffer) {
+ return gameLogBuffer.toString();
+ }
+ }
+
public static void main(String[] args) {
launch(args);
}
@@ -49,12 +65,17 @@ public class JFXLauncher extends Application {
engine.setJavaScriptEnabled(true);
engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
+ log("[UI] Load state: " + oldState + " -> " + newState);
if (newState == Worker.State.SUCCEEDED) {
log("Страница загружена");
+ } else if (newState == Worker.State.FAILED) {
+ log("[UI] Load FAILED: " + engine.getLoadWorker().getException());
}
});
+
+ engine.setOnAlert(e -> log("[UI] Alert: " + e.getData()));
- String url = "http://localhost:" + PORT + "/ui/";
+ String url = "http://localhost:" + PORT + "/assets/ui/index.html";
engine.load(url);
stage.setTitle(APP_TITLE);
@@ -86,8 +107,9 @@ public class JFXLauncher extends Application {
server.createContext("/api/launch", this::handleLaunch);
server.createContext("/api/install", this::handleInstall);
server.createContext("/api/logs", this::handleLogs);
+ server.createContext("/api/game-logs", this::handleGameLogs);
server.createContext("/api/exit", this::handleExit);
- server.createContext("/ui/", this::handleStatic);
+ server.createContext("/assets/", this::handleStatic);
server.setExecutor(Executors.newCachedThreadPool());
server.start();
@@ -134,6 +156,8 @@ public class JFXLauncher extends Application {
Map data = new HashMap<>();
data.put("username", api.getCurrentUsername());
data.put("passActive", AuthManager.hasActivePass());
+ data.put("role", AuthManager.getRole());
+ data.put("roleName", AuthManager.getRoleName());
sendJson(exchange, Map.of("success", true, "data", data));
} catch (Exception e) {
sendJson(exchange, Map.of("success", false, "error", e.getMessage()));
@@ -218,6 +242,10 @@ public class JFXLauncher extends Application {
sendJson(exchange, Map.of("success", true, "data", logBuffer.toString()));
}
+ private void handleGameLogs(HttpExchange exchange) {
+ sendJson(exchange, Map.of("success", true, "data", getGameLogs()));
+ }
+
private void handleExit(HttpExchange exchange) {
log("Выход...");
if (mainStage != null) mainStage.close();
@@ -227,23 +255,29 @@ public class JFXLauncher extends Application {
private void handleStatic(HttpExchange exchange) {
try {
String path = exchange.getRequestURI().getPath();
- if (path.equals("/ui/") || path.equals("/ui")) path = "/ui/index.html";
+ log("[UI] Request: " + path);
- var resource = JFXLauncher.class.getResource(path);
- if (resource == null) {
+ String relativePath = path.startsWith("/") ? path.substring(1) : path;
+ Path file = Paths.get(relativePath).toAbsolutePath();
+
+ if (!Files.exists(file)) {
+ log("[UI] File not found: " + file);
exchange.sendResponseHeaders(404, 0);
exchange.close();
return;
}
- byte[] content = resource.openStream().readAllBytes();
+ byte[] content = Files.readAllBytes(file);
+ log("[UI] Loaded " + content.length + " bytes: " + path);
String ct = getContentType(path);
exchange.getResponseHeaders().set("Content-Type", ct);
exchange.sendResponseHeaders(200, content.length);
exchange.getResponseBody().write(content);
exchange.close();
- } catch (Exception ignored) {}
+ } catch (Exception e) {
+ log("[UI] Error serving: " + e.getMessage());
+ }
}
private String getContentType(String path) {
diff --git a/launcher/launcher/src/resources/ui/index.html b/launcher/launcher/src/resources/ui/index.html
index daf5136..13583c7 100644
--- a/launcher/launcher/src/resources/ui/index.html
+++ b/launcher/launcher/src/resources/ui/index.html
@@ -30,6 +30,7 @@
-
-
+ -
diff --git a/launcher/launcher/src/resources/ui/launcher.js b/launcher/launcher/src/resources/ui/launcher.js
index e4236d7..42f20d2 100644
--- a/launcher/launcher/src/resources/ui/launcher.js
+++ b/launcher/launcher/src/resources/ui/launcher.js
@@ -70,6 +70,11 @@ async function loadAccountInfo() {
const statusEl = document.getElementById('account-status');
statusEl.textContent = result.data.passActive ? 'PRO' : 'FREE';
statusEl.className = 'badge ' + (result.data.passActive ? 'active' : 'inactive');
+
+ const roleEl = document.getElementById('account-role');
+ if (roleEl && result.data.roleName) {
+ roleEl.textContent = result.data.roleName;
+ }
} else {
showLoginScreen();
}
@@ -239,8 +244,41 @@ document.addEventListener('DOMContentLoaded', async () => {
showMainScreen();
await loadInstances();
}
+
+ // Start polling for server logs
+ startLogPolling();
});
+let lastLogLength = 0;
+let lastGameLogLength = 0;
+function startLogPolling() {
+ setInterval(async () => {
+ // Launcher logs
+ const result = await apiCall('/logs');
+ if (result.success && result.data && result.data.length > lastLogLength) {
+ const newLogs = result.data.substring(lastLogLength);
+ const lines = newLogs.split('\n').filter(l => l.trim());
+ lines.forEach(line => {
+ if (line.includes('[JFX]')) {
+ log(line.replace('[JFX] ', ''), 'info');
+ }
+ });
+ lastLogLength = result.data.length;
+ }
+
+ // Game logs
+ const gameResult = await apiCall('/game-logs');
+ if (gameResult.success && gameResult.data && gameResult.data.length > lastGameLogLength) {
+ const newLogs = gameResult.data.substring(lastGameLogLength);
+ const lines = newLogs.split('\n').filter(l => l.trim());
+ lines.forEach(line => {
+ log('[GAME] ' + line, 'info');
+ });
+ lastGameLogLength = gameResult.data.length;
+ }
+ }, 2000);
+}
+
// ============ Form Handlers ============
document.getElementById('login-form').addEventListener('submit', async (e) => {
diff --git a/launcher/launcher/src/resources/ui/style.css b/launcher/launcher/src/resources/ui/style.css
index 40b3776..481035c 100644
--- a/launcher/launcher/src/resources/ui/style.css
+++ b/launcher/launcher/src/resources/ui/style.css
@@ -194,6 +194,11 @@ input::placeholder {
color: var(--error);
}
+.role-badge {
+ background: rgba(99, 102, 241, 0.2);
+ color: #818cf8;
+}
+
/* Main Content */
.main-content {
flex: 1;
diff --git a/launcher/pom.xml b/launcher/pom.xml
index 9cfb6e5..ea9f5d6 100644
--- a/launcher/pom.xml
+++ b/launcher/pom.xml
@@ -6,7 +6,7 @@
4.0.0
me.sashegdev
ZernMCLauncher
- 1.0.8
+ 1.0.9
pom
ZernMC Launcher Parent
diff --git a/server/main.py b/server/main.py
index 3894c2d..83faa59 100644
--- a/server/main.py
+++ b/server/main.py
@@ -150,6 +150,11 @@ async def lifespan(app: FastAPI):
# Scan launcher versions and generate meta
logger.info("Scanning launcher versions...")
+
+ # Extract new format ZIPs to versions directory
+ logger.info("Extracting new format versions...")
+ extract_new_format_versions()
+
launcher_versions = get_launcher_versions()
if launcher_versions:
latest = launcher_versions[0]
@@ -773,7 +778,12 @@ async def get_pack_file(pack_name: str, file_path: str, request: Request):
# ====================== ЭНДПОИНТЫ ДЛЯ ЛАУНЧЕРА ======================
def get_current_launcher_version() -> str:
- """Get current launcher version from build.version file"""
+ """Get current launcher version from meta system (new format) or build.version (legacy)"""
+ versions = get_launcher_versions()
+ if versions:
+ return versions[0]["meta"]["version"]
+
+ # Fallback to build.version for legacy
version_file = BUILDS_DIR / "build.version"
if version_file.exists():
return version_file.read_text().strip()
@@ -892,6 +902,35 @@ def get_launcher_version_meta(version: str) -> Optional[dict]:
return scan_launcher_version(version)
+def extract_new_format_versions():
+ """Extract new format ZIPs to versions directory"""
+ VERSIONS_DIR.mkdir(exist_ok=True)
+
+ # Find all ZernMC-win-*.zip files
+ new_format_zips = list(BUILDS_DIR.glob("ZernMC-win-*.zip"))
+
+ for zip_file in new_format_zips:
+ version = zip_file.stem.replace("ZernMC-win-", "")
+ extract_dir = VERSIONS_DIR / version
+
+ # Skip if already extracted and meta exists
+ if extract_dir.exists() and (extract_dir / "meta.json").exists():
+ logger.debug(f"Version {version} already extracted")
+ continue
+
+ logger.info(f"Extracting {zip_file.name} to versions/{version}/...")
+
+ try:
+ import zipfile
+ with zipfile.ZipFile(zip_file, 'r') as zf:
+ # Extract all files
+ zf.extractall(extract_dir)
+
+ logger.info(f"Extracted {zip_file.name} successfully")
+ except Exception as e:
+ logger.error(f"Failed to extract {zip_file.name}: {e}")
+
+
# ====================== END ЛАУНЧЕР МЕТА СИСТЕМА ======================
@@ -1008,14 +1047,17 @@ async def get_launcher_version():
@app.get("/launcher/download/jar")
async def download_launcher_jar():
"""Download launcher JAR file"""
- file_path = BUILDS_DIR / "ZernMCLauncher.jar"
+ # Prefer new shaded JAR, fallback to old
+ file_path = BUILDS_DIR / "zernmclauncher.jar"
+ if not file_path.exists():
+ file_path = BUILDS_DIR / "ZernMCLauncher.jar"
if not file_path.exists():
raise HTTPException(404, "JAR file not found")
return FileResponse(
path=file_path,
- filename="ZernMCLauncher.jar",
+ filename="zernmclauncher.jar",
media_type="application/java-archive"
)
@@ -1114,7 +1156,7 @@ async def get_launcher_meta_list():
@app.get("/launcher/meta/{version}")
-async def get_launcher_version_meta(version: str):
+async def get_launcher_version_meta_handler(version: str):
"""Get meta for specific launcher version"""
meta = get_launcher_version_meta(version)
if not meta: