From ec7ef017608f43db1b13e7900d961e5ca5b6efe2 Mon Sep 17 00:00:00 2001 From: SashegDev Date: Sun, 7 Jun 2026 12:32:34 +0000 Subject: [PATCH] =?UTF-8?q?=D0=B8=D0=BD=D0=B8=D0=BC=20=D1=87=D0=B8=D0=BD?= =?UTF-8?q?=D0=B8=D0=BC=20=D1=87=D0=B8=D0=BD=D0=B8=D0=BC=20=D1=87=D0=B8?= =?UTF-8?q?=D0=BD=D0=B8=D0=BC=20=D0=B0=20=D1=82=D0=B0=D0=BA=20=D0=B6=D0=B5?= =?UTF-8?q?=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=81=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B5=D0=BC=D0=B0=20=D0=B4=D1=80=D1=83=D0=B7=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=B8=20=D0=B1=D1=83=D1=82=D1=81=D1=82=D1=80=D0=B0=D0=BF=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 +- .../sashegdev/zernmc/launcher/Bootstrap.java | 256 +++- launcher/launcher/pom.xml | 65 +- .../java/sashegdev/zernmc/launcher/Main.java | 7 +- .../zernmc/launcher/api/LauncherAPI.java | 30 +- .../zernmc/launcher/api/auth/AuthService.java | 5 +- .../launcher/api/launch/LaunchService.java | 22 +- .../zernmc/launcher/auth/AuthManager.java | 155 ++- .../launcher/minecraft/PackDownloader.java | 5 +- .../zernmc/launcher/ui/ArrowMenu.java | 2 +- .../zernmc/launcher/ui/jfx/JFXLauncher.java | 608 ++++++++- .../zernmc/launcher/utils/Config.java | 114 +- .../zernmc/launcher/utils/LauncherLogger.java | 95 ++ .../zernmc/launcher/utils/ZHttpClient.java | 2 - launcher/launcher/src/resources/ui/index.html | 251 ++-- .../launcher/src/resources/ui/launcher.js | 1090 +++++++++++++++-- .../launcher/src/resources/ui/marked.min.js | 69 ++ launcher/launcher/src/resources/ui/style.css | 242 +++- .../launcher/auth/AuthManagerPassTest.java | 274 +++++ server/auth.py | 12 + server/friends.py | 176 +++ server/main.py | 89 +- server/playtime.py | 80 ++ server/tests/test_pass.py | 68 +- test_launcher_integration.py | 382 ++++++ 25 files changed, 3732 insertions(+), 377 deletions(-) create mode 100644 launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/LauncherLogger.java create mode 100644 launcher/launcher/src/resources/ui/marked.min.js create mode 100644 launcher/launcher/src/test/java/me/sashegdev/zernmc/launcher/auth/AuthManagerPassTest.java create mode 100644 server/friends.py create mode 100644 server/playtime.py create mode 100644 test_launcher_integration.py diff --git a/.gitignore b/.gitignore index 6fdf0b1..681cb2d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,12 @@ jre .vscode dependency-reduced-pom.xml OpenJDK21U-jre_x64_windows_hotspot_21.0.6_7.zip -telegram-bot/ \ No newline at end of file +telegram-bot/ +builds/ +server/news/ +data/ +packs/ +.__pycache__ +.pytest_cache +.venv +resources \ No newline at end of file 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 c978f32..56d4aa7 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 @@ -13,6 +13,11 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.swing.*; +import javax.swing.plaf.basic.BasicProgressBarUI; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -32,6 +37,7 @@ public class Bootstrap { private static Path javafxPath; private static boolean isCliMode; private static boolean isJfxMode; + private static BootstrapUI ui; private static Path getLauncherJar() { return binDir.resolve(JAR_NAME); @@ -53,11 +59,17 @@ public class Bootstrap { log("Mode: " + (isCliMode ? "CLI" : "JFX")); + if (!isCliMode && !GraphicsEnvironment.isHeadless()) { + ui = new BootstrapUI(); + SwingUtilities.invokeLater(() -> ui.show()); + } + String currentVersion = readCurrentVersion(); String serverVersion = getServerVersion(); log("Local version: " + currentVersion); log("Server version: " + serverVersion); + setVersionInfo(currentVersion, serverVersion); loadMirrors(); log("Primary server: " + BASE_URL); @@ -74,6 +86,14 @@ public class Bootstrap { log("Shutdown signal received..."); })); + if (ui != null) { + setTitle("Launching..."); + setProgress(100, 100); + log("Starting launcher..."); + try { Thread.sleep(400); } catch (InterruptedException ignored) {} + ui.close(); + } + launchMain(args); } @@ -180,6 +200,19 @@ public class Bootstrap { Files.writeString(logDir.resolve("launcher.log"), entry + "\n", StandardOpenOption.CREATE, StandardOpenOption.APPEND); } catch (Exception ignored) {} + if (ui != null) ui.setStatus(msg); + } + + private static void setProgress(int current, int total) { + if (ui != null) ui.setProgress(current, total); + } + + private static void setVersionInfo(String localVer, String serverVer) { + if (ui != null) ui.setVersionInfo(localVer, serverVer); + } + + private static void setTitle(String text) { + if (ui != null) ui.setTitleText(text); } private static String readCurrentVersion() { @@ -260,6 +293,9 @@ public class Bootstrap { int downloaded = 0; int skipped = 0; + int failed = 0; + + String selfName = getSelfFileName(); for (Map.Entry entry : serverFiles.entrySet()) { String filePath = entry.getKey(); @@ -273,20 +309,46 @@ public class Bootstrap { continue; } + // Skip self-update (can't overwrite running executable) + if (selfName != null && (filePath.equalsIgnoreCase(selfName) || filePath.endsWith("/" + selfName))) { + log("Skipping self-update: " + filePath + " (file in use)"); + skipped++; + continue; + } + if (localHash != null) { log("Updating: " + filePath); } else { log("Downloading: " + filePath); } - downloadFile(newVersion, filePath, serverMeta.size); - downloaded++; + try { + downloadFile(newVersion, filePath, serverMeta.size); + downloaded++; + } catch (Exception e) { + log("Warning: Could not update " + filePath + " - " + e.getMessage()); + failed++; + } } - log("Updated files: " + downloaded + ", skipped: " + skipped); + log("Updated files: " + downloaded + ", skipped: " + skipped + ", failed: " + failed); log("Updated to v" + newVersion); } + private static String getSelfFileName() { + try { + String classPath = Bootstrap.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath(); + if (classPath != null) { + String fn = Paths.get(classPath).getFileName().toString(); + // If running from a JAR, the exe has the same stem + if (fn.endsWith(".jar")) { + return fn.replace(".jar", ".exe"); + } + } + } catch (Exception ignored) {} + return null; + } + private static Map fetchServerMeta(String version) { Map files = new HashMap<>(); try { @@ -390,6 +452,7 @@ public class Bootstrap { long downloaded = 0; long lastUpdate = 0; long startTime = System.currentTimeMillis(); + setTitle("Downloading " + fileName); try (InputStream in = conn.getInputStream(); OutputStream out = new FileOutputStream(outPath.toFile())) { @@ -405,13 +468,9 @@ public class Bootstrap { double downloadedMB = downloaded / 1024.0 / 1024.0; double totalMB = expectedSize / 1024.0 / 1024.0; - System.out.print(String.format("\r[%s] %s - %.1f/%.1f MB (%.1f MB/s", - getProgressBar(downloaded, expectedSize), - fileName, - downloadedMB, - totalMB, - speed - )); + String progressStr = String.format("%.1f/%.1f MB (%.1f MB/s)", downloadedMB, totalMB, speed); + log(progressStr); + setProgress((int) downloaded, (int) Math.max(expectedSize, 1)); lastUpdate = downloaded; } } @@ -419,12 +478,8 @@ public class Bootstrap { long elapsed = System.currentTimeMillis() - startTime; double speed = downloaded / 1024.0 / 1024.0 / (elapsed / 1000.0 + 0.001); - System.out.println(String.format("\r[%s] %s - %.1f MB (%.1f MB/s) - Done!", - getProgressBar(downloaded, expectedSize), - fileName, - downloaded / 1024.0 / 1024.0, - speed - )); + log(String.format("Downloaded %.1f MB (%.1f MB/s) - Done!", downloaded / 1024.0 / 1024.0, speed)); + setProgress((int) downloaded, (int) Math.max(expectedSize, 1)); } private static String getProgressBar(long current, long total) { @@ -488,4 +543,173 @@ public class Bootstrap { return false; } } + + // ====================== SWING UI ====================== + + private static class BootstrapUI { + private final JFrame frame; + private final JLabel statusLabel; + private final JProgressBar progressBar; + private final JLabel titleLabel; + private final JLabel versionLabel; + private final JLabel speedLabel; + private final Color bgColor = new Color(0x0c, 0x0c, 0x12); + private final Color surfaceColor = new Color(0x16, 0x16, 0x1f); + private final Color accentColor = new Color(0xe9, 0x45, 0x60); + private final Color textColor = new Color(0xee, 0xee, 0xf0); + private final Color mutedColor = new Color(0x88, 0x88, 0x9a); + + BootstrapUI() { + try { + UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); + } catch (Exception ignored) {} + + frame = new JFrame("ZernMC Launcher"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setSize(480, 280); + frame.setLocationRelativeTo(null); + frame.setResizable(false); + frame.setBackground(bgColor); + frame.setUndecorated(true); + + JPanel root = new JPanel(new BorderLayout()); + root.setBackground(bgColor); + root.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(0x2a, 0x2a, 0x3a), 1), + BorderFactory.createEmptyBorder(20, 24, 20, 24) + )); + + // Title bar + JPanel titleBar = new JPanel(new BorderLayout()); + titleBar.setOpaque(false); + + JLabel brandLabel = new JLabel("ZernMC Launcher"); + brandLabel.setFont(new Font("Segoe UI", Font.BOLD, 18)); + brandLabel.setForeground(textColor); + + JPanel titleControls = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0)); + titleControls.setOpaque(false); + JButton closeBtn = createTitleButton("\u2715"); + closeBtn.addActionListener(e -> System.exit(0)); + titleControls.add(closeBtn); + + titleBar.add(brandLabel, BorderLayout.WEST); + titleBar.add(titleControls, BorderLayout.EAST); + root.add(titleBar, BorderLayout.NORTH); + + // Center content + JPanel center = new JPanel(); + center.setOpaque(false); + center.setLayout(new BoxLayout(center, BoxLayout.Y_AXIS)); + center.add(Box.createVerticalStrut(16)); + + titleLabel = new JLabel("Initializing..."); + titleLabel.setFont(new Font("Segoe UI", Font.PLAIN, 13)); + titleLabel.setForeground(mutedColor); + titleLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + center.add(titleLabel); + center.add(Box.createVerticalStrut(8)); + + versionLabel = new JLabel(" "); + versionLabel.setFont(new Font("Segoe UI", Font.PLAIN, 12)); + versionLabel.setForeground(mutedColor); + versionLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + center.add(versionLabel); + center.add(Box.createVerticalStrut(16)); + + statusLabel = new JLabel("Starting..."); + statusLabel.setFont(new Font("Segoe UI", Font.PLAIN, 13)); + statusLabel.setForeground(textColor); + statusLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + center.add(statusLabel); + center.add(Box.createVerticalStrut(12)); + + progressBar = new JProgressBar(0, 100); + progressBar.setPreferredSize(new Dimension(400, 6)); + progressBar.setMaximumSize(new Dimension(400, 6)); + progressBar.setAlignmentX(Component.CENTER_ALIGNMENT); + progressBar.setBackground(new Color(0x2a, 0x2a, 0x3a)); + progressBar.setForeground(accentColor); + progressBar.setBorderPainted(false); + progressBar.setValue(0); + progressBar.setUI(new BasicProgressBarUI() { + protected Color getSelectionBackground() { return accentColor; } + protected Color getSelectionForeground() { return accentColor; } + }); + center.add(progressBar); + center.add(Box.createVerticalStrut(6)); + + speedLabel = new JLabel(" "); + speedLabel.setFont(new Font("Segoe UI", Font.PLAIN, 11)); + speedLabel.setForeground(mutedColor); + speedLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + center.add(speedLabel); + + root.add(center, BorderLayout.CENTER); + + // Draggable frame + MouseAdapter dragAdapter = new MouseAdapter() { + private int x, y; + public void mousePressed(MouseEvent e) { x = e.getX(); y = e.getY(); } + public void mouseDragged(MouseEvent e) { + frame.setLocation(e.getXOnScreen() - x, e.getYOnScreen() - y); + } + }; + root.addMouseListener(dragAdapter); + root.addMouseMotionListener(dragAdapter); + + frame.setContentPane(root); + } + + private JButton createTitleButton(String text) { + JButton btn = new JButton(text); + btn.setFont(new Font("Segoe UI", Font.PLAIN, 14)); + btn.setForeground(mutedColor); + btn.setBackground(bgColor); + btn.setBorderPainted(false); + btn.setFocusPainted(false); + btn.setContentAreaFilled(false); + btn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + btn.addMouseListener(new MouseAdapter() { + public void mouseEntered(MouseEvent e) { btn.setForeground(accentColor); } + public void mouseExited(MouseEvent e) { btn.setForeground(mutedColor); } + }); + return btn; + } + + void show() { + frame.setVisible(true); + frame.toFront(); + } + + void close() { + frame.dispose(); + } + + void setStatus(final String text) { + SwingUtilities.invokeLater(() -> statusLabel.setText(text)); + } + + void setProgress(final int current, final int total) { + SwingUtilities.invokeLater(() -> { + if (total > 0) { + int pct = (int) ((long) current * 100 / total); + progressBar.setValue(Math.min(pct, 100)); + speedLabel.setText(String.format("%.1f / %.1f MB", + current / 1024.0 / 1024.0, total / 1024.0 / 1024.0)); + } else { + progressBar.setIndeterminate(true); + } + }); + } + + void setVersionInfo(final String local, final String server) { + SwingUtilities.invokeLater(() -> + versionLabel.setText("v" + local + " \u2192 v" + server)); + } + + void setTitleText(final String text) { + SwingUtilities.invokeLater(() -> titleLabel.setText(text)); + } + } } diff --git a/launcher/launcher/pom.xml b/launcher/launcher/pom.xml index 5af03f8..92ec24c 100644 --- a/launcher/launcher/pom.xml +++ b/launcher/launcher/pom.xml @@ -132,16 +132,17 @@ launch4j-maven-plugin 2.5.0 + - l4j + l4j-gui package launch4j - ../../server/builds/zernmc-${project.version}.exe + ../../server/builds/zernmc.exe ../../server/builds/zernmc-bootstrap.jar - console + gui false me.sashegdev.zernmc.launcher.Bootstrap @@ -157,7 +158,38 @@ ZernMC ZernMC zernmc - zernmc-${project.version}.exe + zernmc.exe + + + + + + + l4j-cli + package + + launch4j + + + ../../server/builds/zernmc-cli.exe + ../../server/builds/zernmc-bootstrap.jar + console + false + me.sashegdev.zernmc.launcher.Bootstrap + + lib/jre21 + 21 + + + ${project.version}.0 + ${project.version} + ZernMC Launcher CLI + ${project.version}.0 + ${project.version} + ZernMC CLI + ZernMC + zernmc-cli + zernmc-cli.exe @@ -219,10 +251,6 @@ - - - - + + +ZernMC Launcher + +Files: +- zernmc.exe - Main launcher with GUI (no console window) +- zernmc-cli.exe - CLI version for servers/advanced users (with console) + +How to use GUI: + Just run zernmc.exe + +How to use CLI: + Run from command line: zernmc-cli.exe --cli + + + + includes="zernmc.exe,zernmc-cli.exe,bin/**,assets/**,lib/**,README.txt" + excludes="build.version,*.jar"/> diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/Main.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/Main.java index 540585b..829723e 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/Main.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/Main.java @@ -21,7 +21,9 @@ public class Main { System.setProperty("java.stdout.encoding", "UTF-8"); System.setProperty("java.stderr.encoding", "UTF-8"); System.setProperty("org.jline.terminal.disableDeprecatedProviderWarning", "true"); - + + LauncherLogger.init(); + if (System.getProperty("os.name").toLowerCase().contains("windows")) { try { new ProcessBuilder("cmd", "/c", "chcp", "65001").inheritIO().start().waitFor(); @@ -29,8 +31,7 @@ public class Main { } ZAnsi.install(); - System.out.print("\033[H\033[2J"); - System.out.println(ZAnsi.brightGreen("Welcome to ZernMC Launcher " + CURRENT_VERSION)); + LauncherLogger.info("Starting ZernMC Launcher " + CURRENT_VERSION); List argList = List.of(args); boolean jfxMode = argList.contains("--jfx"); diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/LauncherAPI.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/LauncherAPI.java index 1c76188..934f67c 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/LauncherAPI.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/api/LauncherAPI.java @@ -3,6 +3,7 @@ package me.sashegdev.zernmc.launcher.api; import me.sashegdev.zernmc.launcher.api.auth.AuthService; import me.sashegdev.zernmc.launcher.api.instance.InstanceService; import me.sashegdev.zernmc.launcher.api.launch.LaunchService; +import me.sashegdev.zernmc.launcher.utils.LauncherLogger; import me.sashegdev.zernmc.launcher.utils.ZHttpClient; import java.util.ArrayList; @@ -153,26 +154,31 @@ public class LauncherAPI { try { String token = authService.getCurrentToken(); if (token == null) { + LauncherLogger.warn("getZernMCPacks: not logged in"); return ApiResponse.error("Not logged in"); } String response = ZHttpClient.get("/packs"); - org.json.JSONArray arr = new org.json.JSONArray(response); + org.json.JSONObject root = new org.json.JSONObject(response); + org.json.JSONArray arr = root.optJSONArray("packs"); List> packs = new ArrayList<>(); - for (int i = 0; i < arr.length(); i++) { - org.json.JSONObject pack = arr.getJSONObject(i); - Map packInfo = new java.util.HashMap<>(); - packInfo.put("name", pack.optString("name", "")); - packInfo.put("displayName", pack.optString("displayName", pack.optString("name", ""))); - packInfo.put("version", pack.optString("version", "")); - packInfo.put("mcVersion", pack.optString("mcVersion", "")); - packInfo.put("loader", pack.optString("loader", "vanilla")); - packInfo.put("description", pack.optString("description", "")); - packs.add(packInfo); + if (arr != null) { + for (int i = 0; i < arr.length(); i++) { + org.json.JSONObject pack = arr.getJSONObject(i); + Map packInfo = new java.util.HashMap<>(); + packInfo.put("name", pack.optString("name", "")); + packInfo.put("displayName", pack.optString("displayName", pack.optString("name", ""))); + packInfo.put("version", pack.optString("version", "")); + packInfo.put("mcVersion", pack.optString("minecraft_version", "")); + packInfo.put("loader", pack.optString("loader_type", "vanilla")); + packInfo.put("description", pack.optString("description", "")); + packs.add(packInfo); + } } + LauncherLogger.info("getZernMCPacks: loaded " + packs.size() + " packs"); return ApiResponse.success(packs); } catch (Exception e) { - System.out.println("[API] Packs fetch failed: " + e.getMessage()); + LauncherLogger.error("getZernMCPacks failed: " + e.getMessage()); return ApiResponse.error("Failed to load packs: " + e.getMessage()); } } 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 4a0cd0b..4a49672 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 @@ -1,5 +1,7 @@ package me.sashegdev.zernmc.launcher.api.auth; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import me.sashegdev.zernmc.launcher.api.ApiResponse; import me.sashegdev.zernmc.launcher.auth.AuthManager; import me.sashegdev.zernmc.launcher.utils.ZHttpClient; @@ -73,7 +75,8 @@ public class AuthService { public ApiResponse activatePass(String passCode) { try { String response = post("/auth/pass/activate", - "{\"code\":\"" + passCode + "\"}"); + "{\"pass_code\":\"" + passCode + "\"}"); + AuthManager.refreshUserInfo(); return ApiResponse.success(true); } catch (Exception e) { return ApiResponse.error("Pass activation error: " + e.getMessage()); 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 ddd5868..8a4e695 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 @@ -8,6 +8,7 @@ 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.Config; +import me.sashegdev.zernmc.launcher.utils.LauncherLogger; import java.io.BufferedReader; import java.io.IOException; @@ -39,6 +40,7 @@ public class LaunchService { return ApiResponse.error("Pack not found: " + instanceName); } + LauncherLogger.info("Preparing launch for: " + instanceName); LaunchCommandBuilder builder = new LaunchCommandBuilder(instance); LaunchOptions options = createOptions(); @@ -51,6 +53,7 @@ public class LaunchService { ); return ApiResponse.success(info); } catch (Exception e) { + LauncherLogger.error("Error preparing launch for " + instanceName, e); return ApiResponse.error("Error preparing launch: " + e.getMessage()); } } @@ -62,6 +65,8 @@ public class LaunchService { return ApiResponse.error("Pack not found: " + instanceName); } + LauncherLogger.info("Launching: " + instanceName + " (serverPack=" + instance.isServerPack() + ")"); + LaunchCommandBuilder builder = new LaunchCommandBuilder(instance); LaunchOptions options = createOptions(); options.setUsername(AuthManager.getUsername()); @@ -69,8 +74,8 @@ public class LaunchService { options.setUuid(AuthManager.getUuid()); List command = builder.build(options); - System.out.println("[LAUNCH] Generated command for " + instanceName + ":"); - command.forEach(arg -> System.out.println(" " + arg)); + LauncherLogger.info("Generated command for " + instanceName + ":"); + command.forEach(arg -> LauncherLogger.debug(" " + arg)); ProcessBuilder processBuilder = new ProcessBuilder(command); processBuilder.directory(instance.getPath().toFile()); @@ -84,7 +89,7 @@ public class LaunchService { long pid = process.pid(); runningProcesses.put(pid, process); - System.out.println("[LAUNCH] Process started, pid=" + pid); + LauncherLogger.info("Process started, pid=" + pid); java.io.FileOutputStream logFileOut = new java.io.FileOutputStream(gameLog.toFile(), true); @@ -116,6 +121,7 @@ public class LaunchService { ProcessInfo info = new ProcessInfo(instanceName, pid, "RUNNING"); return ApiResponse.success(info); } catch (Exception e) { + LauncherLogger.error("Launch error for " + instanceName, e); return ApiResponse.error("Launch error: " + e.getMessage()); } } @@ -171,15 +177,21 @@ public class LaunchService { options.setWidth(Config.getWindowWidth()); options.setHeight(Config.getWindowHeight()); options.setJavaPath(Config.getJavaPath()); + List extraArgs = new ArrayList<>(); + if (Config.isSystemBasedJvm()) { + String[] systemFlags = Config.getSystemJvmFlags().split("\\s+"); + for (String arg : systemFlags) { + if (!arg.isEmpty()) extraArgs.add(arg); + } + } String args = Config.getExtraJvmArgs(); if (args != null && !args.isEmpty()) { - List extraArgs = new ArrayList<>(); for (String arg : args.split("\\s+")) { arg = arg.trim(); if (!arg.isEmpty()) extraArgs.add(arg); } - options.setExtraJvmArgs(extraArgs); } + options.setExtraJvmArgs(extraArgs); return options; } 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 c5012ff..c568fcd 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 @@ -6,6 +6,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.annotations.SerializedName; import me.sashegdev.zernmc.launcher.utils.Config; +import me.sashegdev.zernmc.launcher.utils.LauncherLogger; import me.sashegdev.zernmc.launcher.utils.ZAnsi; import me.sashegdev.zernmc.launcher.utils.ZHttpClient; @@ -36,24 +37,57 @@ public class AuthManager { public static final String PERM_DOWNLOAD_PACK = "download_pack"; public static boolean loadSavedSession() { - if (!Files.exists(AUTH_FILE)) return false; + if (!Files.exists(AUTH_FILE)) { + LauncherLogger.warn("loadSavedSession: auth.json not found at " + AUTH_FILE); + return false; + } try { String json = Files.readString(AUTH_FILE); AuthSession loaded = GSON.fromJson(json, AuthSession.class); - if (loaded == null || loaded.accessToken == null) return false; + if (loaded == null || loaded.accessToken == null) { + LauncherLogger.warn("loadSavedSession: invalid auth.json content, deleting"); + Files.deleteIfExists(AUTH_FILE); + return false; + } session = loaded; - userInfo = fetchUserInfo(); + LauncherLogger.info("loadSavedSession: loaded session for " + loaded.username + + " expiresAt=" + loaded.expiresAt + " hasRefresh=" + (loaded.refreshToken != null)); + + refreshUserInfo(); if (isAccessTokenExpired()) { - return tryRefresh(); + LauncherLogger.info("loadSavedSession: token expired, attempting refresh"); + boolean refreshed = tryRefresh(); + if (!refreshed) { + if (session == null) { + LauncherLogger.warn("loadSavedSession: token rejected by server (401)"); + return false; + } + LauncherLogger.warn("loadSavedSession: refresh failed (network/no refreshToken)," + + " keeping session for retry on next launch"); + return false; + } } + if (session == null) { + LauncherLogger.warn("loadSavedSession: session invalidated during token refresh"); + return false; + } + LauncherLogger.info("loadSavedSession: session valid for " + session.username); return true; } catch (Exception e) { + LauncherLogger.error("loadSavedSession error: " + e.getMessage()); + invalidateSession(); return false; } } + public static boolean tryAutoLogin() { + if (isLoggedIn()) return true; + if (!Files.exists(AUTH_FILE)) return false; + return loadSavedSession(); + } + public static AuthResult login(String username, String password) { return authRequest("/auth/login", username, password); } @@ -70,6 +104,8 @@ public class AuthManager { if (resp.statusCode() == 200) { session = GSON.fromJson(resp.body(), AuthSession.class); session.expiresAt = System.currentTimeMillis() / 1000L + session.expiresIn; + LauncherLogger.info("authRequest: login successful, expiresAt=" + session.expiresAt + + " hasRefresh=" + (session.refreshToken != null)); saveSession(); userInfo = fetchUserInfo(); return AuthResult.ok(); @@ -99,6 +135,10 @@ public class AuthManager { return session != null && session.accessToken != null; } + public static boolean authFileExists() { + return Files.exists(AUTH_FILE); + } + public static String getUsername() { return session != null ? session.username : "Player"; } @@ -110,7 +150,11 @@ public class AuthManager { public static String getAccessToken() { if (session == null) return "0"; if (isAccessTokenExpired()) { - tryRefresh(); + boolean refreshed = tryRefresh(); + if (!refreshed) { + if (session == null) return "0"; + return session.accessToken != null ? session.accessToken : "0"; + } } return session != null && session.accessToken != null ? session.accessToken : "0"; } @@ -121,7 +165,14 @@ public class AuthManager { } private static boolean tryRefresh() { - if (session == null || session.refreshToken == null) return false; + if (session == null) { + LauncherLogger.warn("tryRefresh: session is null"); + return false; + } + if (session.refreshToken == null) { + LauncherLogger.warn("tryRefresh: no refreshToken in session"); + return false; + } try { String body = "{\"refresh_token\":\"" + session.refreshToken + "\"}"; SimpleHttpResponse resp = post("/auth/refresh", body); @@ -131,22 +182,45 @@ public class AuthManager { newSession.expiresAt = System.currentTimeMillis() / 1000L + newSession.expiresIn; session = newSession; userInfo = fetchUserInfo(); + if (userInfo != null) { + session.role = userInfo.role; + } saveSession(); + LauncherLogger.info("tryRefresh: token refreshed successfully"); return true; } - } catch (Exception ignored) {} + + if (resp.statusCode() == 401) { + LauncherLogger.warn("tryRefresh: server rejected refresh token (401)"); + invalidateSession(); + } else { + LauncherLogger.warn("tryRefresh: server returned " + resp.statusCode()); + } + } catch (Exception e) { + LauncherLogger.warn("tryRefresh: network error: " + e.getMessage()); + return false; + } + return false; + } + + private static void invalidateSession() { session = null; userInfo = null; - try { Files.deleteIfExists(AUTH_FILE); } catch (Exception ignored) {} - return false; + try { + Files.deleteIfExists(AUTH_FILE); + LauncherLogger.info("Session invalidated, auth.json deleted"); + } catch (Exception e) { + LauncherLogger.error("Failed to delete auth.json", e); + } } private static void saveSession() { try { Files.createDirectories(AUTH_FILE.getParent()); Files.writeString(AUTH_FILE, GSON.toJson(session)); + LauncherLogger.info("Session saved to " + AUTH_FILE); } catch (IOException e) { - System.err.println(ZAnsi.yellow("Failed to save session: " + e.getMessage())); + LauncherLogger.error("Failed to save session", e); } } @@ -180,14 +254,25 @@ public class AuthManager { if (conn != null) conn.disconnect(); } } catch (Exception e) { - System.err.println("Failed to get UserInfo: " + e.getMessage()); + LauncherLogger.warn("Failed to get UserInfo: " + e.getMessage()); return null; } } public static boolean hasPass() { + if (!isLoggedIn()) return false; if (userInfo != null) return userInfo.has_pass; - return getRole() >= ROLE_PASS_HOLDER; + if (getRole() >= ROLE_PASS_HOLDER) return true; + try { + String response = ZHttpClient.get("/auth/pass/my"); + JsonObject json = JsonParser.parseString(response).getAsJsonObject(); + if (json.has("has_active")) { + return json.get("has_active").getAsBoolean(); + } + } catch (Exception e) { + LauncherLogger.warn("Failed to check pass: " + e.getMessage()); + } + return false; } public static boolean canViewPacks() { @@ -277,16 +362,30 @@ public class AuthManager { return body.length() > 200 ? body.substring(0, 200) + "..." : body; } + public static void updateRole(int newRole) { + if (session != null) { + session.role = newRole; + saveSession(); + } + refreshUserInfo(); + } + + public static void refreshUserInfo() { + UserInfo fresh = fetchUserInfo(); + if (fresh != null) { + userInfo = fresh; + if (session != null) { + session.role = fresh.role; + } + } + if (session != null) { + saveSession(); + } + } + public static boolean hasActivePass() { if (!isLoggedIn()) return false; - try { - String response = ZHttpClient.get("/auth/pass/my"); - JsonObject json = JsonParser.parseString(response).getAsJsonObject(); - return json.has("has_active") && json.get("has_active").getAsBoolean(); - } catch (Exception e) { - System.err.println(ZAnsi.red("Failed to check pass: ") + e.getMessage()); - return false; - } + return hasPass(); } public static String getPassStatus() { @@ -305,7 +404,7 @@ public class AuthManager { @SerializedName("access_token") public String accessToken; @SerializedName("refresh_token") public String refreshToken; @SerializedName("expires_in") public int expiresIn; - public transient long expiresAt; + public long expiresAt; public String username; public String uuid; public int role; @@ -341,6 +440,20 @@ public class AuthManager { public static AuthResult ok() { return new AuthResult(true, null); } public static AuthResult fail(String msg) { return new AuthResult(false, msg); } } + + // === TEST HELPERS === + static void resetForTest() { + session = null; + userInfo = null; + } + + static void setTestSession(AuthSession s) { + session = s; + } + + static void setTestUserInfo(UserInfo u) { + userInfo = u; + } } class SimpleHttpResponse { diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/PackDownloader.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/PackDownloader.java index d063ff8..fae0515 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/PackDownloader.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/PackDownloader.java @@ -8,6 +8,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import me.sashegdev.zernmc.launcher.auth.AuthManager; +import me.sashegdev.zernmc.launcher.utils.LauncherLogger; import me.sashegdev.zernmc.launcher.utils.ProgressBar; import me.sashegdev.zernmc.launcher.utils.ZAnsi; import me.sashegdev.zernmc.launcher.utils.ZHttpClient; @@ -118,7 +119,7 @@ public class PackDownloader { result.add(new ServerPack(name, version, minecraftVersion, loaderType, loaderVersion, updatedAt, filesCount)); } catch (Exception e) { - System.err.println("Error parsing pack: " + e.getMessage()); + LauncherLogger.warn("Error parsing pack: " + e.getMessage()); } } @@ -137,7 +138,7 @@ public class PackDownloader { * Install or update a pack from the server */ public boolean installOrUpdatePack(String packName, ServerPack serverPack) throws Exception { - System.out.println(ZAnsi.cyan("Installing pack " + packName + " from server...")); + LauncherLogger.info("Installing pack " + packName + " from server..."); // 1. Get manifest PackManifest manifest = getPackManifest(packName); 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 45ce4b1..866214e 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 @@ -59,7 +59,7 @@ public class ArrowMenu { if (next == -1) { return -1; } - if (next == 0x5B) { // '[' + if (next == 0x5B || next == 0x4F) { // '[' (CSI) or 'O' (SS3) int arrow = nonBlockingRead(); if (arrow == 0x41) { // Up selected = (selected - 1 + options.size()) % options.size(); 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 3ab3d6a..19b36c8 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 @@ -14,14 +14,20 @@ import me.sashegdev.zernmc.launcher.auth.AuthManager; import me.sashegdev.zernmc.launcher.minecraft.Instance; import me.sashegdev.zernmc.launcher.minecraft.InstanceManager; import me.sashegdev.zernmc.launcher.minecraft.MinecraftLib; +import me.sashegdev.zernmc.launcher.minecraft.PackDownloader; +import me.sashegdev.zernmc.launcher.minecraft.ServerPack; import me.sashegdev.zernmc.launcher.minecraft.launch.LaunchCommandBuilder; import me.sashegdev.zernmc.launcher.utils.Config; +import me.sashegdev.zernmc.launcher.utils.LauncherLogger; +import java.awt.Desktop; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; +import me.sashegdev.zernmc.launcher.utils.ZHttpClient; import java.net.URL; import java.util.jar.JarEntry; import java.util.jar.JarFile; @@ -33,7 +39,9 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import com.sun.net.httpserver.HttpServer; @@ -41,7 +49,7 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.Headers; public class JFXLauncher extends Application { - private static final int PORT = 8080; + private static int PORT = 8080; private static final String APP_TITLE = "ZernMC Launcher"; private static final String LAUNCHER_SERVER = System.getProperty("launcher.server", "http://87.120.187.36:1582"); private final LauncherAPI api = new LauncherAPI(); @@ -57,6 +65,9 @@ public class JFXLauncher extends Application { private static volatile int installProgressCurrent = 0; private static volatile int installProgressTotal = 100; private static volatile boolean installInProgress = false; + private static volatile String installStageName = ""; + private static volatile int installStageIndex = 0; + private static volatile int installStageCount = 1; private static final java.util.concurrent.CopyOnWriteArrayList logConsumers = new java.util.concurrent.CopyOnWriteArrayList<>(); private static final java.util.concurrent.CopyOnWriteArrayList gameLogConsumers = new java.util.concurrent.CopyOnWriteArrayList<>(); @@ -86,6 +97,21 @@ public class JFXLauncher extends Application { installProgressCurrent = current; installProgressTotal = total; } + + public static void setInstallStage(String stageName, int stageIndex, int stageCount) { + installStageName = stageName; + installStageIndex = stageIndex; + installStageCount = stageCount; + } + + public static void setInstallProgressWithStage(String label, int current, int total, String stageName, int stageIndex, int stageCount) { + installProgressLabel = label; + installProgressCurrent = current; + installProgressTotal = total; + installStageName = stageName; + installStageIndex = stageIndex; + installStageCount = stageCount; + } public static void setInstallInProgress(boolean inProgress) { installInProgress = inProgress; @@ -95,30 +121,31 @@ public class JFXLauncher extends Application { return installInProgress; } - public static void appendLauncherLog(String log) { + public static void appendLauncherLog(String msg) { synchronized (launcherLogBuffer) { - launcherLogBuffer.append(log).append("\n"); + launcherLogBuffer.append(msg).append("\n"); } + LauncherLogger.info("[EXT] " + msg); for (LogConsumer consumer : logConsumers) { - try { consumer.onLog(log); } catch (Exception ignored) {} + try { consumer.onLog(msg); } catch (Exception ignored) {} } } - public static void appendGameLog(String log) { - System.out.println("[GAME] " + log); + public static void appendGameLog(String msg) { synchronized (gameLogBuffer) { - gameLogBuffer.append(log).append("\n"); + gameLogBuffer.append(msg).append("\n"); if (gameLogFile != null) { try { String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")); - Files.writeString(gameLogFile, "[" + timestamp + "] " + log + "\n", + Files.writeString(gameLogFile, "[" + timestamp + "] " + msg + "\n", StandardOpenOption.CREATE, StandardOpenOption.APPEND); } catch (Exception ignored) {} } } + LauncherLogger.info("[GAME] " + msg); for (LogConsumer consumer : gameLogConsumers) { - try { consumer.onLog(log); } catch (Exception ignored) {} + try { consumer.onLog(msg); } catch (Exception ignored) {} } } @@ -281,18 +308,37 @@ public class JFXLauncher extends Application { this.mainStage = stage; try { - // Initialize launcher log file - Path logsDir = Paths.get("logs"); - Files.createDirectories(logsDir); - launcherLogFile = logsDir.resolve("launcher.log"); - Files.writeString(launcherLogFile, "=== Launcher Log " + LocalDateTime.now() + " ===\n", - StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + launcherLogFile = LauncherLogger.getLogFile(); extractAssets(); log("Starting JFX UI..."); + log("Loading saved session..."); + boolean sessionLoaded = false; + try { + sessionLoaded = AuthManager.loadSavedSession(); + } catch (Exception e) { + log("Session load error: " + e.getMessage()); + } + if (sessionLoaded) { + log("Session loaded for: " + AuthManager.getUsername()); + log("Pass active: " + AuthManager.hasActivePass()); + log("Role: " + AuthManager.getRole() + " (" + AuthManager.getRoleName() + ")"); + } else { + log("No saved session found, will show login screen"); + } + log("Starting background network init..."); + new Thread(() -> { + try { + ZHttpClient.checkAllServicesOnStartup(); + log("Network init complete"); + } catch (Exception e) { + log("Network init warning: " + e.getMessage()); + } + }, "network-init").start(); startServer(); WebView webView = new WebView(); + webView.setContextMenuEnabled(false); WebEngine engine = webView.getEngine(); engine.setJavaScriptEnabled(true); @@ -305,14 +351,67 @@ public class JFXLauncher extends Application { } }); - engine.setOnAlert(e -> log("[UI] Alert: " + e.getData())); + // Capture JS console.log/error via alert bridge + engine.setOnAlert(e -> { + String msg = e.getData(); + if (msg.startsWith("[LOG] ")) { + System.out.println("[JS] " + msg.substring(6)); + } else if (msg.startsWith("[ERR] ")) { + System.err.println("[JS] " + msg.substring(6)); + } else { + log("[UI] Alert: " + msg); + } + }); + + engine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> { + if (newState == Worker.State.SUCCEEDED) { + // Override console.log/error to use alert bridge + engine.executeScript( + "if (!window._consoleOverridden) {" + + " window._consoleOverridden = true;" + + " var _origLog = console.log;" + + " var _origErr = console.error;" + + " console.log = function() {" + + " var args = Array.prototype.slice.call(arguments).map(function(a){return typeof a==='object'?JSON.stringify(a):String(a)});" + + " alert('[LOG] ' + args.join(' '));" + + " _origLog.apply(console, arguments);" + + " };" + + " console.error = function() {" + + " var args = Array.prototype.slice.call(arguments).map(function(a){return typeof a==='object'?JSON.stringify(a):String(a)});" + + " alert('[ERR] ' + args.join(' '));" + + " _origErr.apply(console, arguments);" + + " };" + + "}" + ); + engine.executeScript( + "window.open = function(url, name, features) {" + + " var req = new XMLHttpRequest();" + + " req.open('POST', '/api/open-url', false);" + + " req.send(JSON.stringify({url: url}));" + + " return window;" + + "};" + + "document.addEventListener('click', function(e) {" + + " var target = e.target;" + + " while (target && target.tagName !== 'A') target = target.parentNode;" + + " if (target && target.href && target.href !== '#' && !target.href.startsWith('http://localhost:" + PORT + "/')) {" + + " e.preventDefault();" + + " var req = new XMLHttpRequest();" + + " req.open('POST', '/api/open-url', false);" + + " req.send(JSON.stringify({url: target.href}));" + + " }" + + "}, true);" + ); + } + }); String url = "http://localhost:" + PORT + "/assets/ui/index.html"; engine.load(url); stage.setTitle(APP_TITLE); - stage.setWidth(1280); - stage.setHeight(800); + int winW = Config.getWindowWidth(); + int winH = Config.getWindowHeight(); + stage.setWidth(Math.max(winW, 800)); + stage.setHeight(Math.max(winH, 600)); stage.setMinWidth(800); stage.setMinHeight(600); stage.setScene(new Scene(webView)); @@ -335,7 +434,16 @@ public class JFXLauncher extends Application { } private void startServer() throws Exception { - server = HttpServer.create(new InetSocketAddress("localhost", PORT), 0); + int maxPortAttempts = 20; + for (int port = PORT; port < PORT + maxPortAttempts; port++) { + try { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0); + PORT = port; + break; + } catch (java.net.BindException e) { + if (port == PORT + maxPortAttempts - 1) throw e; + } + } server.createContext("/api/login", this::handleLogin); server.createContext("/api/auto-login", this::handleAutoLogin); @@ -354,9 +462,22 @@ public class JFXLauncher extends Application { server.createContext("/api/settings", this::handleSettings); server.createContext("/api/activate-pass", this::handleActivatePass); server.createContext("/api/register", this::handleRegister); + server.createContext("/api/pack-info", this::handlePackInfo); server.createContext("/api/shutdown", this::handleShutdown); server.createContext("/api/exit", this::handleExit); + server.createContext("/api/news", this::handleNews); + server.createContext("/api/open-url", this::handleOpenUrl); server.createContext("/api/exit-parent", this::handleExitParent); + server.createContext("/api/system-info", this::handleSystemInfo); + server.createContext("/api/open-log-file", this::handleOpenLogFile); + server.createContext("/api/friends/list", this::handleFriendList); + server.createContext("/api/friends/add", this::handleFriendAdd); + server.createContext("/api/friends/remove", this::handleFriendRemove); + server.createContext("/api/friends/accept", this::handleFriendAccept); + server.createContext("/api/friends/requests", this::handleFriendRequests); + server.createContext("/api/friends/status", this::handleFriendStatus); + server.createContext("/api/playtime/sync", this::handlePlaytimeSync); + server.createContext("/api/playtime/stats", this::handlePlaytimeStats); server.createContext("/assets/", this::handleStatic); server.setExecutor(Executors.newCachedThreadPool()); @@ -369,8 +490,51 @@ public class JFXLauncher extends Application { if (server != null) server.stop(0); } + private void handleSystemInfo(HttpExchange exchange) { + try { + Map info = new HashMap<>(); + info.put("cpuCores", Config.getSystemCpuCores()); + info.put("totalRamMB", Config.getSystemTotalRamMB()); + info.put("systemJvmFlags", Config.getSystemJvmFlags()); + sendJson(exchange, Map.of("success", true, "data", info)); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + + private void handleOpenLogFile(HttpExchange exchange) { + try { + if (gameLogFile != null && Files.exists(gameLogFile)) { + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(gameLogFile.toFile()); + sendJson(exchange, Map.of("success", true)); + } else { + sendJson(exchange, Map.of("success", false, "error", "Desktop not supported")); + } + } else { + sendJson(exchange, Map.of("success", false, "error", "No game log file found")); + } + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + + private void handleOpenUrl(HttpExchange exchange) { + try { + Map body = parseJson(exchange.getRequestBody()); + String url = body.get("url"); + if (url != null && !url.isEmpty()) { + openInBrowser(url); + } + sendJson(exchange, Map.of("success", true)); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + private void handleLogin(HttpExchange exchange) { try { + log("handleLogin: " + exchange.getRequestMethod() + " from " + exchange.getRemoteAddress()); if (!"POST".equals(exchange.getRequestMethod())) { sendJson(exchange, Map.of("success", false, "error", "Method not supported")); return; @@ -385,19 +549,27 @@ public class JFXLauncher extends Application { Map data = new HashMap<>(); data.put("username", result.getData().getUsername()); data.put("token", result.getData().getToken()); + data.put("passActive", AuthManager.hasActivePass()); + data.put("role", AuthManager.getRole()); + data.put("roleName", AuthManager.getRoleName()); sendJson(exchange, Map.of("success", true, "data", data)); - log("Login: " + username); + log("Login success: " + username); } else { + log("Login failed: " + result.getError()); sendJson(exchange, Map.of("success", false, "error", result.getError())); } } catch (Exception e) { + log("Login error: " + e.getMessage()); + e.printStackTrace(); sendJson(exchange, Map.of("success", false, "error", e.getMessage())); } } private void handleAccount(HttpExchange exchange) { try { + log("handleAccount: isLoggedIn=" + api.isLoggedIn()); if (!api.isLoggedIn()) { + log("handleAccount: not authenticated"); sendJson(exchange, Map.of("success", false, "error", "Not authenticated")); return; } @@ -414,18 +586,27 @@ public class JFXLauncher extends Application { private void handleAutoLogin(HttpExchange exchange) { try { - if (AuthManager.loadSavedSession()) { + boolean loggedIn = AuthManager.isLoggedIn(); + boolean authExists = AuthManager.authFileExists(); + System.out.println("[AUTH] handleAutoLogin: isLoggedIn=" + loggedIn + " authFile=" + authExists); + log("handleAutoLogin: isLoggedIn=" + loggedIn + " authFile=" + authExists); + if (AuthManager.tryAutoLogin()) { Map data = new HashMap<>(); data.put("username", AuthManager.getUsername()); data.put("passActive", AuthManager.hasActivePass()); data.put("role", AuthManager.getRole()); data.put("roleName", AuthManager.getRoleName()); sendJson(exchange, Map.of("success", true, "data", data, "autoLogin", true)); + System.out.println("[AUTH] Auto-login performed: " + AuthManager.getUsername()); log("Auto-login performed: " + AuthManager.getUsername()); } else { + System.out.println("[AUTH] handleAutoLogin: no valid session (authFile=" + authExists + ")"); + log("handleAutoLogin: no valid session (authFile=" + authExists + ")"); sendJson(exchange, Map.of("success", false, "autoLogin", false)); } } catch (Exception e) { + System.out.println("[AUTH] handleAutoLogin error: " + e.getMessage()); + log("handleAutoLogin error: " + e.getMessage()); sendJson(exchange, Map.of("success", false, "error", e.getMessage())); } } @@ -461,7 +642,11 @@ public class JFXLauncher extends Application { sendJson(exchange, Map.of("success", false, "error", "Pack not found")); return; } + + log("handleLaunch: " + name + " isServerPack=" + instance.isServerPack() + " hasPass=" + AuthManager.hasActivePass() + " isLoggedIn=" + api.isLoggedIn()); + if (instance.isServerPack() && !AuthManager.hasActivePass()) { + log("handleLaunch: server pack requires active pass"); sendJson(exchange, Map.of("success", false, "error", "Server pack requires an active pass")); return; } @@ -472,11 +657,13 @@ public class JFXLauncher extends Application { data.put("pid", result.getData().getPid()); data.put("status", result.getData().getStatus()); sendJson(exchange, Map.of("success", true, "data", data)); - log("Launched: " + name); + log("Launched: " + name + " pid=" + result.getData().getPid()); } else { + log("Launch failed: " + result.getError()); sendJson(exchange, Map.of("success", false, "error", result.getError())); } } catch (Exception e) { + log("handleLaunch error: " + e.getMessage()); sendJson(exchange, Map.of("success", false, "error", e.getMessage())); } } @@ -509,37 +696,67 @@ public class JFXLauncher extends Application { Instance instance = InstanceManager.getInstance(name); if (instance != null) { - instance.setMinecraftVersion(version); - instance.setLoaderType(loader); - if (loaderVersion != null) { - instance.setLoaderVersion(loaderVersion); + if (!"zernmc".equalsIgnoreCase(loader)) { + instance.setMinecraftVersion(version); + instance.setLoaderType(loader); + if (loaderVersion != null) { + instance.setLoaderVersion(loaderVersion); + } } sendJson(exchange, Map.of("success", true, "message", "Installation started")); setInstallInProgress(true); - setInstallProgress("Preparing...", 0, 100); + setInstallProgressWithStage("Preparing...", 0, 100, "Preparing", 0, 4); Thread installThread = new Thread(() -> { try { - MinecraftLib lib = new MinecraftLib(instance); - boolean success = false; - if ("vanilla".equalsIgnoreCase(loader)) { - success = lib.installMinecraft(version); + + if ("zernmc".equalsIgnoreCase(loader)) { + setInstallProgressWithStage("Fetching pack info...", 10, 100, "Fetching pack info", 1, 4); + PackDownloader downloader = new PackDownloader(instance); + var packs = downloader.getAvailablePacks(); + ServerPack pack = null; + for (ServerPack p : packs) { + if (p.getName().equals(version)) { + pack = p; + break; + } + } + if (pack != null) { + setInstallProgressWithStage("Downloading pack files...", 30, 100, "Downloading", 2, 4); + success = downloader.installOrUpdatePack(version, pack); + if (success) { + setInstallProgressWithStage("Finalizing...", 90, 100, "Finalizing", 3, 4); + } + } else { + log("Install error: pack not found on server: " + version + " (available: " + (packs != null ? packs.size() : 0) + ")"); + } } else { - success = lib.installPack(name, version, loader, loaderVersion != null ? loaderVersion : ""); + MinecraftLib lib = new MinecraftLib(instance); + setInstallProgressWithStage("Installing Minecraft...", 20, 100, "Installing Minecraft", 1, 3); + if ("vanilla".equalsIgnoreCase(loader)) { + success = lib.installMinecraft(version); + } else { + setInstallProgressWithStage("Installing loader...", 30, 100, "Installing loader", 1, 3); + success = lib.installPack(name, version, loader, loaderVersion != null ? loaderVersion : ""); + } + setInstallProgressWithStage("Finalizing...", 90, 100, "Finalizing", 2, 3); } setInstallInProgress(false); if (success) { + setInstallProgressWithStage("Installation complete!", 100, 100, "Done", 0, 1); log("Installed: " + name); } else { + setInstallProgressWithStage("Installation failed", 0, 100, "Failed", 0, 1); log("Install error: " + name); } } catch (Exception e) { log("Install error: " + e.getMessage()); setInstallInProgress(false); + setInstallProgressWithStage("Error: " + e.getMessage(), 0, 100, "Error", 0, 1); } }); installThread.setDaemon(true); @@ -561,6 +778,9 @@ public class JFXLauncher extends Application { progress.put("current", installProgressCurrent); progress.put("total", installProgressTotal); progress.put("inProgress", installInProgress); + progress.put("stageName", installStageName); + progress.put("stageIndex", installStageIndex); + progress.put("stageCount", Math.max(installStageCount, 1)); if (installInProgress && installProgressTotal > 0) { progress.put("percent", (int) ((installProgressCurrent * 100.0) / installProgressTotal)); @@ -579,6 +799,7 @@ public class JFXLauncher extends Application { } private void handleLogsStream(HttpExchange exchange) { + final LogConsumer[] consumerRef = new LogConsumer[1]; try { exchange.getResponseHeaders().set("Content-Type", "text/event-stream"); exchange.getResponseHeaders().set("Cache-Control", "no-cache"); @@ -586,7 +807,6 @@ public class JFXLauncher extends Application { exchange.sendResponseHeaders(200, 0); final OutputStream os = exchange.getResponseBody(); - int[] lastLength = {getLauncherLogs().length()}; LogConsumer consumer = new LogConsumer() { @Override @@ -600,25 +820,30 @@ public class JFXLauncher extends Application { } } }; + consumerRef[0] = consumer; addLogConsumer(consumer); + Thread.currentThread().setName("sse-logs-stream"); while (!Thread.currentThread().isInterrupted()) { - Thread.sleep(10000); + try { + os.write(": heartbeat\n\n".getBytes(StandardCharsets.UTF_8)); + os.flush(); + Thread.sleep(10000); + } catch (InterruptedException e) { break; } catch (Exception e) { break; } } } catch (Exception ignored) {} finally { - removeLogConsumer(consumer); + if (consumerRef[0] != null) removeLogConsumer(consumerRef[0]); try { exchange.getResponseBody().close(); } catch (Exception ignored) {} } } - private LogConsumer consumer = null; - private void handleGameLogs(HttpExchange exchange) { sendJson(exchange, Map.of("success", true, "data", getGameLogs())); } private void handleGameLogsStream(HttpExchange exchange) { + final LogConsumer[] consumerRef = new LogConsumer[1]; try { exchange.getResponseHeaders().set("Content-Type", "text/event-stream"); exchange.getResponseHeaders().set("Cache-Control", "no-cache"); @@ -627,7 +852,7 @@ public class JFXLauncher extends Application { final OutputStream os = exchange.getResponseBody(); - consumer = new LogConsumer() { + LogConsumer consumer = new LogConsumer() { @Override public synchronized void onLog(String line) { try { @@ -639,17 +864,20 @@ public class JFXLauncher extends Application { } } }; + consumerRef[0] = consumer; addGameLogConsumer(consumer); + Thread.currentThread().setName("sse-game-logs-stream"); while (!Thread.currentThread().isInterrupted()) { - Thread.sleep(10000); + try { + os.write(": heartbeat\n\n".getBytes(StandardCharsets.UTF_8)); + os.flush(); + Thread.sleep(10000); + } catch (InterruptedException e) { break; } catch (Exception e) { break; } } } catch (Exception ignored) {} finally { - if (consumer != null) { - removeGameLogConsumer(consumer); - } - consumer = null; + if (consumerRef[0] != null) removeGameLogConsumer(consumerRef[0]); try { exchange.getResponseBody().close(); } catch (Exception ignored) {} } } @@ -697,6 +925,26 @@ public class JFXLauncher extends Application { } } + private void handleNews(HttpExchange exchange) { + try { + var url = new java.net.URL(ZHttpClient.getBaseUrl() + "/news"); + var conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + int code = conn.getResponseCode(); + if (code == 200) { + byte[] body = conn.getInputStream().readAllBytes(); + var raw = new String(body, java.nio.charset.StandardCharsets.UTF_8); + sendJson(exchange, Map.of("success", true, "data", raw)); + } else { + sendJson(exchange, Map.of("success", false, "error", "Server returned " + code)); + } + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + private void handleActivatePass(HttpExchange exchange) { try { if (!api.isLoggedIn()) { @@ -704,7 +952,7 @@ public class JFXLauncher extends Application { return; } Map body = parseJson(exchange.getRequestBody()); - String code = body.get("code"); + String code = body.get("pass_code"); if (code == null || code.isEmpty()) { sendJson(exchange, Map.of("success", false, "error", "Enter pass code")); return; @@ -735,6 +983,9 @@ public class JFXLauncher extends Application { Map data = new HashMap<>(); data.put("username", result.getData().getUsername()); data.put("token", result.getData().getToken()); + data.put("passActive", AuthManager.hasActivePass()); + data.put("role", AuthManager.getRole()); + data.put("roleName", AuthManager.getRoleName()); sendJson(exchange, Map.of("success", true, "data", data)); log("Registered: " + username); } else { @@ -745,8 +996,76 @@ public class JFXLauncher extends Application { } } + private void handlePackInfo(HttpExchange exchange) { + try { + Map params = parseQuery(exchange.getRequestURI().getQuery()); + String name = params.get("name"); + if (name == null || name.isEmpty()) { + sendJson(exchange, Map.of("success", false, "error", "Missing pack name")); + return; + } + Instance instance = InstanceManager.getInstance(name); + if (instance == null) { + sendJson(exchange, Map.of("success", false, "error", "Pack not found")); + return; + } + Path dir = instance.getPath(); + Map info = new HashMap<>(); + + int modsCount = 0; + Path modsDir = dir.resolve("mods"); + if (Files.exists(modsDir)) { + try (var files = Files.list(modsDir)) { + modsCount = (int) files.filter(p -> p.toString().endsWith(".jar")).count(); + } + } + info.put("modsCount", modsCount); + + List screenshots = new ArrayList<>(); + Path screenshotsDir = dir.resolve("screenshots"); + if (Files.exists(screenshotsDir)) { + try (var files = Files.list(screenshotsDir)) { + files.filter(p -> { + String n = p.getFileName().toString().toLowerCase(); + return n.endsWith(".png") || n.endsWith(".jpg"); + }).limit(20).forEach(p -> screenshots.add(p.getFileName().toString())); + } + } + info.put("screenshots", screenshots); + + List worlds = new ArrayList<>(); + Path savesDir = dir.resolve("saves"); + if (Files.exists(savesDir)) { + try (var dirs = Files.list(savesDir)) { + dirs.filter(Files::isDirectory).limit(20).forEach(p -> worlds.add(p.getFileName().toString())); + } + } + info.put("worlds", worlds); + + List recentLogs = new ArrayList<>(); + Path logsDir = dir.resolve("logs"); + if (Files.exists(logsDir)) { + try (var files = Files.list(logsDir)) { + files.filter(p -> p.toString().endsWith(".log") || p.toString().endsWith(".txt")) + .sorted((a, b) -> { + try { return Long.compare(Files.getLastModifiedTime(b).toMillis(), Files.getLastModifiedTime(a).toMillis()); } + catch (Exception e) { return 0; } + }) + .limit(5) + .forEach(p -> recentLogs.add(p.getFileName().toString())); + } + } + info.put("recentLogs", recentLogs); + + sendJson(exchange, Map.of("success", true, "data", info)); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + private void handleSettings(HttpExchange exchange) { try { + log("handleSettings: " + exchange.getRequestMethod()); if ("POST".equals(exchange.getRequestMethod())) { Map body = parseJson(exchange.getRequestBody()); if (body.containsKey("maxMemory")) { @@ -764,7 +1083,16 @@ public class JFXLauncher extends Application { if (body.containsKey("javaPath")) { Config.setJavaPath(body.get("javaPath")); } - sendJson(exchange, Map.of("success", true)); + if (body.containsKey("locale")) { + Config.setLocale(body.get("locale")); + } + if (body.containsKey("systemBasedJvm")) { + Config.setSystemBasedJvm(Boolean.parseBoolean(body.get("systemBasedJvm"))); + } + Map res = new HashMap<>(); + res.put("success", true); + res.put("maxMemory", Config.getMaxMemory()); + sendJson(exchange, res); return; } Map data = new HashMap<>(); @@ -775,6 +1103,10 @@ public class JFXLauncher extends Application { data.put("windowHeight", Config.getWindowHeight()); data.put("extraJvmArgs", Config.getExtraJvmArgs()); data.put("javaPath", Config.getJavaPath()); + data.put("locale", Config.getLocale()); + data.put("systemBasedJvm", Config.isSystemBasedJvm()); + data.put("cpuCores", Config.getSystemCpuCores()); + data.put("totalRamMB", Config.getSystemTotalRamMB()); sendJson(exchange, Map.of("success", true, "data", data)); } catch (Exception e) { sendJson(exchange, Map.of("success", false, "error", e.getMessage())); @@ -824,9 +1156,17 @@ public class JFXLauncher extends Application { log("[UI] Request: " + path); String relativePath = path.startsWith("/") ? path.substring(1) : path; - Path file = Paths.get(relativePath).toAbsolutePath(); + Path file = Paths.get(relativePath).toAbsolutePath().normalize(); + Path assetsDir = Paths.get("assets").toAbsolutePath().normalize(); - if (!Files.exists(file)) { + if (!file.startsWith(assetsDir)) { + log("[UI] Forbidden: " + file); + exchange.sendResponseHeaders(403, 0); + exchange.close(); + return; + } + + if (!Files.exists(file) || Files.isDirectory(file)) { log("[UI] File not found: " + file); exchange.sendResponseHeaders(404, 0); exchange.close(); @@ -853,15 +1193,51 @@ public class JFXLauncher extends Application { return "text/plain"; } - @SuppressWarnings("unchecked") private Map parseJson(InputStream body) { try { - return gson.fromJson(new String(body.readAllBytes(), StandardCharsets.UTF_8), Map.class); + byte[] rawBytes = body.readAllBytes(); + log("parseJson: " + rawBytes.length + " bytes"); + String raw = new String(rawBytes, StandardCharsets.UTF_8); + Map rawMap = gson.fromJson(raw, Map.class); + Map result = new HashMap<>(); + if (rawMap != null) { + for (Map.Entry entry : rawMap.entrySet()) { + String key = entry.getKey() != null ? entry.getKey().toString() : null; + String value = null; + Object v = entry.getValue(); + if (v instanceof Number) { + Number n = (Number) v; + if (n.doubleValue() == n.longValue()) { + value = String.valueOf(n.longValue()); + } else { + value = String.valueOf(n.doubleValue()); + } + } else { + value = v != null ? v.toString() : null; + } + if (key != null) result.put(key, value); + } + } + return result; } catch (Exception e) { + log("parseJson error: " + e.getMessage()); return new HashMap<>(); } } + private void openInBrowser(String url) { + if (url == null || url.isEmpty()) return; + try { + java.awt.Desktop.getDesktop().browse(new java.net.URI(url)); + } catch (Exception e) { + try { + new ProcessBuilder("xdg-open", url).start(); + } catch (Exception e2) { + log("Failed to open browser: " + e2.getMessage()); + } + } + } + private void sendJson(HttpExchange exchange, Map response) { try { String json = gson.toJson(response); @@ -870,21 +1246,135 @@ public class JFXLauncher extends Application { exchange.sendResponseHeaders(200, bytes.length); exchange.getResponseBody().write(bytes); exchange.close(); - } catch (Exception ignored) {} + } catch (Exception e) { + log("sendJson error: " + e.getMessage()); + } + } + + private void sendRawJson(HttpExchange exchange, String json) { + try { + byte[] bytes = json.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + exchange.getResponseBody().write(bytes); + exchange.close(); + } catch (Exception e) { + log("sendRawJson error: " + e.getMessage()); + } + } + + private String readBody(HttpExchange exchange) throws IOException { + return new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + } + + private String remoteApiCall(String method, String path, String body) throws Exception { + URL url = new URL(LAUNCHER_SERVER + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("User-Agent", "ZernMC-Launcher/1.0"); + String token = me.sashegdev.zernmc.launcher.auth.AuthManager.getAccessToken(); + if (token != null && !token.isEmpty() && !token.equals("0")) { + conn.setRequestProperty("Authorization", "Bearer " + token); + } + conn.setConnectTimeout(10000); + conn.setReadTimeout(15000); + if (body != null && !body.isEmpty()) { + conn.setDoOutput(true); + try (OutputStream os = conn.getOutputStream()) { + os.write(body.getBytes(StandardCharsets.UTF_8)); + } + } + int code = conn.getResponseCode(); + try (InputStream is = code >= 400 ? conn.getErrorStream() : conn.getInputStream()) { + return new String(is.readAllBytes(), StandardCharsets.UTF_8); + } + } + + private void handleFriendList(HttpExchange exchange) { + try { + String response = remoteApiCall("GET", "/api/friends/list", null); + sendRawJson(exchange, response); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + + private void handleFriendAdd(HttpExchange exchange) { + try { + String body = readBody(exchange); + String response = remoteApiCall("POST", "/api/friends/add", body); + sendRawJson(exchange, response); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + + private void handleFriendRemove(HttpExchange exchange) { + try { + String body = readBody(exchange); + String response = remoteApiCall("POST", "/api/friends/remove", body); + sendRawJson(exchange, response); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + + private void handleFriendAccept(HttpExchange exchange) { + try { + String body = readBody(exchange); + String response = remoteApiCall("POST", "/api/friends/accept", body); + sendRawJson(exchange, response); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + + private void handleFriendRequests(HttpExchange exchange) { + try { + String response = remoteApiCall("GET", "/api/friends/requests", null); + sendRawJson(exchange, response); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + + private void handleFriendStatus(HttpExchange exchange) { + try { + String body = readBody(exchange); + String response = remoteApiCall("POST", "/api/friends/status", body); + sendRawJson(exchange, response); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + + private void handlePlaytimeSync(HttpExchange exchange) { + try { + String body = readBody(exchange); + String response = remoteApiCall("POST", "/api/playtime/sync", body); + sendRawJson(exchange, response); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } + } + + private void handlePlaytimeStats(HttpExchange exchange) { + try { + String response = remoteApiCall("GET", "/api/playtime/stats", null); + sendRawJson(exchange, response); + } catch (Exception e) { + sendJson(exchange, Map.of("success", false, "error", e.getMessage())); + } } private void log(String msg) { - String entry = "[" + java.time.LocalTime.now() + "] " + msg + "\n"; + String timestamp = java.time.LocalTime.now().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")); + String entry = "[" + timestamp + "] " + msg; synchronized (launcherLogBuffer) { - launcherLogBuffer.append(entry); - } - System.out.println("[JFX] " + msg); - if (launcherLogFile != null) { - try { - Files.writeString(launcherLogFile, entry, - StandardOpenOption.CREATE, StandardOpenOption.APPEND); - } catch (Exception ignored) {} + launcherLogBuffer.append(entry).append("\n"); } + LauncherLogger.info("[JFX] " + msg); for (LogConsumer consumer : logConsumers) { try { consumer.onLog(entry); } catch (Exception ignored) {} } diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/Config.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/Config.java index 0eddb16..737e61a 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/Config.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/Config.java @@ -14,17 +14,22 @@ public class Config { private static final Properties props = new Properties(); - private static int maxMemory = 4096; - private static String serverUrl = "http://87.120.187.36:1582"; - private static String lastUsername = "Player"; - private static int windowWidth = 1280; - private static int windowHeight = 720; - private static String extraJvmArgs = ""; - private static String javaPath = "java"; + private static volatile int maxMemory = 4096; + private static volatile String serverUrl = "http://87.120.187.36:1582"; + private static volatile String lastUsername = "Player"; + private static volatile int windowWidth = 1280; + private static volatile int windowHeight = 720; + private static volatile String extraJvmArgs = ""; + private static volatile String javaPath = "java"; + private static volatile boolean ramManuallySet = false; + private static volatile String locale = "en"; + private static volatile boolean systemBasedJvm = false; static { load(); - applySmartRamRecommendation(); + if (!ramManuallySet) { + applySmartRamRecommendation(); + } } private static void load() { @@ -37,12 +42,15 @@ public class Config { } maxMemory = Integer.parseInt(props.getProperty("maxMemory", "4096")); + ramManuallySet = Boolean.parseBoolean(props.getProperty("ramManuallySet", "false")); serverUrl = props.getProperty("serverUrl", serverUrl); lastUsername = props.getProperty("lastUsername", lastUsername); windowWidth = Integer.parseInt(props.getProperty("windowWidth", "1280")); windowHeight = Integer.parseInt(props.getProperty("windowHeight", "720")); extraJvmArgs = props.getProperty("extraJvmArgs", ""); javaPath = props.getProperty("javaPath", "java"); + locale = props.getProperty("locale", "en"); + systemBasedJvm = Boolean.parseBoolean(props.getProperty("systemBasedJvm", "false")); } catch (Exception e) { System.err.println(ZAnsi.brightRed("Failed to load config: ") + e.getMessage()); @@ -52,12 +60,15 @@ public class Config { public static void save() { try { props.setProperty("maxMemory", String.valueOf(maxMemory)); + props.setProperty("ramManuallySet", String.valueOf(ramManuallySet)); props.setProperty("serverUrl", serverUrl); props.setProperty("lastUsername", lastUsername); props.setProperty("windowWidth", String.valueOf(windowWidth)); props.setProperty("windowHeight", String.valueOf(windowHeight)); props.setProperty("extraJvmArgs", extraJvmArgs); props.setProperty("javaPath", javaPath); + props.setProperty("locale", locale); + props.setProperty("systemBasedJvm", String.valueOf(systemBasedJvm)); try (var os = Files.newOutputStream(CONFIG_FILE)) { props.store(os, "ZernMC Launcher Configuration"); @@ -68,20 +79,44 @@ public class Config { } private static void applySmartRamRecommendation() { - long totalRamMB = Runtime.getRuntime().maxMemory() / (1024 * 1024); + long totalRamMB = getTotalSystemRamMB(); + if (totalRamMB <= 0) return; - long recommended = (long) (totalRamMB * 0.70); + long recommended; + if (totalRamMB <= 8192) { + recommended = 2560; + } else if (totalRamMB <= 12288) { + recommended = 3072; + } else if (totalRamMB <= 16384) { + recommended = 4096; + } else { + recommended = 5120; + } - recommended = Math.max(1536, recommended); - recommended = Math.min(recommended, totalRamMB - 1024); - - if (Math.abs(maxMemory - recommended) > 1024) { + if (Math.abs(maxMemory - recommended) > 512) { maxMemory = (int) recommended; save(); System.out.println(ZAnsi.cyan("Auto-recommended RAM: " + maxMemory + " MB")); } } + public static void resetRamRecommendation() { + ramManuallySet = false; + applySmartRamRecommendation(); + } + + private static long getTotalSystemRamMB() { + try { + Class beanClass = Class.forName("com.sun.management.OperatingSystemMXBean"); + java.lang.management.OperatingSystemMXBean osBean = java.lang.management.ManagementFactory.getOperatingSystemMXBean(); + if (beanClass.isInstance(osBean)) { + Object totalBytes = beanClass.getMethod("getTotalMemorySize").invoke(osBean); + return ((Number) totalBytes).longValue() / (1024 * 1024); + } + } catch (Exception ignored) {} + return 0; + } + public static int getMaxMemory() { return maxMemory; } @@ -99,6 +134,7 @@ public class Config { if (memory > 32768) memory = 32768; maxMemory = memory; + ramManuallySet = true; save(); } @@ -163,6 +199,56 @@ public class Config { save(); } + public static String getLocale() { + return locale; + } + + public static void setLocale(String lang) { + if (lang != null && (lang.equals("en") || lang.equals("ru"))) { + locale = lang; + save(); + } + } + + public static boolean isSystemBasedJvm() { + return systemBasedJvm; + } + + public static void setSystemBasedJvm(boolean enabled) { + systemBasedJvm = enabled; + save(); + } + + public static int getSystemCpuCores() { + return Runtime.getRuntime().availableProcessors(); + } + + public static long getSystemTotalRamMB() { + long totalMb = getTotalSystemRamMB(); + if (totalMb > 0) return totalMb; + return Runtime.getRuntime().maxMemory() / (1024 * 1024); + } + + public static String getSystemJvmFlags() { + int cores = getSystemCpuCores(); + long ramMB = getSystemTotalRamMB(); + StringBuilder sb = new StringBuilder(); + sb.append("-XX:ParallelGCThreads=").append(Math.max(1, cores)); + sb.append(" -XX:ConcGCThreads=").append(Math.max(1, cores / 2)); + sb.append(" -XX:+UseContainerSupport"); + sb.append(" -XX:+AlwaysPreTouch"); + if (ramMB >= 8192) { + sb.append(" -XX:+UseZGC"); + sb.append(" -XX:ZAllocationSpikeTolerance=2.0"); + } else { + sb.append(" -XX:+UseG1GC"); + sb.append(" -XX:MaxGCPauseMillis=50"); + sb.append(" -XX:G1HeapRegionSize=16M"); + } + sb.append(" -Xss4M"); + return sb.toString(); + } + public static String getRamInfo() { long totalMB = Runtime.getRuntime().maxMemory() / (1024 * 1024); return "Available RAM: " + totalMB + " MB | Recommended: " + maxMemory + " MB"; diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/LauncherLogger.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/LauncherLogger.java new file mode 100644 index 0000000..960a26b --- /dev/null +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/LauncherLogger.java @@ -0,0 +1,95 @@ +package me.sashegdev.zernmc.launcher.utils; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.locks.ReentrantLock; + +public class LauncherLogger { + + private static Path logFile; + private static boolean initialized = false; + private static final ReentrantLock lock = new ReentrantLock(); + + public static synchronized void init() { + if (initialized) return; + initialized = true; + + try { + Path logsDir = Paths.get(System.getProperty("user.home"), ".zernmc", "logs"); + Files.createDirectories(logsDir); + logFile = logsDir.resolve("launcher.log"); + + Files.writeString(logFile, + "=== Launcher Log " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + " ===\n", + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + + System.out.println("[LauncherLogger] initialized, log: " + logFile.toAbsolutePath()); + } catch (Exception e) { + System.err.println("[LauncherLogger] init error: " + e.getMessage()); + e.printStackTrace(); + } + } + + public static Path getLogFile() { + return logFile; + } + + public static void info(String msg) { + write("INFO", msg, null); + } + + public static void warn(String msg) { + write("WARN", msg, null); + } + + public static void error(String msg) { + write("ERROR", msg, null); + } + + public static void error(String msg, Throwable t) { + write("ERROR", msg, t); + } + + public static void debug(String msg) { + write("DEBUG", msg, null); + } + + private static void write(String level, String msg, Throwable t) { + String ts = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")); + String line = "[" + ts + "] [" + level + "] " + msg; + + System.out.println(line); + if (t != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + pw.flush(); + System.err.print(sw.toString()); + } + + if (logFile != null) { + lock.lock(); + try { + Files.writeString(logFile, line + "\n", StandardOpenOption.APPEND); + if (t != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + t.printStackTrace(pw); + pw.flush(); + Files.writeString(logFile, sw.toString(), StandardOpenOption.APPEND); + } + } catch (IOException e) { + System.err.println("[LauncherLogger] write error: " + e.getMessage()); + } finally { + lock.unlock(); + } + } + } +} diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java index 3e1432d..bc71777 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ZHttpClient.java @@ -373,8 +373,6 @@ public class ZHttpClient { } public static String get(String endpoint) throws IOException, InterruptedException { - checkAllServicesOnStartup(); - if (useProxyMode.get()) { return proxyGet(endpoint); } diff --git a/launcher/launcher/src/resources/ui/index.html b/launcher/launcher/src/resources/ui/index.html index 8647d32..b94d6eb 100644 --- a/launcher/launcher/src/resources/ui/index.html +++ b/launcher/launcher/src/resources/ui/index.html @@ -5,9 +5,6 @@ ZernMC Launcher - - - @@ -30,24 +27,24 @@

ZernMC

-

Launcher 1.0.9

+

Launcher 1.0.9

@@ -55,7 +52,7 @@ @@ -83,28 +80,28 @@ @@ -153,8 +153,8 @@
-

No pack selected

-

Select a pack from the sidebar or add a new one

+

No pack selected

+

Select a pack from the sidebar or add a new one

-
-
-Loader Ver
-
0Files
-
-Size
+
+
+
+
-Loader Ver
+
0Files
+
-Size
+
-Playtime
-

Loading description...

+

Loading description...

@@ -189,7 +190,7 @@ @@ -197,50 +198,57 @@
-

News

+

News

-
-
-
Coming Soon
-

ZernMC Server Updates

-

News and announcements will appear here. Stay tuned for the latest updates about the server and launcher.

- -
-
-
Info
-

Launcher v1.0.9

-

English UI, JavaFX redesign, improved pack management, and more. Check the GitHub for the full changelog.

- -
-
-
Guide
-

Getting Started

-

Install a pack, activate your pass via the website, and start playing. Need help? Contact a moderator.

- -
+
+
Loading news...
+
+
+ + +
+
+

Friends

+
+ +
+
+ +
+
No friends yet
+
+
-

Settings

+

Settings

-

Activate Pass

-

Enter your pass code to access server packs

+

Activate Pass

+

Enter your pass code to access server packs

- - + +
-

Allocated RAM

-

Loading...

+

Allocated RAM

+

Loading...

@@ -249,8 +257,8 @@
-

Game Resolution

-

Width x Height

+

Game Resolution

+

Width x Height

@@ -260,8 +268,8 @@
-

Extra JVM Arguments

-

Additional Java VM options

+

Extra JVM Arguments

+

Additional Java VM options

@@ -269,7 +277,7 @@
-

Java Path

+

Java Path

~/.zernmc/jre/

@@ -278,82 +286,128 @@
-

Server

+

Server

http://87.120.187.36:1582

Checking...
+
+
+

Language

+

Interface language

+
+
+ +
+
+
+
+

System-based JVM Optimization

+

-

+
+
+ +
+
+
+
+

Game Log

+

View real-time game logs

+
+
+ +
+
+ + + + + +