Fix: Multiple launcher issues
- Fix CLI arrow keys: remove 50ms timeout in escape sequence handling (ArrowMenu, LoginMenu) - Add network logs polling to UI via /api/logs endpoint - Display user role in launcher header (AuthManager, AuthService, JFXLauncher, UI) - Capture and display game logs in launcher via /api/game-logs endpoint - Fix demo mode bug in VersionManifest.ruleMatches() - was incorrectly adding --demo flag - Fix modloader launch: pass proper auth info (accessToken, uuid) from AuthManager - Add game log capture in MinecraftLib and LaunchService
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>me.sashegdev</groupId>
|
||||
<artifactId>ZernMCLauncher</artifactId>
|
||||
<version>1.0.8</version>
|
||||
<version>1.0.9</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zernmc-bootstrap</artifactId>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>me.sashegdev</groupId>
|
||||
<artifactId>ZernMCLauncher</artifactId>
|
||||
<version>1.0.8</version>
|
||||
<version>1.0.9</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zernmclauncher</artifactId>
|
||||
@@ -223,11 +223,24 @@
|
||||
<move file="../../server/builds/zernmc-${project.version}.exe"
|
||||
tofile="../../server/builds/zernmc.exe" overwrite="true"/>
|
||||
|
||||
<!-- Создаём zip с exe, jar и lib -->
|
||||
<!-- Создаем папку bin и копируем JAR -->
|
||||
<mkdir dir="../../server/builds/bin"/>
|
||||
<copy file="../../server/builds/zernmclauncher.jar"
|
||||
tofile="../../server/builds/bin/zernmclauncher.jar" overwrite="true"/>
|
||||
|
||||
<!-- Копируем UI в assets -->
|
||||
<mkdir dir="../../server/builds/assets"/>
|
||||
<copy todir="../../server/builds/assets/ui" overwrite="true">
|
||||
<fileset dir="${project.basedir}/src/resources/ui">
|
||||
<include name="**/*"/>
|
||||
</fileset>
|
||||
</copy>
|
||||
|
||||
<!-- Создаём zip -->
|
||||
<zip destfile="../../server/builds/ZernMC-win-${project.version}.zip"
|
||||
basedir="../../server/builds"
|
||||
includes="zernmc.exe,zernmclauncher.jar,zernmc-bootstrap.jar,lib/**"
|
||||
excludes="build.version,*-${project.version}.*"/>
|
||||
includes="zernmc.exe,bin/**,assets/**,lib/**"
|
||||
excludes="build.version,*-${project.version}.*,zernmclauncher.jar,zernmc-bootstrap.jar"/>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
|
||||
+10
-2
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
+37
-1
@@ -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;
|
||||
|
||||
@@ -45,14 +49,46 @@ 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<String> 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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+35
-3
@@ -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));
|
||||
}
|
||||
|
||||
+10
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
String url = "http://localhost:" + PORT + "/ui/";
|
||||
engine.setOnAlert(e -> log("[UI] Alert: " + e.getData()));
|
||||
|
||||
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<String, Object> 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) {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<div class="account-info">
|
||||
<span id="account-name">-</span>
|
||||
<span id="account-status" class="badge">-</span>
|
||||
<span id="account-role" class="badge role-badge">-</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>me.sashegdev</groupId>
|
||||
<artifactId>ZernMCLauncher</artifactId>
|
||||
<version>1.0.8</version>
|
||||
<version>1.0.9</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>ZernMC Launcher Parent</name>
|
||||
|
||||
+45
-3
@@ -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,6 +1047,9 @@ async def get_launcher_version():
|
||||
@app.get("/launcher/download/jar")
|
||||
async def download_launcher_jar():
|
||||
"""Download launcher JAR file"""
|
||||
# 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():
|
||||
@@ -1015,7 +1057,7 @@ async def download_launcher_jar():
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user