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 981ac96..2142390 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 @@ -394,9 +394,49 @@ public class Bootstrap { String summary = "Updated files: " + downloaded + ", skipped: " + skipped + ", failed: " + failed; if (staged > 0) summary += ", staged self-update: " + staged; log(summary); + + cleanStaleJavaFxJars(serverFiles); + log("Updated to v" + newVersion); } + /** + * Removes old lib/javafx/*-win.jar files that are no longer part of the + * server meta (e.g. the previous JavaFX 23 jars after an upgrade to + * 23.0.1). Keeping both would put two JARs exporting the same module on + * the --module-path, which breaks module resolution. + */ + private static void cleanStaleJavaFxJars(Map serverFiles) { + Path javafxDir = baseDir.resolve("lib").resolve("javafx"); + if (!Files.isDirectory(javafxDir)) return; + + java.util.Set keep = new java.util.HashSet<>(); + for (String filePath : serverFiles.keySet()) { + String normalized = filePath.toLowerCase().replace("\\", "/"); + if (normalized.startsWith("lib/javafx/") && normalized.endsWith("-win.jar")) { + keep.add(new File(filePath).getName().toLowerCase()); + } + } + + try (java.util.stream.Stream stream = Files.list(javafxDir)) { + stream.filter(Files::isRegularFile) + .filter(p -> { + String name = p.getFileName().toString().toLowerCase(); + return name.endsWith("-win.jar") && !keep.contains(name); + }) + .forEach(p -> { + try { + Files.delete(p); + log("Removed stale JavaFX jar: " + p.getFileName()); + } catch (Exception e) { + log("Warning: could not remove stale JavaFX jar " + p.getFileName() + " - " + e.getMessage()); + } + }); + } catch (Exception e) { + log("Warning: failed to scan JavaFX dir: " + e.getMessage()); + } + } + private static boolean isSelfExe(String filePath) { String lower = filePath.toLowerCase().replace("\\", "/"); return lower.equals("zernmc.exe") || lower.equals("zernmc-cli.exe") diff --git a/launcher/launcher/pom.xml b/launcher/launcher/pom.xml index 6574adc..c474757 100644 --- a/launcher/launcher/pom.xml +++ b/launcher/launcher/pom.xml @@ -61,32 +61,37 @@ org.openjfx javafx-controls - 23 + 23.0.1 win + provided org.openjfx javafx-web - 23 + 23.0.1 win + provided org.openjfx javafx-graphics - 23 + 23.0.1 win + provided org.openjfx javafx-base - 23 + 23.0.1 win + provided org.openjfx javafx-media - 23 + 23.0.1 win + provided @@ -235,27 +240,27 @@ - + - + - + - + - + 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 a6416f5..094ac62 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/Main.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/Main.java @@ -50,7 +50,7 @@ public class Main { private static void launchJFX() { try { - System.setProperty("javafx.runtime.version", "21"); + System.setProperty("javafx.runtime.version", "23.0.1"); JFXLauncher.main(new String[]{}); } catch (Exception e) { 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 2ffcde7..f0baf04 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 @@ -6,7 +6,7 @@ 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 me.sashegdev.zernmc.launcher.ui.jfx.JFXBridge; import me.sashegdev.zernmc.launcher.utils.Config; import me.sashegdev.zernmc.launcher.utils.LauncherLogger; @@ -102,14 +102,14 @@ public class LaunchService { String line; while ((line = reader.readLine()) != null) { String timestamped = "[" + java.time.LocalTime.now().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")) + "] " + line; - JFXLauncher.appendGameLog(line); + JFXBridge.appendGameLog(line); try { logFileOut.write((timestamped + "\n").getBytes(java.nio.charset.StandardCharsets.UTF_8)); logFileOut.flush(); } catch (Exception ignored) {} } } catch (Exception e) { - JFXLauncher.appendGameLog("[Error reading logs: " + e.getMessage() + "]"); + JFXBridge.appendGameLog("[Error reading logs: " + e.getMessage() + "]"); } finally { try { logFileOut.close(); } catch (Exception ignored) {} } @@ -127,11 +127,11 @@ public class LaunchService { try { int exitCode = process.waitFor(); runningProcesses.remove(pid); - JFXLauncher.appendGameLog("[Minecraft exited with code: " + exitCode + "]"); + JFXBridge.appendGameLog("[Minecraft exited with code: " + exitCode + "]"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (Exception e) { - JFXLauncher.appendGameLog("[Error watching process: " + e.getMessage() + "]"); + JFXBridge.appendGameLog("[Error watching process: " + e.getMessage() + "]"); } }, "ProcessWatcher-" + instanceName); processWatcher.setDaemon(true); diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java index 3bc1f3c..0eaa569 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/MinecraftLib.java @@ -5,7 +5,7 @@ import me.sashegdev.zernmc.launcher.minecraft.installer.ModLoaderInstaller; 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.ui.jfx.JFXBridge; import me.sashegdev.zernmc.launcher.utils.ConsoleUtils; import me.sashegdev.zernmc.launcher.utils.LauncherLogger; import me.sashegdev.zernmc.launcher.utils.ZAnsi; @@ -141,10 +141,10 @@ public class MinecraftLib { try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { - JFXLauncher.appendGameLog(line); + JFXBridge.appendGameLog(line); } } catch (Exception e) { - JFXLauncher.appendGameLog("[Error reading output: " + e.getMessage() + "]"); + JFXBridge.appendGameLog("[Error reading output: " + e.getMessage() + "]"); } }); outThread.setDaemon(true); @@ -155,10 +155,10 @@ public class MinecraftLib { try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { String line; while ((line = reader.readLine()) != null) { - JFXLauncher.appendGameLog("[ERR] " + line); + JFXBridge.appendGameLog("[ERR] " + line); } } catch (Exception e) { - JFXLauncher.appendGameLog("[Error reading stderr: " + e.getMessage() + "]"); + JFXBridge.appendGameLog("[Error reading stderr: " + e.getMessage() + "]"); } }); errThread.setDaemon(true); 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 c2d55e8..11ab2f4 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 @@ -250,7 +250,7 @@ public class PackDownloader { instance.setMinecraftVersion(manifest.getMinecraftVersion()); instance.setLoaderType(manifest.getLoaderType()); instance.setLoaderVersion(manifest.getLoaderVersion()); - instance.setAssetIndex(manifest.getAssetIndex()); + instance.setAssetIndex(resolveAssetIndex(manifest)); reportProgress("Pack installed successfully!", 100, "Installing pack files", 4, 5); System.out.println(ZAnsi.brightGreen("Pack installed successfully!")); @@ -300,7 +300,7 @@ public class PackDownloader { instance.setMinecraftVersion(manifest.getMinecraftVersion()); instance.setLoaderType(manifest.getLoaderType()); instance.setLoaderVersion(manifest.getLoaderVersion()); - instance.setAssetIndex(manifest.getAssetIndex()); + instance.setAssetIndex(resolveAssetIndex(manifest)); System.out.println(ZAnsi.brightGreen("Pack installed successfully!")); LauncherLogger.info("installOrUpdatePack: SUCCESS"); @@ -771,6 +771,56 @@ public class PackDownloader { public boolean isEmpty() { return files == null || files.isEmpty(); } } + /** + * Resolve the correct asset index for the installed Minecraft version. + * The manifest/instance.json asset_index value may be wrong (e.g. Zern-OBT + * shipped "1.20.1" while the real index for MC 1.20.1 is "5"). The source of + * truth is the local Mojang version JSON downloaded during install + * (versions/<mc>/<mc>.json -> assetIndex.id). We only trust it when the + * corresponding index file actually exists on disk; otherwise fall back to + * the manifest value. + */ + private String resolveAssetIndex(PackManifest manifest) { + String manifestIndex = manifest.getAssetIndex(); + if (manifest == null || manifest.getMinecraftVersion() == null) { + return manifestIndex; + } + try { + Path versionJson = instance.getPath().resolve("versions") + .resolve(manifest.getMinecraftVersion()) + .resolve(manifest.getMinecraftVersion() + ".json"); + if (Files.exists(versionJson)) { + JsonObject versionData = JsonParser.parseString(Files.readString(versionJson)).getAsJsonObject(); + String realIndex = null; + if (versionData.has("assetIndex") && versionData.get("assetIndex").isJsonObject() + && versionData.getAsJsonObject("assetIndex").has("id")) { + realIndex = versionData.getAsJsonObject("assetIndex").get("id").getAsString(); + } else if (versionData.has("assets") && !versionData.get("assets").isJsonNull()) { + realIndex = versionData.get("assets").getAsString(); + } + if (realIndex != null && !realIndex.isEmpty()) { + Path indexPath = instance.getPath().resolve("assets").resolve("indexes") + .resolve(realIndex + ".json"); + if (Files.exists(indexPath)) { + if (!realIndex.equals(manifestIndex)) { + LauncherLogger.warn("resolveAssetIndex: manifest asset_index=" + manifestIndex + + " but real index=" + realIndex + " exists on disk, using real index"); + } + return realIndex; + } else { + LauncherLogger.warn("resolveAssetIndex: real index=" + realIndex + + " from version JSON, but " + indexPath + " does not exist; falling back to manifest=" + manifestIndex); + } + } + } else { + LauncherLogger.warn("resolveAssetIndex: local version JSON not found: " + versionJson); + } + } catch (Exception e) { + LauncherLogger.warn("resolveAssetIndex: failed to resolve: " + e.getClass().getSimpleName() + " - " + e.getMessage()); + } + return manifestIndex; + } + public static class DiffResponse { private int version; private List to_download; diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java index 8b81b84..bc0c35d 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/minecraft/launch/LaunchCommandBuilder.java @@ -498,6 +498,29 @@ public class LaunchCommandBuilder { assetIndex = instance.getMinecraftVersion(); System.out.println(ZAnsi.yellow("Asset index not found, using version: " + assetIndex)); } else { + // Validate that the index file exists. A wrong/stale asset index + // (e.g. the Zern-OBT "1.20.1" bug, real index is "5") makes the game + // fail to load the base resource pack (grey panorama, no sounds). + Path indexPath = instance.getPath().resolve("assets").resolve("indexes").resolve(assetIndex + ".json"); + if (!Files.exists(indexPath)) { + System.out.println(ZAnsi.yellow("Asset index file missing: " + assetIndex + ".json, searching for existing index...")); + try (java.util.stream.Stream stream = Files.list(instance.getPath().resolve("assets").resolve("indexes"))) { + java.util.List existing = stream + .filter(p -> p.getFileName().toString().endsWith(".json")) + .sorted() + .collect(java.util.stream.Collectors.toList()); + if (!existing.isEmpty()) { + String found = existing.get(existing.size() - 1).getFileName().toString() + .replace(".json", ""); + System.out.println(ZAnsi.green("Using existing asset index: " + found + " (was " + assetIndex + ")")); + assetIndex = found; + } else { + System.out.println(ZAnsi.yellow("No asset index files found in assets/indexes/")); + } + } catch (Exception e) { + System.out.println(ZAnsi.yellow("Failed to search asset indexes: " + e.getMessage())); + } + } System.out.println(ZAnsi.green("Using asset index: " + assetIndex)); } args.add(assetIndex); diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXBridge.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXBridge.java new file mode 100644 index 0000000..f797be4 --- /dev/null +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/ui/jfx/JFXBridge.java @@ -0,0 +1,81 @@ +package me.sashegdev.zernmc.launcher.ui.jfx; + +/** + * Safe reflection bridge to JFXLauncher for use from non-JFX code (CLI, menus, + * installers). + * + * IMPORTANT: JFXLauncher extends javafx.application.Application. In CLI mode + * JavaFX is NOT on the module path, so loading JFXLauncher (or any class that + * references JavaFX) throws NoClassDefFoundError (an Error, not an Exception). + * Therefore the bridge must catch Throwable and never let JFX-launcher errors + * leak into CLI/game-launch code paths. + * + * Rule: any call into JFX UI state from non-JFX context MUST go through this + * bridge. Direct JFXLauncher.x() calls are only allowed inside JFXLauncher.java. + */ +public final class JFXBridge { + + private JFXBridge() {} + + private static final String JFX_CLASS = "me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher"; + + private static volatile java.lang.reflect.Method appendGameLogMethod; + private static volatile java.lang.reflect.Method setInstallProgressMethod; + private static volatile java.lang.reflect.Method setInstallStageMethod; + + private static java.lang.reflect.Method method(String name, Class... params) { + java.lang.reflect.Method m = null; + try { + Class clazz = Class.forName(JFX_CLASS); + m = clazz.getMethod(name, params); + } catch (Throwable t) { + // JavaFX not available (CLI mode) or JFXLauncher not initialized. + } + return m; + } + + /** Forward a game log line to the JFX UI. Safe no-op in CLI mode. */ + public static void appendGameLog(String msg) { + if (appendGameLogMethod == null) { + appendGameLogMethod = method("appendGameLog", String.class); + } + if (appendGameLogMethod == null) { + return; + } + try { + appendGameLogMethod.invoke(null, msg); + } catch (Throwable t) { + appendGameLogMethod = null; + } + } + + /** Forward install progress to the JFX UI. Safe no-op in CLI mode. */ + public static void setInstallProgress(String label, int current, int total) { + if (setInstallProgressMethod == null) { + setInstallProgressMethod = method("setInstallProgress", String.class, int.class, int.class); + } + if (setInstallProgressMethod == null) { + return; + } + try { + setInstallProgressMethod.invoke(null, label, current, total); + } catch (Throwable t) { + setInstallProgressMethod = null; + } + } + + /** Forward install stage to the JFX UI. Safe no-op in CLI mode. */ + public static void setInstallStage(String stageName, int stageIndex, int stageCount) { + if (setInstallStageMethod == null) { + setInstallStageMethod = method("setInstallStage", String.class, int.class, int.class); + } + if (setInstallStageMethod == null) { + return; + } + try { + setInstallStageMethod.invoke(null, stageName, stageIndex, stageCount); + } catch (Throwable t) { + setInstallStageMethod = null; + } + } +} diff --git a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ProgressBar.java b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ProgressBar.java index c2278c6..ac587aa 100644 --- a/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ProgressBar.java +++ b/launcher/launcher/src/main/java/sashegdev/zernmc/launcher/utils/ProgressBar.java @@ -1,5 +1,7 @@ package me.sashegdev.zernmc.launcher.utils; +import me.sashegdev.zernmc.launcher.ui.jfx.JFXBridge; + import java.text.DecimalFormat; public class ProgressBar { @@ -14,11 +16,7 @@ public class ProgressBar { currentLabel = label; currentTotal = total; - try { - Class jfxClass = Class.forName("me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher"); - java.lang.reflect.Method setProgress = jfxClass.getMethod("setInstallProgress", String.class, int.class, int.class); - setProgress.invoke(null, label, (int) current, (int) total); - } catch (Exception ignored) {} + JFXBridge.setInstallProgress(label, (int) current, (int) total); if (total <= 0) { System.out.print("\r" + ZAnsi.cyan(label) + " ..."); @@ -40,11 +38,7 @@ public class ProgressBar { currentLabel = label; currentTotal = totalBytes; - try { - Class jfxClass = Class.forName("me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher"); - java.lang.reflect.Method setProgress = jfxClass.getMethod("setInstallProgress", String.class, int.class, int.class); - setProgress.invoke(null, label + " " + formatBytes(downloaded) + "/" + formatBytes(totalBytes), (int) downloaded, (int) totalBytes); - } catch (Exception ignored) {} + JFXBridge.setInstallProgress(label + " " + formatBytes(downloaded) + "/" + formatBytes(totalBytes), (int) downloaded, (int) totalBytes); if (totalBytes <= 0) { System.out.print("\r" + ZAnsi.cyan(label) + " ..."); @@ -71,11 +65,7 @@ public class ProgressBar { currentLabel = label; currentTotal = total; - try { - Class jfxClass = Class.forName("me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher"); - java.lang.reflect.Method setProgress = jfxClass.getMethod("setInstallProgress", String.class, int.class, int.class); - setProgress.invoke(null, label, (int) current, (int) (total > 0 ? total : 100)); - } catch (Exception ignored) {} + JFXBridge.setInstallProgress(label, (int) current, (int) (total > 0 ? total : 100)); if (total <= 0) { char[] spinner = {'|', '/', '-', '\\'}; @@ -92,11 +82,7 @@ public class ProgressBar { } public static void setStage(String stageName, int stageIndex, int stageCount) { - try { - Class jfxClass = Class.forName("me.sashegdev.zernmc.launcher.ui.jfx.JFXLauncher"); - java.lang.reflect.Method setStage = jfxClass.getMethod("setInstallStage", String.class, int.class, int.class); - setStage.invoke(null, stageName, stageIndex, stageCount); - } catch (Exception ignored) {} + JFXBridge.setInstallStage(stageName, stageIndex, stageCount); } public static void clearLine() { diff --git a/launcher/pom.xml b/launcher/pom.xml index 0afa4f5..0ddd088 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -18,8 +18,8 @@ - 1.0.15 - 3 + 1.0.16 + 0 21 21 UTF-8 @@ -74,31 +74,31 @@ org.openjfx javafx-controls - 23 + 23.0.1 win org.openjfx javafx-web - 23 + 23.0.1 win org.openjfx javafx-graphics - 23 + 23.0.1 win org.openjfx javafx-base - 23 + 23.0.1 win org.openjfx javafx-media - 23 + 23.0.1 win diff --git a/server/main.py b/server/main.py index 00c20ea..9ceaa7b 100644 --- a/server/main.py +++ b/server/main.py @@ -1332,8 +1332,13 @@ def generate_launcher_builds_meta(): rel_path = str(file_path.relative_to(BUILDS_DIR)) # Only include runtime files needed for launcher updates - # Skip: JRE, JavaFX, old ZIPs, extracted versions, docs, root JARs - if rel_path.startswith(("jre21/", "lib/", "lib-javafx/", "versions/", "libs/")): + # Skip: JRE, old ZIPs, extracted versions, docs, root JARs. + # lib/javafx/*-win.jar IS included so existing clients get the + # JavaFX update (e.g. 23 -> 23.0.1 WebKit WebSocket fix) via the + # incremental updater, not only through a full ZIP reinstall. + if rel_path.startswith(("jre21/", "lib-javafx/", "versions/", "libs/", "logs/")): + continue + if rel_path.startswith("lib/") and not rel_path.startswith("lib/javafx/"): continue if rel_path.endswith(".zip") or rel_path in ("README.txt", "build.version"): continue