From b4f55b0707be89ef7fd2bc8e7284d38aeff5f82f Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 05:11:16 +0800 Subject: [PATCH 01/13] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=8B=AC=E7=AB=8B?= =?UTF-8?q?=E5=88=A4=E6=96=AD=EF=BC=8C=E5=8F=AA=E7=94=B1=20LauncherHelper?= =?UTF-8?q?=20=E5=9C=A8=E5=86=B3=E7=AD=96=E5=90=8E=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/game/JavaCompatibility.java | 40 ++++ .../hmcl/game/JavaCompatibilityEvaluator.java | 175 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibility.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibilityEvaluator.java diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibility.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibility.java new file mode 100644 index 0000000000..6f90e360e9 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibility.java @@ -0,0 +1,40 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.game; + +public record JavaCompatibility( + int targetMajor, + int actualMajor, + Level level +) { + public enum Level { + /// The selected runtime matches what the instance expects. + OK, + + /// The selected runtime is newer than expected. + /// + /// This is deliberately not an error: whether the game actually breaks depends on + /// which mods are installed, and that cannot be determined statically. The correct + /// response is therefore to tell the user and offer a way back, never to block. + NEWER_THAN_EXPECTED + } + + public boolean isOk() { + return level == Level.OK; + } +} \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibilityEvaluator.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibilityEvaluator.java new file mode 100644 index 0000000000..20e0ee5ab3 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibilityEvaluator.java @@ -0,0 +1,175 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.game; + +import org.jackhuang.hmcl.addon.mod.ModLoaderType; +import org.jackhuang.hmcl.java.JavaRuntime; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/// Decides whether the Java runtime HMCL selected is newer than the version the game +/// instance expects. +/// +/// This is a pure function: no IO, no UI, no settings access. That is what keeps it +/// decoupled from [org.jackhuang.hmcl.java.JavaManager] (which picks a runtime) and +/// from the launch flow (which decides what to do about a deviation). +/// +/// ### Why this does not hardcode version ranges +/// +/// `JavaVersionConstraint` encodes "Minecraft 1.18-1.20.4 needs Java 17" as literal +/// ranges. Every new Minecraft release invalidates such a range, which is how the +/// existing entries drifted out of date and why only Forge is covered today. +/// +/// This class instead asks the version JSON: [GameInstanceManifest#javaVersion()] is +/// Mojang's own declaration of the runtime requirement, shipped with the game. A new +/// Minecraft release is picked up with zero code changes. +public final class JavaCompatibilityEvaluator { + + private JavaCompatibilityEvaluator() { + } + + /// The Java major versions Mojang publishes Minecraft runtimes for, ascending. + /// + /// Used as the ruler for "how many steps newer" a runtime is. Java major versions are + /// not evenly spaced (8, 16, 17, 21, 25), so counting steps requires this list rather + /// than plain arithmetic. + /// + /// Maintenance: append here when a new Java LTS becomes an official Minecraft runtime. + /// This is the only place to update. + private static final List KNOWN_RUNTIMES = List.of( + GameJavaVersion.JAVA_8, + GameJavaVersion.JAVA_16, + GameJavaVersion.JAVA_17, + GameJavaVersion.JAVA_21, + GameJavaVersion.JAVA_25 + ); + + /// How many steps newer than expected each loader is known to tolerate. + /// + /// Maintenance: adding a loader is one line; tuning one is one number. A slightly + /// wrong number causes a spurious or missing warning, never a structural failure, + /// which is exactly why this belongs in data rather than in logic. + /// + /// The [ModLoaderType] reference is stored directly so a rename breaks the build + /// instead of failing silently at runtime. + private enum LoaderTolerance { + FABRIC(ModLoaderType.FABRIC, 1), + QUILT(ModLoaderType.QUILT, 1), + FORGE(ModLoaderType.FORGE, 1), + NEO_FORGE(ModLoaderType.NEO_FORGE, 1), + LEGACY_FABRIC(ModLoaderType.LEGACY_FABRIC, 0), + LITE_LOADER(ModLoaderType.LITE_LOADER, 0), + /// Cleanroom declares its own Java requirement, see [resolveTarget]. + CLEANROOM(ModLoaderType.CLEANROOM, 0); + + final ModLoaderType type; + final int steps; + + LoaderTolerance(ModLoaderType type, int steps) { + this.type = type; + this.steps = steps; + } + } + + /// Vanilla carries the best forward-compatibility record, so it gets the widest window. + private static final int VANILLA_TOLERANCE = 2; + + public static JavaCompatibility evaluate( + @Nullable GameVersionNumber gameVersion, + GameInstanceManifest manifest, + JavaRuntime java, + GameComponentAnalyzer analyzer) { + + GameJavaVersion target = resolveTarget(gameVersion, manifest, analyzer); + if (target == null) + return new JavaCompatibility(0, java.getParsedVersion(), JavaCompatibility.Level.OK); + + int targetMajor = target.majorVersion(); + int actualMajor = java.getParsedVersion(); + + // Older than expected is already handled by the mandatory constraints + // (GAME_JSON / VANILLA), so this evaluator only looks upward. + if (actualMajor <= targetMajor) + return new JavaCompatibility(targetMajor, actualMajor, JavaCompatibility.Level.OK); + + if (actualMajor <= upperBound(targetMajor, toleranceSteps(analyzer))) + return new JavaCompatibility(targetMajor, actualMajor, JavaCompatibility.Level.OK); + + return new JavaCompatibility(targetMajor, actualMajor, JavaCompatibility.Level.NEWER_THAN_EXPECTED); + } + + /// The Java version this instance is expected to run on. + public static @Nullable GameJavaVersion resolveTarget( + @Nullable GameVersionNumber gameVersion, + GameInstanceManifest manifest, + GameComponentAnalyzer analyzer) { + + // Cleanroom's Java requirement is independent of the game version. + String cleanroomVersion = analyzer.getVersion(GameComponentType.CLEANROOM); + if (cleanroomVersion != null) + return GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion); + + // Mojang's own declaration. Present for every release that ships a runtime + // requirement, and authoritative for future ones. + GameJavaVersion declared = manifest.javaVersion(); + if (declared != null) + return declared; + + // Versions predating the field; fall back to the known minimum. + return gameVersion != null ? GameJavaVersion.getMinimumJavaVersion(gameVersion) : null; + } + + /// The narrowest tolerance among the loaders present. Being conservative here means + /// risking a spurious warning rather than a missing one. + private static int toleranceSteps(GameComponentAnalyzer analyzer) { + int steps = VANILLA_TOLERANCE; + boolean anyLoader = false; + + for (LoaderTolerance tolerance : LoaderTolerance.values()) { + if (analyzer.has(tolerance.type)) { + anyLoader = true; + steps = Math.min(steps, tolerance.steps); + } + } + + return anyLoader ? steps : VANILLA_TOLERANCE; + } + + /// Walks [KNOWN_RUNTIMES] forward from the expected version. + /// + /// A runtime newer than the last known entry is out of range and warns, so a future + /// Java release is flagged until this list is extended — it never silently passes. + private static int upperBound(int targetMajor, int steps) { + int index = -1; + for (int i = 0; i < KNOWN_RUNTIMES.size(); i++) { + if (KNOWN_RUNTIMES.get(i).majorVersion() == targetMajor) { + index = i; + break; + } + } + + // Unknown target: compare numerically so we never stay silent. + if (index < 0) + return targetMajor; + + int bounded = Math.min(index + steps, KNOWN_RUNTIMES.size() - 1); + return KNOWN_RUNTIMES.get(bounded).majorVersion(); + } +} \ No newline at end of file From c4b3a5169963461e7e1f0e49e0f1f52343a3360e Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 05:17:33 +0800 Subject: [PATCH 02/13] =?UTF-8?q?=E4=B8=BA=20LauncherHelper=20=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=86=B3=E7=AD=96=E9=83=A8=E5=88=86=EF=BC=8C=E5=BD=BB?= =?UTF-8?q?=E5=BA=95=E8=A7=A3=E8=80=A6=E6=89=BE=20Java"=E5=92=8C"=E8=A6=81?= =?UTF-8?q?=E4=B8=8D=E8=A6=81=E9=97=AE=E7=94=A8=E6=88=B7"=E4=B8=A4?= =?UTF-8?q?=E4=BB=B6=E4=BA=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/LauncherHelper.java | 81 ++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 853c405eaf..b68f9deb15 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -445,7 +445,33 @@ private static Task checkGameState(HMCLGameInstance gameInstance, G } else if (javaVersionType == JavaVersionType.AUTO || javaVersionType == JavaVersionType.VERSION) { task = getJavaTask.thenComposeAsync(Schedulers.javafx(), java -> { if (java != null) { - return Task.completed(java); + // Only warn when HMCL made the decision. When the user picked a specific + // Java version, they know what they selected and own the consequence. + if (javaVersionType != JavaVersionType.AUTO) + return Task.completed(java); + + JavaCompatibility compatibility = + JavaCompatibilityEvaluator.evaluate(gameVersion, manifest, java, analyzer); + if (compatibility.isOk()) + return Task.completed(java); + + CompletableFuture future = new CompletableFuture<>(); + Task result = Task.fromCompletableFuture(future); + Runnable breakAction = () -> future.completeExceptionally( + new CancellationException("Launch operation was cancelled by user")); + + // Resolved before the dialog is shown: JavaManager.getAllJava() blocks + // until the initial Java scan finishes, so it must not run inside a + // JavaFX callback or it will freeze the UI. + JavaRuntime preferred = findInstalledJava(compatibility.targetMajor()); + + Controllers.confirm( + i18n("launch.advice.modded_java", compatibility.targetMajor(), gameVersion) + + "\n\n" + i18n("launch.advice.switch_java", compatibility.targetMajor()), + i18n("message.warning"), + MessageType.WARNING, + () -> switchToExpectedJava(gameInstance, compatibility, preferred, future, breakAction), + () -> future.complete(java)); } // Reset invalid java version @@ -729,6 +755,59 @@ else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA)) return task.withStage("launch.state.java"); } + /// Returns an installed runtime whose major version matches {@code major}, or null if + /// none is installed. + /// + /// {@link JavaManager#getAllJava} blocks until the initial Java scan completes, so this + /// is resolved before the confirmation dialog is shown rather than inside its callback. + private static @Nullable JavaRuntime findInstalledJava(int major) { + try { + for (JavaRuntime installed : JavaManager.getAllJava()) { + if (installed.getParsedVersion() == major) + return installed; + } + } catch (InterruptedException e) { + // Preserve the interrupt so the shutdown path can observe it. + Thread.currentThread().interrupt(); + } + return null; + } + + /// Switches to, or downloads, the Java version the instance is expected to run on. + /// + /// Note: this deliberately does not call `setting.setJavaAutoSelected()`. Silently + /// rewriting the user's Java selection mode is what makes this warning disappear + /// permanently on subsequent launches. + private static void switchToExpectedJava( + HMCLGameInstance gameInstance, + JavaCompatibility compatibility, + @Nullable JavaRuntime preferred, + CompletableFuture future, + Runnable breakAction) { + + if (preferred != null) { + future.complete(preferred); + return; + } + + GameJavaVersion target = GameJavaVersion.get(compatibility.targetMajor()); + if (target == null) { + // No published runtime for this major version, so there is nothing to offer. + breakAction.run(); + return; + } + + downloadJava(target, gameInstance.getRepository()) + .whenCompleteAsync((downloaded, throwable) -> { + if (throwable == null) { + future.complete(downloaded); + } else { + LOG.warning("Failed to download java", throwable); + breakAction.run(); + } + }, Schedulers.javafx()); + } + private static CompletableFuture downloadJava(GameJavaVersion javaVersion, HMCLGameRepository repository) { CompletableFuture future = new CompletableFuture<>(); Controllers.dialog(new MessageDialogPane.Builder( From f34b6c923d9487f9b412085fa344cebe472e2ab6 Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 05:25:59 +0800 Subject: [PATCH 03/13] =?UTF-8?q?=E4=B8=BA=E6=9C=AC=E6=AC=A1=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=A2=9E=E5=8A=A0=E7=9A=84=E9=94=AE=E5=80=BC=E8=A1=A5?= =?UTF-8?q?=E5=85=A8=E7=BF=BB=E8=AF=91=EF=BC=8C=E6=AD=A4=E9=83=A8=E5=88=86?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E7=94=B1=20DeepSeek=20=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_ar.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_de.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_es.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_ja.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_lzh.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_ru.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_uk.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_zh.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 1 + 10 files changed, 11 insertions(+) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 651f5cac89..91d018b32e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -896,6 +896,7 @@ launch.advice.modlauncher8=The Forge version you are using is not compatible wit launch.advice.newer_java=You are using an older Java version to launch the game. It is recommended to update to Java 8, otherwise some mods may cause the game to crash. launch.advice.not_enough_space=You have allocated a memory size larger than the actual %d MiB of memory installed on your computer. You may experience degraded performance or even be unable to launch the game. launch.advice.require_newer_java_version=The current game version requires Java %s, but we could not find one. Do you want to download one now? +launch.advice.switch_java=Do you want to switch to Java %1$s? launch.advice.too_large_memory_for_32bit=You have allocated a memory size larger than the memory limitation of the 32-bit Java installation. You may be unable to launch the game. launch.advice.vanilla_linux_java_8=Minecraft 1.12.2 or earlier only supports Java 8 for the Linux x86-64 platform because the later versions cannot load 32-bit native libraries like liblwjgl.so\n\nPlease download it from java.com or install OpenJDK 8. launch.advice.vanilla_x86.translation=Minecraft is not fully supported on your platform, so you may experience missing features or even be unable to launch the game.\nYou can download Java for the x86-64 architecture here for a full gaming experience. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 95e20c2468..e485059c57 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -851,6 +851,7 @@ launch.advice.modlauncher8=إصدار Forge الذي تستخدمه غير مت launch.advice.newer_java=أنت تستخدم إصداراً قديماً من Java لتشغيل اللعبة. يُنصح بالتحديث إلى Java 8، وإلا قد تتسبب بعض المودات في تعطل اللعبة. launch.advice.not_enough_space=لقد خصّصت حجم ذاكرة أكبر من الذاكرة الفعلية المثبّتة (%d ميجابايت) على حاسوبك. قد تواجه أداءً منخفضاً أو عدم القدرة على تشغيل اللعبة. launch.advice.require_newer_java_version=يتطلب إصدار اللعبة الحالي Java %s، لكن لم نجد أياً. هل تريد تنزيله الآن؟ +launch.advice.switch_java=هل تريد التبديل إلى Java %1$s؟ launch.advice.too_large_memory_for_32bit=لقد خصّصت حجم ذاكرة يتجاوز حد تثبيت Java 32-bit. قد لا تتمكن من تشغيل اللعبة. launch.advice.vanilla_linux_java_8=يدعم Minecraft 1.12.2 أو أقدم Java 8 فقط على منصة Linux x86-64 لأن الإصدارات الأحدث لا تستطيع تحميل المكتبات الأصلية 32-bit مثل liblwjgl.so\n\nيُرجى تنزيله من java.com أو تثبيت OpenJDK 8. launch.advice.vanilla_x86.translation=Minecraft غير مدعوم بشكل كامل على منصتك، لذا قد تواجه ميزات مفقودة أو عدم القدرة على تشغيل اللعبة.\nيمكنك تنزيل Java لمعمارية x86-64 من هنا لتجربة لعب كاملة. diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 997645d3b9..68831c03d0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -893,6 +893,7 @@ launch.advice.modlauncher8=Die Forge-Version, die Sie verwenden, ist nicht mit d launch.advice.newer_java=Sie verwenden eine ältere Java-Version, um das Spiel zu starten. Es wird empfohlen, auf Java 8 zu aktualisieren, da sonst einige Mods das Spiel zum Absturz bringen können. launch.advice.not_enough_space=Sie haben eine Speichergröße zugewiesen, die größer ist als die tatsächlich auf Ihrem Computer installierten %d MiB Speicher. Es kann zu Leistungseinbußen kommen oder das Spiel kann sogar nicht gestartet werden. launch.advice.require_newer_java_version=Die aktuelle Spielversion erfordert Java %s, aber wir konnten keine finden. Möchten Sie jetzt eine herunterladen? +launch.advice.switch_java=Möchten Sie zu Java %1$s wechseln? launch.advice.too_large_memory_for_32bit=Sie haben eine Speichergröße zugewiesen, die das Speicherlimit der 32-Bit-Java-Installation überschreitet. Sie können das Spiel möglicherweise nicht starten. launch.advice.vanilla_linux_java_8=Minecraft 1.12.2 oder früher unterstützt nur Java 8 für die Linux-x86-64-Plattform, da die späteren Versionen keine 32-Bit-nativen Bibliotheken wie liblwjgl.so laden können\n\nBitte laden Sie es von java.com herunter oder installieren Sie OpenJDK 8. launch.advice.vanilla_x86.translation=Minecraft wird auf Ihrer Plattform nicht vollständig unterstützt, daher fehlen möglicherweise Funktionen oder Sie können das Spiel sogar nicht starten.\nSie können Java für die x86-64-Architektur hier herunterladen, um ein vollständiges Spielerlebnis zu erhalten. diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index ec11203a3d..396eb108d7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -804,6 +804,7 @@ launch.advice.modlauncher8=La versión de Forge que estás utilizando no es comp launch.advice.newer_java=Estás utilizando una versión antigua de Java para iniciar el juego. Se recomienda actualizar a Java 8, de lo contrario algunos mods pueden hacer que el juego se bloquee. launch.advice.not_enough_space=Has asignado un tamaño de memoria mayor que los %d MiB reales de memoria instalados en tu máquina. Es posible que el rendimiento del juego se vea afectado, o incluso que no puedas iniciar el juego. launch.advice.require_newer_java_version=La versión actual del juego requiere Java %s, pero no hemos podido encontrar uno. ¿Quieres descargar uno ahora? +launch.advice.switch_java=¿Quieres cambiar a Java %1$s? launch.advice.too_large_memory_for_32bit=Has asignado un tamaño de memoria mayor que la limitación de memoria de la instalación de Java de 32 bits. Es posible que no puedas iniciar el juego. launch.advice.vanilla_linux_java_8=Minecraft 1.12.2 o inferior sólo admite Java 8 para la plataforma Linux x86-64, porque las versiones posteriores no pueden cargar las bibliotecas nativas de 32 bits como liblwjgl.so\n\nPor favor, descárguelo de java.com, o instale OpenJDK 8. launch.advice.vanilla_x86.translation=Minecraft no proporciona actualmente soporte oficial para arquitecturas distintas de x86 y x86-64.\n\nPor favor, instale Java para x86-64 para jugar a Minecraft a través del entorno de traducción Rosetta, o descargue sus bibliotecas nativas correspondientes y especifique su ruta. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 5e4fb6d3c3..771eccd970 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -529,6 +529,8 @@ launch.advice.java8_51_1_13=Minecraft 1.13は、1.8.0_51より前のJava8でク launch.advice.java9=Java9以降のバージョンのJavaでMinecraft1.12以前を起動することはできません。ゲームを高速化するには、Java8をお勧めします。 launch.advice.newer_java=多くのMinecraft1.12以降、およびほとんどのModには、Java8が必要です。 launch.advice.not_enough_space=割り当てたメモリが多すぎます。物理メモリサイズが%dMiBであるため、ゲームがクラッシュする可能性があります。 +launch.advice.modded_java=一部のModは新しいJavaバージョンと互換性がない可能性があります。Minecraft %2$s を起動するには Java %1$s の使用を推奨します。 +launch.advice.switch_java=Java %1$s に切り替えますか? launch.advice.too_large_memory_for_32bit=32ビットJavaランタイム環境が原因で、割り当てたメモリが多すぎるため、ゲームがクラッシュする可能性があります。32ビットシステムの最大メモリ容量は1024MiBです。 launch.advice.vanilla_linux_java_8=Linux x86-64の場合、Minecraft1.12.2以下はJava8でのみ実行できます。\nJava9以降のバージョンでは、liblwjgl.soなどの32ビットネイティブライブラリをロードできません。 launch.advice.vanilla_x86.translation=Minecraftは現在、x86およびx86-64以外のアーキテクチャの公式サポートを提供していません。\nJava for x86-64を使用して、トランスレータを介してminecraftを実行するか、プラットフォームの対応するネイティブライブラリをダウンロードして指定してくださいその配置パス。 diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 13a3d35e68..d425da5e6a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -635,6 +635,7 @@ launch.advice.modlauncher8=鍛版之所行弗兼於爪哇之所行。宜新鍛 launch.advice.newer_java=余識君之啟戲以舊爪哇也,蓋改囊或是以崩戲也。宜新爪哇於八,然後復啟。 launch.advice.not_enough_space=君所分之憶巨,乃逾算機之總憶 %d 兆二進字節,庶戲崩。 launch.advice.require_newer_java_version=戲之是版須爪哇 %s,然 HMCL 尋而弗得。擊「然」,而 HMCL 將取之。然否?\n凡有謬,遽求助於右上之鈕。 +launch.advice.switch_java=欲易以爪哇 %1$s 乎? launch.advice.too_large_memory_for_32bit=分憶甚益,逾三十二位爪哇之限,戲或以崩。宜削於一千零二十四兆二進字節。\n凡有謬,遽求助於右上之鈕。 launch.advice.vanilla_linux_java_8=於磷若 x86-64 之械網,礦藝一點一二點二以降弗容於爪哇九以晉。宜啟以爪哇八。\n凡有謬,遽求助於右上之鈕。 launch.advice.vanilla_x86.translation=礦藝未完適子之算機,或會涉君之戲感,或無能啟戲。\n君可於x86-64 架構之爪哇以更善之。 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index d2c83825f6..9fc66eed0a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -793,6 +793,7 @@ launch.advice.modded_java=Некоторые моды могут быть нес launch.advice.newer_java=Вы используете старую версию Java для запуска игры. Рекомендуется обновить Java 8, иначе некоторые моды могут привести к сбою игры. launch.advice.not_enough_space=Вы выделили слишком много памяти, поскольку размер физической памяти составляет %d МиБ, ваша игра может рухнуть. launch.advice.require_newer_java_version=Для текущей версии игры требуется Java %s, но мы не смогли ее найти. Хотите скачать ее сейчас? +launch.advice.switch_java=Хотите переключиться на Java %1$s? launch.advice.too_large_memory_for_32bit=Вы выделили слишком много памяти, из-за 32-разрядной JRE ваша игра может рухнуть. Максимальный объем памяти для 32-разрядных систем составляет 1024 МиБ. launch.advice.vanilla_linux_java_8=На Linux x86-64, Minecraft 1.12.2 и ниже может работать только на Java 8.\nВерсии Java 9 и выше не могут загружать 32-битные нативные библиотеки, такие как liblwjgl.so. launch.advice.modlauncher8=Используемая вами версия Forge несовместима с текущей версией Java. Попробуйте обновить Forge. diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 9c8c0ba7ed..3c5598b11a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -852,6 +852,7 @@ launch.advice.modlauncher8=Версія Forge, яку ви використов launch.advice.newer_java=Ви використовуєте старішу версію Java для запуску гри. Рекомендується оновити до Java 8, інакше деякі моди можуть призвести до збою гри. launch.advice.not_enough_space=Ви виділили розмір пам'яті, більший за фактичні %d МіБ пам'яті, встановленої на вашому комп'ютері. Ви можете зіткнутися з погіршеною продуктивністю або навіть не зможете запустити гру. launch.advice.require_newer_java_version=Поточна версія гри вимагає Java %s, але ми не змогли знайти її. Бажаєте завантажити її зараз? +launch.advice.switch_java=Хочете перейти на Java %1$s? launch.advice.too_large_memory_for_32bit=Ви виділили розмір пам'яті, більший за обмеження пам'яті 32-бітної інсталяції Java. Ви можете не змогти запустити гру. launch.advice.vanilla_linux_java_8=Minecraft 1.12.2 або раніші підтримують лише Java 8 для платформи Linux x86-64, оскільки новіші версії не можуть завантажити 32-бітні нативні бібліотеки, такі як liblwjgl.so. Завантажте її з java.com або встановіть OpenJDK 8. launch.advice.vanilla_x86.translation=Minecraft не повністю підтримується на вашій платформі, тому ви можете зіткнутися з відсутністю функцій або навіть не зможете запустити гру. Ви можете завантажити Java для архітектури x86-64 тут для повного ігрового досвіду. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 3e0ca35f55..5451d34a9b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -708,6 +708,7 @@ launch.advice.modlauncher8=你所使用的 Forge 版本與目前使用的 Java launch.advice.newer_java=偵測到你正在使用舊版本 Java 啟動遊戲,這可能導致部分模組引發遊戲崩潰。建議更新至 Java 8 後再次啟動。 launch.advice.not_enough_space=你設定的記憶體分配值過大,超過了系統記憶體大小 %d MiB,可能影響遊戲體驗或無法啟動遊戲。 launch.advice.require_newer_java_version=目前遊戲版本需要 Java %s,但 HMCL 未能找到該 Java 版本。你可以點擊「是」,HMCL 會自動下載它。是否下載? +launch.advice.switch_java=是否要切換到 Java %1$s? launch.advice.too_large_memory_for_32bit=你設定的記憶體分配值過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲。請將記憶體分配值調至低於 1024 MiB 的值。 launch.advice.vanilla_linux_java_8=對於 Linux x86-64 平台,Minecraft 1.12.2 及更低版本與 Java 9+ 不相容,請使用 Java 8 啟動遊戲。 launch.advice.vanilla_x86.translation=Minecraft 尚未為你的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n你可以在 這裡 下載 x86-64 架構的 Java 以獲得更完整的體驗。\n是否繼續啟動? diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 0331188d4d..693f51d43b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -714,6 +714,7 @@ launch.advice.modlauncher8=你所使用的 Forge 版本与当前使用的 Java launch.advice.newer_java=检测到你正在使用旧版本 Java 启动游戏,这可能导致部分模组引发游戏崩溃。建议更新至 Java 8 后再次启动。 launch.advice.not_enough_space=你设置的内存分配值过大,超过了系统内存容量 %d MiB,可能导致游戏无法启动。 launch.advice.require_newer_java_version=当前游戏版本需要 Java %s,但 HMCL 未能找到该 Java 版本。你可以点击“是”,HMCL 会自动下载它。是否下载?\n如遇到问题,你可以点击右上角帮助按钮进行求助。 +launch.advice.switch_java=是否要切换到 Java %1$s? launch.advice.too_large_memory_for_32bit=你设置的内存分配值过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏。请将内存分配值调至 1024 MiB 或更小。\n如遇到问题,你可以点击右上角帮助按钮进行求助。 launch.advice.vanilla_linux_java_8=对于 Linux x86-64 平台,Minecraft 1.12.2 及更低版本与 Java 9+ 不兼容,请使用 Java 8 启动游戏。\n如遇到问题,你可以点击右上角帮助按钮进行求助。 launch.advice.vanilla_x86.translation=Minecraft 尚未为你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n你可以在 这里 下载 x86-64 架构的 Java 以获得更完整的体验。 From 827221f44d0537a295dfb8380213cef8715a0b2b Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 06:06:05 +0800 Subject: [PATCH 04/13] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20AUTO=20=E5=88=86?= =?UTF-8?q?=E6=94=AF=E5=BC=B9=E7=AA=97=E5=90=8E=E7=BC=BA=E5=B0=91=20return?= =?UTF-8?q?=20result=EF=BC=8C=E5=AF=BC=E8=87=B4=E7=94=A8=E6=88=B7=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E8=A2=AB=E5=BF=BD=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index b68f9deb15..9062776e76 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -472,6 +472,7 @@ private static Task checkGameState(HMCLGameInstance gameInstance, G MessageType.WARNING, () -> switchToExpectedJava(gameInstance, compatibility, preferred, future, breakAction), () -> future.complete(java)); + return result; } // Reset invalid java version From 48df5d42f9f122b8810017cc40b81c02249298a1 Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 06:12:42 +0800 Subject: [PATCH 05/13] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E4=B8=8E=E7=B1=BB=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/setting/GameSettings.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java index 799afe2f3b..58a1862e3c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java @@ -352,6 +352,23 @@ public InheritableProperty javaTypeProperty() { return javaType; } + /// Property name for the Java version mismatch the user has accepted, in the form + /// {@code actualMajor:expectedMajor}. + public static final String PROPERTY_JAVA_MISMATCH_ACKNOWLEDGED = "javaMismatchAcknowledged"; + + /// The Java version mismatch the user has accepted for this instance, in the form + /// {@code actualMajor:expectedMajor}. + /// + /// When it matches the current situation, the warning is suppressed. Cleared when the + /// game crashes because of a Java version mismatch, so the user is asked again. + @SerializedName(PROPERTY_JAVA_MISMATCH_ACKNOWLEDGED) + private final InheritableProperty javaMismatchAcknowledged = newInheritableProperty(PROPERTY_JAVA_MISMATCH_ACKNOWLEDGED, ""); + + /// Returns the acknowledged Java version mismatch property. + public InheritableProperty javaMismatchAcknowledgedProperty() { + return javaMismatchAcknowledged; + } + /// Property name for the user input used by `VERSION` Java selection mode. public static final String PROPERTY_CUSTOM_JAVA_VERSION = "customJavaVersion"; @@ -928,6 +945,40 @@ public void setJavaAutoSelected() { target.javaTypeProperty().setValue(JavaVersionType.AUTO); } + /// Checks whether any Java version mismatch has been acknowledged. + /// + /// Used to skip crash analysis when there is nothing to revoke, which is the common + /// case and keeps the per-launch cost at zero for users who never dismissed a warning. + public boolean hasJavaMismatchAcknowledgement() { + String value = getInheritable(GameSettings::javaMismatchAcknowledgedProperty); + return value != null && !value.isEmpty(); + } + + /// Checks whether the user already accepted launching this instance on + /// {@code actualMajor} while it expects {@code expectedMajor}. + public boolean isJavaMismatchAcknowledged(int actualMajor, int expectedMajor) { + return (actualMajor + ":" + expectedMajor) + .equals(getInheritable(GameSettings::javaMismatchAcknowledgedProperty)); + } + + /// Records that the user accepted the risk of a newer Java version. + /// + /// Stored per instance so the warning is not shown again while the situation is + /// unchanged and the game keeps starting normally. + public void acknowledgeJavaMismatch(int actualMajor, int expectedMajor) { + GameSettings target = instance != null ? instance : preset; + target.javaMismatchAcknowledgedProperty().setValue(actualMajor + ":" + expectedMajor); + } + + /// Forgets the acknowledgement so the warning is shown again on the next launch. + /// + /// Called when the game crashed because of a Java version mismatch: the user accepted + /// the risk, and it did materialise. + public void clearJavaMismatchAcknowledgement() { + GameSettings target = instance != null ? instance : preset; + target.javaMismatchAcknowledgedProperty().setValue(""); + } + /// Finds the effective Java runtime. public @Nullable JavaRuntime getJava(@Nullable GameVersionNumber gameVersion, @Nullable GameInstanceManifest manifest) throws InterruptedException { JavaVersionType javaVersionType = getInheritable(GameSettings::javaTypeProperty); From c9eeb88bb1ad6652b8792287c98c5220da78ed1b Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 06:12:42 +0800 Subject: [PATCH 06/13] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E4=B8=8E=E7=B1=BB=E6=96=B9=E6=B3=95=E4=B8=8E=E5=B8=B8=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/setting/GameSettings.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java index 799afe2f3b..58a1862e3c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java @@ -352,6 +352,23 @@ public InheritableProperty javaTypeProperty() { return javaType; } + /// Property name for the Java version mismatch the user has accepted, in the form + /// {@code actualMajor:expectedMajor}. + public static final String PROPERTY_JAVA_MISMATCH_ACKNOWLEDGED = "javaMismatchAcknowledged"; + + /// The Java version mismatch the user has accepted for this instance, in the form + /// {@code actualMajor:expectedMajor}. + /// + /// When it matches the current situation, the warning is suppressed. Cleared when the + /// game crashes because of a Java version mismatch, so the user is asked again. + @SerializedName(PROPERTY_JAVA_MISMATCH_ACKNOWLEDGED) + private final InheritableProperty javaMismatchAcknowledged = newInheritableProperty(PROPERTY_JAVA_MISMATCH_ACKNOWLEDGED, ""); + + /// Returns the acknowledged Java version mismatch property. + public InheritableProperty javaMismatchAcknowledgedProperty() { + return javaMismatchAcknowledged; + } + /// Property name for the user input used by `VERSION` Java selection mode. public static final String PROPERTY_CUSTOM_JAVA_VERSION = "customJavaVersion"; @@ -928,6 +945,40 @@ public void setJavaAutoSelected() { target.javaTypeProperty().setValue(JavaVersionType.AUTO); } + /// Checks whether any Java version mismatch has been acknowledged. + /// + /// Used to skip crash analysis when there is nothing to revoke, which is the common + /// case and keeps the per-launch cost at zero for users who never dismissed a warning. + public boolean hasJavaMismatchAcknowledgement() { + String value = getInheritable(GameSettings::javaMismatchAcknowledgedProperty); + return value != null && !value.isEmpty(); + } + + /// Checks whether the user already accepted launching this instance on + /// {@code actualMajor} while it expects {@code expectedMajor}. + public boolean isJavaMismatchAcknowledged(int actualMajor, int expectedMajor) { + return (actualMajor + ":" + expectedMajor) + .equals(getInheritable(GameSettings::javaMismatchAcknowledgedProperty)); + } + + /// Records that the user accepted the risk of a newer Java version. + /// + /// Stored per instance so the warning is not shown again while the situation is + /// unchanged and the game keeps starting normally. + public void acknowledgeJavaMismatch(int actualMajor, int expectedMajor) { + GameSettings target = instance != null ? instance : preset; + target.javaMismatchAcknowledgedProperty().setValue(actualMajor + ":" + expectedMajor); + } + + /// Forgets the acknowledgement so the warning is shown again on the next launch. + /// + /// Called when the game crashed because of a Java version mismatch: the user accepted + /// the risk, and it did materialise. + public void clearJavaMismatchAcknowledgement() { + GameSettings target = instance != null ? instance : preset; + target.javaMismatchAcknowledgedProperty().setValue(""); + } + /// Finds the effective Java runtime. public @Nullable JavaRuntime getJava(@Nullable GameVersionNumber gameVersion, @Nullable GameInstanceManifest manifest) throws InterruptedException { JavaVersionType javaVersionType = getInheritable(GameSettings::javaTypeProperty); From 50961c4a545d013cc5a408ec01c6a7bb4bb0b05f Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 06:24:39 +0800 Subject: [PATCH 07/13] =?UTF-8?q?=E8=AE=B0=E4=BD=8F=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=8E=A5=E5=8F=97=E7=9A=84=20Java=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E4=B8=8D=E5=8C=B9=E9=85=8D=EF=BC=8C=E4=BB=85=E5=9C=A8=E5=9B=A0?= =?UTF-8?q?=E6=AD=A4=E5=B4=A9=E6=BA=83=E6=97=B6=E9=87=8D=E6=96=B0=E6=8F=90?= =?UTF-8?q?=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/LauncherHelper.java | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 8afc4c5b36..fc47a1ccbb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -468,6 +468,12 @@ private static Task checkGameState(HMCLGameInstance gameInstance, G if (compatibility.isOk()) return Task.completed(java); + // The user already accepted this exact combination, and the game has not + // crashed because of Java since. Do not ask again. + if (setting.isJavaMismatchAcknowledged( + compatibility.actualMajor(), compatibility.targetMajor())) + return Task.completed(java); + CompletableFuture future = new CompletableFuture<>(); Task result = Task.fromCompletableFuture(future); Runnable breakAction = () -> future.completeExceptionally( @@ -484,7 +490,14 @@ private static Task checkGameState(HMCLGameInstance gameInstance, G i18n("message.warning"), MessageType.WARNING, () -> switchToExpectedJava(gameInstance, compatibility, preferred, future, breakAction), - () -> future.complete(java)); + () -> { + // The user accepted the risk. Remember it per instance so + // this does not come back on every launch. + setting.acknowledgeJavaMismatch( + compatibility.actualMajor(), compatibility.targetMajor()); + future.complete(java); + }); + return result; } @@ -1128,6 +1141,7 @@ public void onExit(int exitCode, ExitType exitType) { if (exitType != ExitType.NORMAL) { gameInstance.markLaunchedAbnormally(); + revokeJavaMismatchAcknowledgement(logs); runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs).show()); } @@ -1136,6 +1150,31 @@ public void onExit(int exitCode, ExitType exitType) { } +/// Revokes the accepted Java version mismatch when the crash was actually caused by a + /// Java version mismatch, so the next launch warns the user again. + /// + /// Crashes from unrelated causes leave the acknowledgement intact: the user accepted + /// the risk and it did not materialise, so asking again would be nagging. + /// + /// The acknowledgement is stored as {@code actualMajor:expectedMajor} rather than a + /// flag, so a change of either version (a newer Java, an updated instance) asks again + /// instead of staying silent forever. + private void revokeJavaMismatchAcknowledgement(List logs) { + // Nothing to revoke. This is also the common case, and it avoids running the + // crash analysis on every launch for users who never dismissed the warning. + if (!setting.hasJavaMismatchAcknowledgement()) + return; + + String rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n")); + + boolean causedByJava = CrashReportAnalyzer.analyze(rawLog).stream() + .map(CrashReportAnalyzer.Result::rule) + .anyMatch(JAVA_MISMATCH_CRASH_RULES::contains); + + if (causedByJava) + setting.clearJavaMismatchAcknowledgement(); + } + private static final Queue> PROCESSES = new ConcurrentLinkedQueue<>(); public static int countMangedProcesses() { From b02007d32e5d23e07786be004027d20d49bbde9c Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 06:56:31 +0800 Subject: [PATCH 08/13] =?UTF-8?q?=E6=94=B9=E7=94=A8=20Instance=20=E7=9A=84?= =?UTF-8?q?=E9=9D=9E=E7=BB=A7=E6=89=BF=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/setting/GameSettings.java | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java index 58a1862e3c..a0bb3841a7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java @@ -159,6 +159,21 @@ public SettingProperty iconProperty() { return icon; } + /// The Java version mismatch the user has accepted for this instance, in the form + /// {@code actualMajor:expectedMajor}. + /// + /// An instance-scoped property rather than an inheritable one: inheritable properties + /// are only readable on an instance once the name is listed in + /// [getOverrideProperties], and that set is maintained by the settings UI, not by + /// [SettingProperty#setValue]. + @SerializedName("javaMismatchAcknowledged") + private final SettingProperty javaMismatchAcknowledged = newSettingProperty("javaMismatchAcknowledged", ""); + + /// Returns the acknowledged Java version mismatch property. + public SettingProperty javaMismatchAcknowledgedProperty() { + return javaMismatchAcknowledged; + } + /// Setting property names overridden by this instance. @SerializedName("overrideProperties") private final ObservableSet overrideProperties = FXCollections.observableSet(); @@ -352,23 +367,6 @@ public InheritableProperty javaTypeProperty() { return javaType; } - /// Property name for the Java version mismatch the user has accepted, in the form - /// {@code actualMajor:expectedMajor}. - public static final String PROPERTY_JAVA_MISMATCH_ACKNOWLEDGED = "javaMismatchAcknowledged"; - - /// The Java version mismatch the user has accepted for this instance, in the form - /// {@code actualMajor:expectedMajor}. - /// - /// When it matches the current situation, the warning is suppressed. Cleared when the - /// game crashes because of a Java version mismatch, so the user is asked again. - @SerializedName(PROPERTY_JAVA_MISMATCH_ACKNOWLEDGED) - private final InheritableProperty javaMismatchAcknowledged = newInheritableProperty(PROPERTY_JAVA_MISMATCH_ACKNOWLEDGED, ""); - - /// Returns the acknowledged Java version mismatch property. - public InheritableProperty javaMismatchAcknowledgedProperty() { - return javaMismatchAcknowledged; - } - /// Property name for the user input used by `VERSION` Java selection mode. public static final String PROPERTY_CUSTOM_JAVA_VERSION = "customJavaVersion"; @@ -950,15 +948,13 @@ public void setJavaAutoSelected() { /// Used to skip crash analysis when there is nothing to revoke, which is the common /// case and keeps the per-launch cost at zero for users who never dismissed a warning. public boolean hasJavaMismatchAcknowledgement() { - String value = getInheritable(GameSettings::javaMismatchAcknowledgedProperty); - return value != null && !value.isEmpty(); + return !acknowledgedValue().isEmpty(); } /// Checks whether the user already accepted launching this instance on /// {@code actualMajor} while it expects {@code expectedMajor}. public boolean isJavaMismatchAcknowledged(int actualMajor, int expectedMajor) { - return (actualMajor + ":" + expectedMajor) - .equals(getInheritable(GameSettings::javaMismatchAcknowledgedProperty)); + return (actualMajor + ":" + expectedMajor).equals(acknowledgedValue()); } /// Records that the user accepted the risk of a newer Java version. @@ -966,8 +962,8 @@ public boolean isJavaMismatchAcknowledged(int actualMajor, int expectedMajor) { /// Stored per instance so the warning is not shown again while the situation is /// unchanged and the game keeps starting normally. public void acknowledgeJavaMismatch(int actualMajor, int expectedMajor) { - GameSettings target = instance != null ? instance : preset; - target.javaMismatchAcknowledgedProperty().setValue(actualMajor + ":" + expectedMajor); + if (instance != null) + instance.javaMismatchAcknowledgedProperty().setValue(actualMajor + ":" + expectedMajor); } /// Forgets the acknowledgement so the warning is shown again on the next launch. @@ -975,8 +971,15 @@ public void acknowledgeJavaMismatch(int actualMajor, int expectedMajor) { /// Called when the game crashed because of a Java version mismatch: the user accepted /// the risk, and it did materialise. public void clearJavaMismatchAcknowledgement() { - GameSettings target = instance != null ? instance : preset; - target.javaMismatchAcknowledgedProperty().setValue(""); + if (instance != null) + instance.javaMismatchAcknowledgedProperty().setValue(""); + } + + private String acknowledgedValue() { + if (instance == null) + return ""; + String value = instance.javaMismatchAcknowledgedProperty().getValue(); + return value != null ? value : ""; } /// Finds the effective Java runtime. From 1c6b04d7f7383e65ef146040a4f638b6183cb5ca Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 07:23:03 +0800 Subject: [PATCH 09/13] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E7=9A=84=20instance=20=E7=9A=84=E6=AD=BB=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/setting/GameSettings.java | 39 ------------------- 1 file changed, 39 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java index a0bb3841a7..7e7c45693a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java @@ -943,45 +943,6 @@ public void setJavaAutoSelected() { target.javaTypeProperty().setValue(JavaVersionType.AUTO); } - /// Checks whether any Java version mismatch has been acknowledged. - /// - /// Used to skip crash analysis when there is nothing to revoke, which is the common - /// case and keeps the per-launch cost at zero for users who never dismissed a warning. - public boolean hasJavaMismatchAcknowledgement() { - return !acknowledgedValue().isEmpty(); - } - - /// Checks whether the user already accepted launching this instance on - /// {@code actualMajor} while it expects {@code expectedMajor}. - public boolean isJavaMismatchAcknowledged(int actualMajor, int expectedMajor) { - return (actualMajor + ":" + expectedMajor).equals(acknowledgedValue()); - } - - /// Records that the user accepted the risk of a newer Java version. - /// - /// Stored per instance so the warning is not shown again while the situation is - /// unchanged and the game keeps starting normally. - public void acknowledgeJavaMismatch(int actualMajor, int expectedMajor) { - if (instance != null) - instance.javaMismatchAcknowledgedProperty().setValue(actualMajor + ":" + expectedMajor); - } - - /// Forgets the acknowledgement so the warning is shown again on the next launch. - /// - /// Called when the game crashed because of a Java version mismatch: the user accepted - /// the risk, and it did materialise. - public void clearJavaMismatchAcknowledgement() { - if (instance != null) - instance.javaMismatchAcknowledgedProperty().setValue(""); - } - - private String acknowledgedValue() { - if (instance == null) - return ""; - String value = instance.javaMismatchAcknowledgedProperty().getValue(); - return value != null ? value : ""; - } - /// Finds the effective Java runtime. public @Nullable JavaRuntime getJava(@Nullable GameVersionNumber gameVersion, @Nullable GameInstanceManifest manifest) throws InterruptedException { JavaVersionType javaVersionType = getInheritable(GameSettings::javaTypeProperty); From 4c724a682b8ad7cedfd9c0709ca1e236479bb05a Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 07:29:17 +0800 Subject: [PATCH 10/13] =?UTF-8?q?=E4=BF=9D=E8=AF=81=20"javaMismatchAcknowl?= =?UTF-8?q?edged":=20"25:17"=20=E6=AD=A3=E5=B8=B8=E5=B7=A5=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/LauncherHelper.java | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index fc47a1ccbb..837430f039 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -470,8 +470,7 @@ private static Task checkGameState(HMCLGameInstance gameInstance, G // The user already accepted this exact combination, and the game has not // crashed because of Java since. Do not ask again. - if (setting.isJavaMismatchAcknowledged( - compatibility.actualMajor(), compatibility.targetMajor())) + if (isJavaMismatchAcknowledged(gameInstance, compatibility)) return Task.completed(java); CompletableFuture future = new CompletableFuture<>(); @@ -493,8 +492,8 @@ private static Task checkGameState(HMCLGameInstance gameInstance, G () -> { // The user accepted the risk. Remember it per instance so // this does not come back on every launch. - setting.acknowledgeJavaMismatch( - compatibility.actualMajor(), compatibility.targetMajor()); + writeJavaMismatchAcknowledgement(gameInstance, + compatibility.actualMajor() + ":" + compatibility.targetMajor()); future.complete(java); }); @@ -782,6 +781,36 @@ else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA)) return task.withStage("launch.state.java"); } + /// The acknowledged Java version mismatch for this instance, in the form + /// {@code actualMajor:expectedMajor}, or an empty string when there is none. + /// + /// Read through {@link HMCLGameInstance#getSettings} rather than the effective settings: + /// {@link GameSettings.Effective#getInstance} is null for every instance that has no + /// instance-specific settings file, which is the common case. + private static String readJavaMismatchAcknowledgement(HMCLGameInstance gameInstance) { + GameSettings.Instance instance = gameInstance.getSettings(); + if (instance == null) + return ""; + + String value = instance.javaMismatchAcknowledgedProperty().getValue(); + return value != null ? value : ""; + } + + /// Records the Java version mismatch the user accepted. + /// + /// {@link HMCLGameInstance#getSettingsOrCreate} creates the instance settings file when + /// it does not exist yet, which is what makes this persist for instances that had none. + private static void writeJavaMismatchAcknowledgement(HMCLGameInstance gameInstance, String value) { + GameSettings.Instance instance = gameInstance.getSettingsOrCreate(); + if (instance != null) + instance.javaMismatchAcknowledgedProperty().setValue(value); + } + + private static boolean isJavaMismatchAcknowledged(HMCLGameInstance gameInstance, JavaCompatibility compatibility) { + return (compatibility.actualMajor() + ":" + compatibility.targetMajor()) + .equals(readJavaMismatchAcknowledgement(gameInstance)); + } + /// Returns an installed runtime whose major version matches {@code major}, or null if /// none is installed. /// @@ -1162,7 +1191,7 @@ public void onExit(int exitCode, ExitType exitType) { private void revokeJavaMismatchAcknowledgement(List logs) { // Nothing to revoke. This is also the common case, and it avoids running the // crash analysis on every launch for users who never dismissed the warning. - if (!setting.hasJavaMismatchAcknowledgement()) + if (readJavaMismatchAcknowledgement(gameInstance).isEmpty()) return; String rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n")); @@ -1172,7 +1201,7 @@ private void revokeJavaMismatchAcknowledgement(List logs) { .anyMatch(JAVA_MISMATCH_CRASH_RULES::contains); if (causedByJava) - setting.clearJavaMismatchAcknowledgement(); + writeJavaMismatchAcknowledgement(gameInstance, ""); } private static final Queue> PROCESSES = new ConcurrentLinkedQueue<>(); From e8b3e53c597fb6ba4b288dc2127db34080f27c48 Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 21:23:25 +0800 Subject: [PATCH 11/13] =?UTF-8?q?=E8=A1=A5=E5=85=A8=E6=8D=A2=E8=A1=8C?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/game/JavaCompatibility.java | 2 +- .../org/jackhuang/hmcl/game/JavaCompatibilityEvaluator.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibility.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibility.java index 6f90e360e9..a0e2b09c22 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibility.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibility.java @@ -37,4 +37,4 @@ public enum Level { public boolean isOk() { return level == Level.OK; } -} \ No newline at end of file +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibilityEvaluator.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibilityEvaluator.java index 20e0ee5ab3..ef9caa19d9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibilityEvaluator.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaCompatibilityEvaluator.java @@ -28,8 +28,8 @@ /// instance expects. /// /// This is a pure function: no IO, no UI, no settings access. That is what keeps it -/// decoupled from [org.jackhuang.hmcl.java.JavaManager] (which picks a runtime) and -/// from the launch flow (which decides what to do about a deviation). +/// decoupled from {@code JavaManager} (which picks a runtime) and from the launch flow +/// (which decides what to do about a deviation). /// /// ### Why this does not hardcode version ranges /// @@ -172,4 +172,4 @@ private static int upperBound(int targetMajor, int steps) { int bounded = Math.min(index + steps, KNOWN_RUNTIMES.size() - 1); return KNOWN_RUNTIMES.get(bounded).majorVersion(); } -} \ No newline at end of file +} From 18630e5ae7ec3c49d177b863f8c4002f37659a74 Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Tue, 1 Sep 2026 21:29:28 +0800 Subject: [PATCH 12/13] =?UTF-8?q?=E5=B0=86=E6=96=B9=E6=B3=95=E4=B8=A2?= =?UTF-8?q?=E5=9B=9E=E5=86=85=E5=B1=82=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/game/LauncherHelper.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 837430f039..3963f72848 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -1177,9 +1177,7 @@ public void onExit(int exitCode, ExitType exitType) { checkExit(); } - } - -/// Revokes the accepted Java version mismatch when the crash was actually caused by a + /// Revokes the accepted Java version mismatch when the crash was actually caused by a /// Java version mismatch, so the next launch warns the user again. /// /// Crashes from unrelated causes leave the acknowledgement intact: the user accepted @@ -1204,6 +1202,8 @@ private void revokeJavaMismatchAcknowledgement(List logs) { writeJavaMismatchAcknowledgement(gameInstance, ""); } + } + private static final Queue> PROCESSES = new ConcurrentLinkedQueue<>(); public static int countMangedProcesses() { From 8376222355f886c59cfa0fb3d4fcd93d28e00cbc Mon Sep 17 00:00:00 2001 From: Chen-Mengze Date: Wed, 2 Sep 2026 21:53:37 +0800 Subject: [PATCH 13/13] =?UTF-8?q?=E5=A5=97=E7=94=A8=20findSuitableJava=20?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E5=A5=97=E6=9E=B6=E6=9E=84=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=EF=BC=88=E5=90=AB=20ARM64=20Win/macOS=20=E4=B8=94=20MC=20<=201?= =?UTF-8?q?.6=20=E7=9A=84=20forceX86=EF=BC=89=E3=80=82=20=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E4=B8=8D=E6=94=AF=E6=8C=81=E6=97=B6=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E5=BD=93=E5=89=8D=20Java=20=E7=BB=A7=E7=BB=AD=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=EF=BC=8C=E4=B8=8D=E5=8F=96=E6=B6=88=E3=80=82=20Join?= =?UTF-8?q?=20=E4=B9=8B=E5=90=8E=E5=86=8D=E6=8A=8A=20FX=20=E9=98=9F?= =?UTF-8?q?=E5=88=97=20flush=20=E6=8E=89=EF=BC=88runLater=20=E6=98=AF=20FI?= =?UTF-8?q?FO=EF=BC=8C=E6=8F=92=E4=B8=80=E4=B8=AA=20latch=20=E5=B9=B6=20aw?= =?UTF-8?q?ait=20=E5=8D=B3=E5=8F=AF=EF=BC=89=EF=BC=8C=E9=A1=BA=E4=BE=BF?= =?UTF-8?q?=E7=BB=99=20logs=20=E7=9A=84=E8=AF=BB=E5=8F=96=E5=8A=A0?= =?UTF-8?q?=E9=94=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/game/LauncherHelper.java | 106 ++++++++++++++---- 1 file changed, 87 insertions(+), 19 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 3963f72848..62cb08842d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -481,14 +481,14 @@ private static Task checkGameState(HMCLGameInstance gameInstance, G // Resolved before the dialog is shown: JavaManager.getAllJava() blocks // until the initial Java scan finishes, so it must not run inside a // JavaFX callback or it will freeze the UI. - JavaRuntime preferred = findInstalledJava(compatibility.targetMajor()); + JavaRuntime preferred = findInstalledJava(gameVersion, compatibility.targetMajor()); Controllers.confirm( i18n("launch.advice.modded_java", compatibility.targetMajor(), gameVersion) + "\n\n" + i18n("launch.advice.switch_java", compatibility.targetMajor()), i18n("message.warning"), MessageType.WARNING, - () -> switchToExpectedJava(gameInstance, compatibility, preferred, future, breakAction), + () -> switchToExpectedJava(gameInstance, compatibility, preferred, java, future, breakAction), () -> { // The user accepted the risk. Remember it per instance so // this does not come back on every launch. @@ -811,24 +811,56 @@ private static boolean isJavaMismatchAcknowledged(HMCLGameInstance gameInstance, .equals(readJavaMismatchAcknowledgement(gameInstance)); } - /// Returns an installed runtime whose major version matches {@code major}, or null if - /// none is installed. + /// Returns an installed runtime that can run this instance on the expected Java version, + /// or null if there is none. See [pickInstalledJava] for the selection rules. /// /// {@link JavaManager#getAllJava} blocks until the initial Java scan completes, so this /// is resolved before the confirmation dialog is shown rather than inside its callback. - private static @Nullable JavaRuntime findInstalledJava(int major) { + private static @Nullable JavaRuntime findInstalledJava(@Nullable GameVersionNumber gameVersion, int major) { try { - for (JavaRuntime installed : JavaManager.getAllJava()) { - if (installed.getParsedVersion() == major) - return installed; - } + return pickInstalledJava(JavaManager.getAllJava(), gameVersion, major); } catch (InterruptedException e) { // Preserve the interrupt so the shutdown path can observe it. Thread.currentThread().interrupt(); + return null; + } + } + + /// Picks the runtime to offer from {@code candidates}: matching major version, and an + /// architecture that can actually run this instance. + /// + /// The architecture filter mirrors {@link JavaManager#findSuitableJava}: AUTO picked the + /// current runtime for a reason, and offering a 32-bit runtime on a 64-bit system (or a + /// non-x86 one for the versions that need it on Windows/macOS ARM64) would trade a + /// warning for a game that cannot allocate its heap. + static @Nullable JavaRuntime pickInstalledJava( + Collection candidates, @Nullable GameVersionNumber gameVersion, int major) { + + boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64 + && (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) + && (gameVersion == null || gameVersion.compareTo("1.6") < 0); + + for (JavaRuntime candidate : candidates) { + if (forceX86 ? !candidate.getArchitecture().isX86() + : candidate.getArchitecture() != Architecture.SYSTEM_ARCH) + continue; + + if (candidate.getParsedVersion() == major) + return candidate; } return null; } + /// The Mojang runtime to download for {@code major} on {@code platform}, or null when + /// Mojang publishes none for it. + static @Nullable GameJavaVersion resolveDownloadableJava(Platform platform, int major) { + GameJavaVersion target = GameJavaVersion.get(major); + if (target == null) + return null; + + return GameJavaVersion.getSupportedVersions(platform).contains(target) ? target : null; + } + /// Switches to, or downloads, the Java version the instance is expected to run on. /// /// Note: this deliberately does not call `setting.setJavaAutoSelected()`. Silently @@ -838,6 +870,7 @@ private static void switchToExpectedJava( HMCLGameInstance gameInstance, JavaCompatibility compatibility, @Nullable JavaRuntime preferred, + @Nullable JavaRuntime currentJava, CompletableFuture future, Runnable breakAction) { @@ -846,10 +879,17 @@ private static void switchToExpectedJava( return; } - GameJavaVersion target = GameJavaVersion.get(compatibility.targetMajor()); + // Mojang does not publish a runtime for every platform (Linux ARM64, for instance). + // Starting a download there can only fail, and failing it cancels a launch the user + // already agreed to, so keep the runtime they have instead. + GameJavaVersion target = resolveDownloadableJava(SYSTEM_PLATFORM, compatibility.targetMajor()); if (target == null) { - // No published runtime for this major version, so there is nothing to offer. - breakAction.run(); + LOG.warning("No downloadable Java " + compatibility.targetMajor() + " for " + SYSTEM_PLATFORM + + ", keeping the current runtime"); + if (currentJava != null) + future.complete(currentJava); + else + breakAction.run(); return; } @@ -1150,6 +1190,12 @@ public void onExit(int exitCode, ExitType exitType) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + + // A batch that holds a single log is handed to the FX thread with runLater + // and is never waited for, so the last line can still be sitting in the event + // queue here. Flush it, or the exception that ended the game is missing from + // `logs` when the crash is analysed below. + flushPendingFxEvents(); } launchingLatch.countDown(); @@ -1170,7 +1216,7 @@ public void onExit(int exitCode, ExitType exitType) { if (exitType != ExitType.NORMAL) { gameInstance.markLaunchedAbnormally(); - revokeJavaMismatchAcknowledgement(logs); + revokeJavaMismatchAcknowledgement(); runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs).show()); } @@ -1186,13 +1232,19 @@ public void onExit(int exitCode, ExitType exitType) { /// The acknowledgement is stored as {@code actualMajor:expectedMajor} rather than a /// flag, so a change of either version (a newer Java, an updated instance) asks again /// instead of staying silent forever. - private void revokeJavaMismatchAcknowledgement(List logs) { + private void revokeJavaMismatchAcknowledgement() { // Nothing to revoke. This is also the common case, and it avoids running the // crash analysis on every launch for users who never dismissed the warning. if (readJavaMismatchAcknowledgement(gameInstance).isEmpty()) return; - String rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n")); + String rawLog; + lock.lock(); + try { + rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n")); + } finally { + lock.unlock(); + } boolean causedByJava = CrashReportAnalyzer.analyze(rawLog).stream() .map(CrashReportAnalyzer.Result::rule) @@ -1202,6 +1254,26 @@ private void revokeJavaMismatchAcknowledgement(List logs) { writeJavaMismatchAcknowledgement(gameInstance, ""); } + /// Waits until every task queued on the JavaFX application thread so far has run. + /// + /// [javafx.application.Platform#runLater] executes tasks in submission order, so a + /// task submitted now runs after every task submitted before it. + /// + /// `javafx.application.Platform` is spelled out because the on-demand import of + /// `org.jackhuang.hmcl.util.platform` already owns the simple name `Platform` here. + private void flushPendingFxEvents() { + if (javafx.application.Platform.isFxApplicationThread()) + return; + + CountDownLatch latch = new CountDownLatch(1); + runLater(latch::countDown); + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } private static final Queue> PROCESSES = new ConcurrentLinkedQueue<>(); @@ -1214,8 +1286,4 @@ public static int countMangedProcesses() { return PROCESSES.size(); } - public static void stopManagedProcesses() { - while (!PROCESSES.isEmpty()) - Optional.ofNullable(PROCESSES.poll()).map(WeakReference::get).ifPresent(ManagedProcess::stop); - } }