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 aaef6607bc..71a2fafd42 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -82,6 +82,19 @@ public final class LauncherHelper { private static final String LWJGL_3_4_1_TIP = "lwjgl3.4.1-ffm"; + /// Crash rules that mean the crash really was caused by a Java version mismatch. + /// + /// Reused from [CrashReportAnalyzer] rather than matching log text here, so patterns + /// added upstream apply automatically. + private static final Set JAVA_MISMATCH_CRASH_RULES = EnumSet.of( + CrashReportAnalyzer.Rule.JAVA_VERSION_IS_TOO_HIGH, + CrashReportAnalyzer.Rule.JDK_9, + CrashReportAnalyzer.Rule.NEED_JDK11, + CrashReportAnalyzer.Rule.MODLAUNCHER_8, + CrashReportAnalyzer.Rule.TOO_OLD_JAVA, + CrashReportAnalyzer.Rule.MAC_JDK_8U261 + ); + private final HMCLGameInstance gameInstance; private Account account; private Path scriptFile; @@ -444,7 +457,46 @@ 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); + + // The user already accepted this exact combination, and the game has not + // crashed because of Java since. Do not ask again. + if (isJavaMismatchAcknowledged(gameInstance, compatibility)) + 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(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, java, future, breakAction), + () -> { + // The user accepted the risk. Remember it per instance so + // this does not come back on every launch. + writeJavaMismatchAcknowledgement(gameInstance, + compatibility.actualMajor() + ":" + compatibility.targetMajor()); + future.complete(java); + }); + + return result; } // Reset invalid java version @@ -728,6 +780,129 @@ 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 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(@Nullable GameVersionNumber gameVersion, int major) { + try { + 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 + /// 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, + @Nullable JavaRuntime currentJava, + CompletableFuture future, + Runnable breakAction) { + + if (preferred != null) { + future.complete(preferred); + return; + } + + // 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) { + LOG.warning("No downloadable Java " + compatibility.targetMajor() + " for " + SYSTEM_PLATFORM + + ", keeping the current runtime"); + if (currentJava != null) + future.complete(currentJava); + else + 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( @@ -1014,6 +1189,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(); @@ -1034,12 +1215,64 @@ public void onExit(int exitCode, ExitType exitType) { if (exitType != ExitType.NORMAL) { gameInstance.markLaunchedAbnormally(); + revokeJavaMismatchAcknowledgement(); runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs).show()); } checkExit(); } + /// 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() { + // 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; + 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) + .anyMatch(JAVA_MISMATCH_CRASH_RULES::contains); + + if (causedByJava) + 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<>(); @@ -1052,8 +1285,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); - } } 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..7e7c45693a 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(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 96f06124ec..4a26d12e6e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -897,6 +897,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 059c0fe977..00f0210084 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 b03771e524..5d70f1f671 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 3eec4b737c..21614a3230 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 caff311186..bc1d0352ce 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -709,6 +709,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 ed8b84ace4..006847af40 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -715,6 +715,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 以获得更完整的体验。 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..a0e2b09c22 --- /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; + } +} 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..ef9caa19d9 --- /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 {@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 +/// +/// `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(); + } +}