From a58de31d4b5e19b71a86ec2c757b219758be155c Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Mon, 27 Sep 2021 20:53:25 +0800 Subject: [PATCH] feat: auto select java. Closes #1068. --- .../hmcl/game/HMCLGameRepository.java | 3 +- .../jackhuang/hmcl/game/HMCLJavaVersion.java | 93 ----- .../jackhuang/hmcl/game/LauncherHelper.java | 386 +++++++++--------- .../hmcl/setting/VersionSetting.java | 14 +- .../hmcl/ui/construct/MultiFileItem.java | 16 +- .../hmcl/ui/versions/VersionSettingsPage.java | 50 ++- .../resources/assets/lang/I18N.properties | 2 + .../resources/assets/lang/I18N_zh.properties | 2 + .../assets/lang/I18N_zh_CN.properties | 7 +- .../hmcl/game/JavaVersionConstraint.java | 162 ++++++++ .../java/org/jackhuang/hmcl/task/Task.java | 41 +- .../hmcl/game/JavaVersionConstraintTest.java | 39 ++ .../hmcl/util/VersionNumberTest.java | 4 + 13 files changed, 484 insertions(+), 335 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLJavaVersion.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/game/JavaVersionConstraintTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 1c16d43c8..96833d5f6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -293,10 +293,9 @@ public class HMCLGameRepository extends DefaultGameRepository { vs.setUsesGlobal(true); } - public LaunchOptions getLaunchOptions(String version, File gameDir, boolean checkJava) throws InterruptedException { + public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir) { VersionSetting vs = getVersionSetting(version); - JavaVersion javaVersion = Optional.ofNullable(vs.getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment()); LaunchOptions.Builder builder = new LaunchOptions.Builder() .setGameDir(gameDir) .setJava(javaVersion) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLJavaVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLJavaVersion.java deleted file mode 100644 index f61d92d78..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLJavaVersion.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.jackhuang.hmcl.game; - -import org.jackhuang.hmcl.util.Range; -import org.jackhuang.hmcl.util.platform.JavaVersion; -import org.jackhuang.hmcl.util.versioning.VersionNumber; -import org.jetbrains.annotations.Nullable; - -import static org.jackhuang.hmcl.download.LibraryAnalyzer.LAUNCH_WRAPPER_MAIN; - -public enum HMCLJavaVersion { - - // Minecraft>=1.17 requires Java 16 - VANILLA_JAVA_16(HMCLJavaVersion.RULE_MANDATORY, versionRange("1.17", HMCLJavaVersion.MAX), versionRange("16", HMCLJavaVersion.MAX)), - // Minecraft>=1.13 requires Java 8 - VANILLA_JAVA_8(HMCLJavaVersion.RULE_MANDATORY, versionRange("1.13", HMCLJavaVersion.MAX), versionRange("8", HMCLJavaVersion.MAX)), - // Minecraft>=1.7.10+Forge accepts Java 8 - SUGGEST_JAVA_8(HMCLJavaVersion.RULE_SUGGESTED, versionRange("1.7.10", HMCLJavaVersion.MAX), versionRange("8", HMCLJavaVersion.MAX)), - // LaunchWrapper<=1.12 will crash because of assuming the system class loader is an instance of URLClassLoader (Java 8) - LAUNCH_WRAPPER(HMCLJavaVersion.RULE_MANDATORY, versionRange("0", "1.12"), versionRange("0", "8")) { - @Override - public boolean test(Version version) { - return LAUNCH_WRAPPER_MAIN.equals(version.getMainClass()) && - version.getLibraries().stream() - .filter(library -> "launchwrapper".equals(library.getArtifactId())) - .anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0); - } - }, - // Minecraft>=1.13 may crash when generating world on Java [1.8,1.8.0_51) - VANILLA_JAVA_8_51(HMCLJavaVersion.RULE_SUGGESTED, versionRange("1.13", HMCLJavaVersion.MAX), versionRange("1.8.0_51", HMCLJavaVersion.MAX)), - - ; - - private final int type; - private final Range gameVersion; - private final Range javaVersion; - - HMCLJavaVersion(int type, Range gameVersion, Range javaVersion) { - this.type = type; - this.gameVersion = gameVersion; - this.javaVersion = javaVersion; - } - - public boolean test(Version version) { - return true; - } - - @Nullable - public static JavaVersion findSuitableJavaVersion(VersionNumber gameVersion, Version version) throws InterruptedException { - Range mandatoryJavaRange = versionRange(MIN, MAX); - Range suggestedJavaRange = versionRange(MIN, MAX); - for (HMCLJavaVersion java : values()) { - if (java.gameVersion.contains(gameVersion) && java.test(version)) { - if (java.type == RULE_MANDATORY) { - mandatoryJavaRange = mandatoryJavaRange.intersectionWith(java.javaVersion); - suggestedJavaRange = suggestedJavaRange.intersectionWith(java.javaVersion); - } else if (java.type == RULE_SUGGESTED) { - suggestedJavaRange = suggestedJavaRange.intersectionWith(java.javaVersion); - } - } - } - - JavaVersion mandatory = null; - JavaVersion suggested = null; - for (JavaVersion javaVersion : JavaVersion.getJavas()) { - // select the latest java version that this version accepts. - if (mandatoryJavaRange.contains(javaVersion.getVersionNumber())) { - if (mandatory == null) mandatory = javaVersion; - else if (javaVersion.getVersionNumber().compareTo(mandatory.getVersionNumber()) > 0) { - mandatory = javaVersion; - } - } - if (suggestedJavaRange.contains(javaVersion.getVersionNumber())) { - if (suggested == null) suggested = javaVersion; - else if (javaVersion.getVersionNumber().compareTo(suggested.getVersionNumber()) > 0) { - suggested = javaVersion; - } - } - } - - if (suggested != null) return suggested; - else return mandatory; - } - - public static final int RULE_MANDATORY = 1; - public static final int RULE_SUGGESTED = 2; - - public static final String MIN = "0"; - public static final String MAX = "10000"; - - private static Range versionRange(String fromInclusive, String toExclusive) { - return Range.between(VersionNumber.asVersion(fromInclusive), VersionNumber.asVersion(toExclusive)); - } -} 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 1ae40f22b..afae52993 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -24,7 +24,6 @@ import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.auth.*; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.MaintainTask; import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask; import org.jackhuang.hmcl.download.game.GameVerificationFixTask; @@ -64,9 +63,11 @@ import java.net.SocketTimeoutException; import java.net.URL; import java.util.*; import java.util.concurrent.*; +import java.util.function.Consumer; import java.util.logging.Level; import static org.jackhuang.hmcl.setting.ConfigHolder.config; +import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Pair.pair; @@ -110,13 +111,10 @@ public final class LauncherHelper { Version version = repository.getResolvedVersion(selectedVersion); Platform.runLater(() -> { - try { - checkGameState(profile, setting, version, () -> { - Controllers.dialog(launchingStepsPane); - Schedulers.defaultScheduler().execute(this::launch0); - }); - } catch (InterruptedException | RejectedExecutionException ignore) { - } + checkGameState(profile, setting, version, javaVersion -> { + Controllers.dialog(launchingStepsPane); + Schedulers.defaultScheduler().execute(() -> launch0(javaVersion)); + }); }); } @@ -126,7 +124,7 @@ public final class LauncherHelper { launch(); } - private void launch0() { + private void launch0(JavaVersion javaVersion) { HMCLGameRepository repository = profile.getRepository(); DefaultDependencyManager dependencyManager = profile.getDependency(); Version version = MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion)); @@ -175,7 +173,7 @@ public final class LauncherHelper { } }).withStage("launch.state.logging_in")) .thenComposeAsync(authInfo -> Task.supplyAsync(() -> { - LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, profile.getGameDir(), !setting.isNotCheckJVM()); + LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersion, profile.getGameDir()); return new HMCLGameLauncher( repository, version, @@ -301,224 +299,204 @@ public final class LauncherHelper { executor.start(); } - private static void checkGameState(Profile profile, VersionSetting setting, Version version, Runnable onAccept) throws InterruptedException { + private static void checkGameState(Profile profile, VersionSetting setting, Version version, Consumer onAccept) { + VersionNumber gameVersion = VersionNumber.asVersion(profile.getRepository().getGameVersion(version).orElse("Unknown")); + if (setting.isNotCheckJVM()) { - onAccept.run(); + Task.composeAsync(() -> { + return setting.getJavaVersion(gameVersion, version); + }).thenAcceptAsync(javaVersion -> { + onAccept.accept(Optional.ofNullable(javaVersion).orElseGet(JavaVersion::fromCurrentEnvironment)); + }).start(); return; } - boolean javaChanged = false; - boolean mayContinueAfterJavaChanged = false; - boolean java8required = false; - boolean newJavaRequired = false; + Task.composeAsync(() -> { + return setting.getJavaVersion(gameVersion, version); + }).thenComposeAsync(Schedulers.javafx(), javaVersion -> { + // Reset invalid java version + if (javaVersion == null) { + CompletableFuture future = new CompletableFuture<>(); + Runnable continueAction = () -> future.complete(JavaVersion.fromCurrentEnvironment()); - // Without onAccept called, the launching operation will be terminated. - - VersionNumber gameVersion = VersionNumber.asVersion(profile.getRepository().getGameVersion(version).orElse("Unknown")); - if (setting.getJavaVersion() == null) { - Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, onAccept); - setting.setJava(null); - setting.setDefaultJavaPath(null); - setting.setJavaVersion(JavaVersion.fromCurrentEnvironment()); - // continue java version checking - } - - // Check java version recorded in game json - // We only checks for 1.7.10 and above, since 1.7.2 with Forge can only run on Java 7, but it is recorded Java 8 in game json, which is not correct. - if (!javaChanged && version.getJavaVersion() != null && gameVersion.compareTo(VersionNumber.asVersion("1.7.10")) >= 0) { - if (setting.getJavaVersion().getParsedVersion() < version.getJavaVersion().getMajorVersion()) { - Optional acceptableJava = JavaVersion.getJavas().stream() - .filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion()) - .max(Comparator.comparing(JavaVersion::getVersionNumber)); - if (acceptableJava.isPresent()) { - setting.setJavaVersion(acceptableJava.get()); - mayContinueAfterJavaChanged = true; + if (setting.isJavaAutoSelected()) { + Controllers.dialog(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction); } else { - MessageDialogPane dialog = new MessageDialogPane( - i18n("launch.advice.require_newer_java_version", - gameVersion.toString(), - version.getJavaVersion().getMajorVersion()), - i18n("message.warning"), - MessageType.QUESTION); + Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction); - JFXButton linkButton = new JFXButton(i18n("download.external_link")); - linkButton.setOnAction(e -> FXUtils.openLink("https://adoptopenjdk.net/")); - linkButton.getStyleClass().add("dialog-accept"); - dialog.addButton(linkButton); + setting.setJava(null); + setting.setDefaultJavaPath(null); + setting.setJavaVersion(JavaVersion.fromCurrentEnvironment()); + } - JFXButton yesButton = new JFXButton(i18n("button.ok")); - yesButton.setOnAction(event -> { - downloadJava(version.getJavaVersion(), profile) - .thenAcceptAsync(x -> { - try { - Optional newAcceptableJava = JavaVersion.getJavas().stream() - .filter(javaVersion -> javaVersion.getParsedVersion() >= version.getJavaVersion().getMajorVersion()) - .max(Comparator.comparing(JavaVersion::getVersionNumber)); - newAcceptableJava.ifPresent(setting::setJavaVersion); - } catch (InterruptedException e) { - LOG.log(Level.SEVERE, "Cannot list javas", e); - } - }, Platform::runLater).thenAccept(x -> onAccept.run()); - }); - yesButton.getStyleClass().add("dialog-accept"); - dialog.addButton(yesButton); + return Task.fromCompletableFuture(future); + } else { + return Task.completed(javaVersion); + } + }).thenComposeAsync(javaVersion -> { + return Task.allOf(Task.completed(javaVersion), Task.supplyAsync(() -> JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version))); + }).thenComposeAsync(Schedulers.javafx(), javaVersions -> { + JavaVersion javaVersion = (JavaVersion) javaVersions.get(0); + JavaVersion suggestedJavaVersion = (JavaVersion) javaVersions.get(1); + if (setting.isJavaAutoSelected()) return Task.completed(javaVersion); - JFXButton noButton = new JFXButton(i18n("button.cancel")); - noButton.getStyleClass().add("dialog-cancel"); - dialog.addButton(noButton); - dialog.setCancelButton(noButton); + JavaVersionConstraint violatedMandatoryConstraint = null; + JavaVersionConstraint violatedSuggestedConstraint = null; + for (JavaVersionConstraint constraint : JavaVersionConstraint.values()) { + if (constraint.getGameVersion().contains(gameVersion) && constraint.appliesToVersion(gameVersion, version)) { + if (!constraint.getJavaVersion(version).contains(javaVersion.getVersionNumber())) { + if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) { + violatedMandatoryConstraint = constraint; + } else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) { + violatedSuggestedConstraint = constraint; + } + } - Controllers.dialog(dialog); - return; } } - } - // Game later than 1.17 requires Java 16. - if (!javaChanged && setting.getJavaVersion().getParsedVersion() < JavaVersion.JAVA_16 && gameVersion.compareTo(VersionNumber.asVersion("1.17")) >= 0) { - Optional acceptableJava = JavaVersion.getJavas().stream() - .filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_16) - .max(Comparator.comparing(JavaVersion::getVersionNumber)); - if (acceptableJava.isPresent()) { - setting.setJavaVersion(acceptableJava.get()); - mayContinueAfterJavaChanged = true; - } else { - Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"), () -> { - FXUtils.openLink("https://adoptopenjdk.net/"); - }, null); - } - javaChanged = true; - } + boolean suggested = false; + CompletableFuture future = new CompletableFuture<>(); + Runnable continueAction = () -> future.complete(javaVersion); + Runnable breakAction = () -> { + future.completeExceptionally(new CancellationException("Launch operation was cancelled by user")); + }; - // Game later than 1.7.2 accepts Java 8. - if (!javaChanged && setting.getJavaVersion().getParsedVersion() < JavaVersion.JAVA_8 && gameVersion.compareTo(VersionNumber.asVersion("1.7.2")) > 0) { - Optional java8 = JavaVersion.getJavas().stream() - .filter(javaVersion -> javaVersion.getParsedVersion() >= JavaVersion.JAVA_8) - .max(Comparator.comparing(JavaVersion::getVersionNumber)); - if (java8.isPresent()) { - newJavaRequired = true; - setting.setJavaVersion(java8.get()); - } else { - if (gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0) { - // Minecraft 1.13 and later versions only support Java 8 or later. - // Terminate launching operation. - Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, null); + if (violatedMandatoryConstraint != null) { + if (suggestedJavaVersion != null) { + Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> { + setting.setJavaAutoSelected(); + future.complete(suggestedJavaVersion); + }, breakAction); + return Task.fromCompletableFuture(future); } else { - // Most mods require Java 8 or later version. - Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, onAccept); + switch (violatedMandatoryConstraint) { + case GAME_JSON: + MessageDialogPane dialog = new MessageDialogPane( + i18n("launch.advice.require_newer_java_version", + gameVersion.toString(), + version.getJavaVersion().getMajorVersion()), + i18n("message.warning"), + MessageType.QUESTION); + + JFXButton linkButton = new JFXButton(i18n("download.external_link")); + linkButton.setOnAction(e -> FXUtils.openLink("https://adoptopenjdk.net/")); + linkButton.getStyleClass().add("dialog-accept"); + dialog.addButton(linkButton); + + JFXButton yesButton = new JFXButton(i18n("button.ok")); + yesButton.setOnAction(event -> { + downloadJava(version.getJavaVersion(), profile) + .thenAcceptAsync(x -> { + try { + Optional newAcceptableJava = JavaVersion.getJavas().stream() + .filter(newJava -> newJava.getParsedVersion() >= version.getJavaVersion().getMajorVersion()) + .max(Comparator.comparing(JavaVersion::getVersionNumber)); + if (newAcceptableJava.isPresent()) { + setting.setJavaVersion(newAcceptableJava.get()); + future.complete(newAcceptableJava.get()); + return; + } + } catch (InterruptedException e) { + LOG.log(Level.SEVERE, "Cannot list javas", e); + } + future.complete(javaVersion); + }, Platform::runLater) + .exceptionally(Lang.handleUncaught); + }); + yesButton.getStyleClass().add("dialog-accept"); + dialog.addButton(yesButton); + + JFXButton noButton = new JFXButton(i18n("button.cancel")); + noButton.getStyleClass().add("dialog-cancel"); + dialog.addButton(noButton); + dialog.setCancelButton(noButton); + + Controllers.dialog(dialog); + return Task.fromCompletableFuture(future); + case VANILLA_JAVA_16: + Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"), () -> { + FXUtils.openLink("https://adoptopenjdk.net/"); + }, breakAction); + return null; + case VANILLA_JAVA_8: + Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction); + return null; + case LAUNCH_WRAPPER: + Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction); + return null; + } } - javaChanged = true; - } - } - - // LaunchWrapper 1.12 will crash because of assuming the system class loader is an instance of URLClassLoader. - if (!javaChanged && setting.getJavaVersion().getParsedVersion() >= JavaVersion.JAVA_9 - && gameVersion.compareTo(VersionNumber.asVersion("1.13")) < 0 - && LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass()) - && version.getLibraries().stream() - .filter(library -> "launchwrapper".equals(library.getArtifactId())) - .anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) { - Optional java8 = JavaVersion.getJavas().stream().filter(javaVersion -> javaVersion.getParsedVersion() == JavaVersion.JAVA_8).findAny(); - if (java8.isPresent()) { - java8required = true; - setting.setJavaVersion(java8.get()); - Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.corrected"), i18n("message.info"), MessageType.INFO, onAccept); - javaChanged = true; - } else { - Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, null); - javaChanged = true; - } - } - - // Minecraft 1.13 may crash when generating world on Java 8 earlier than 1.8.0_51 - VersionNumber JAVA_8 = VersionNumber.asVersion("1.8.0_51"); - if (!javaChanged && gameVersion.compareTo(VersionNumber.asVersion("1.13")) >= 0 && setting.getJavaVersion().getParsedVersion() == JavaVersion.JAVA_8 && setting.getJavaVersion().getVersionNumber().compareTo(JAVA_8) < 0) { - Optional java8 = JavaVersion.getJavas().stream() - .filter(javaVersion -> javaVersion.getVersionNumber().compareTo(JAVA_8) >= 0) - .max(Comparator.comparing(JavaVersion::getVersionNumber)); - if (java8.isPresent()) { - newJavaRequired = true; - setting.setJavaVersion(java8.get()); - } else { - Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageType.WARNING, onAccept); - javaChanged = true; - } - } - - if (!javaChanged && setting.getJavaVersion().getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 && - Architecture.CURRENT.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_64) { - final JavaVersion java32 = setting.getJavaVersion(); - - // First find if same java version but whose platform is 64-bit installed. - Optional java64 = JavaVersion.getJavas().stream() - .filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.getPlatform()) - .filter(javaVersion -> javaVersion.getParsedVersion() == java32.getParsedVersion()) - .max(Comparator.comparing(JavaVersion::getVersionNumber)); - - if (!java64.isPresent()) { - final boolean java8requiredFinal = java8required, newJavaRequiredFinal = newJavaRequired; - - // Then find if other java version which satisfies requirements installed. - java64 = JavaVersion.getJavas().stream() - .filter(javaVersion -> javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.getPlatform()) - .filter(javaVersion -> { - if (java8requiredFinal) return javaVersion.getParsedVersion() == JavaVersion.JAVA_8; - if (newJavaRequiredFinal) return javaVersion.getParsedVersion() >= JavaVersion.JAVA_8; - return true; - }) - .max(Comparator.comparing(JavaVersion::getVersionNumber)); } - if (java64.isPresent()) { - setting.setJavaVersion(java64.get()); - } else { - Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.error"), MessageType.ERROR, onAccept); - javaChanged = true; + // 32-bit JVM cannot make use of too much memory. + if (javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 && + setting.getMaxMemory() > 1.5 * 1024) { + // 1.5 * 1024 is an inaccurate number. + // Actual memory limit depends on operating system and memory. + Controllers.confirm(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), continueAction, null); + return null; } - } - // 32-bit JVM cannot make use of too much memory. - if (!javaChanged && setting.getJavaVersion().getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 && - setting.getMaxMemory() > 1.5 * 1024) { - // 1.5 * 1024 is an inaccurate number. - // Actual memory limit depends on operating system and memory. - Controllers.confirm(i18n("launch.advice.too_large_memory_for_32bit"), i18n("message.error"), onAccept, null); - javaChanged = true; - } - - // Cannot allocate too much memory exceeding free space. - if (!javaChanged && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) { - Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), onAccept, null); - javaChanged = true; - } - - // Forge 2760~2773 will crash game with LiteLoader. - if (!javaChanged) { - boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge")) - .anyMatch(it -> - VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) <= 0 && - VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0); - boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader")); - if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) { - Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), onAccept, null); - javaChanged = true; + if (violatedSuggestedConstraint != null) { + suggested = true; + switch (violatedSuggestedConstraint) { + case MODDED_JAVA_7: + Controllers.dialog(i18n("launch.advice.java.modded_java_7"), i18n("message.error"), MessageType.ERROR, continueAction); + return null; + case MODDED_JAVA_8: + Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, continueAction); + break; + case VANILLA_JAVA_8_51: + Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageType.WARNING, continueAction); + break; + } } - } - // OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions. - if (!javaChanged) { - boolean hasForge28_2_2 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge")) - .anyMatch(it -> - VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0); - boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine")); - if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) { - Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), onAccept, null); - javaChanged = true; + if (!suggested && javaVersion.getPlatform() != Architecture.CURRENT.getPlatform()) { + Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.warning"), MessageType.ERROR, continueAction); + suggested = true; } - } - if (!javaChanged || mayContinueAfterJavaChanged) - onAccept.run(); + // Cannot allocate too much memory exceeding free space. + if (!suggested && OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) { + Controllers.confirm(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY), i18n("message.error"), continueAction, null); + suggested = true; + } + + // Forge 2760~2773 will crash game with LiteLoader. + if (!suggested) { + boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge")) + .anyMatch(it -> + VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) <= 0 && + VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0); + boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader")); + if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) { + Controllers.confirm(i18n("launch.advice.forge2760_liteloader"), i18n("message.error"), continueAction, null); + suggested = true; + } + } + + // OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions. + if (!suggested) { + boolean hasForge28_2_2 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge")) + .anyMatch(it -> + VersionNumber.VERSION_COMPARATOR.compare("1.14.4-28.2.2", it.getVersion()) <= 0); + boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine")); + if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo(VersionNumber.asVersion("1.14.4")) == 0) { + Controllers.confirm(i18n("launch.advice.forge28_2_2_optifine"), i18n("message.error"), continueAction, null); + suggested = true; + } + } + + return Task.fromCompletableFuture(future); + }).thenAcceptAsync(Schedulers.javafx(), javaVersion -> { + if (javaVersion == null) { + return; + } + + onAccept.accept(javaVersion); + }).start(); } private static CompletableFuture downloadJava(GameJavaVersion javaVersion, Profile profile) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java index 379f94705..cdcaf8c94 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java @@ -22,6 +22,7 @@ import com.google.gson.annotations.JsonAdapter; import javafx.beans.InvalidationListener; import javafx.beans.property.*; import org.jackhuang.hmcl.game.*; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.platform.JavaVersion; @@ -36,7 +37,6 @@ import java.nio.file.Paths; import java.util.List; import java.util.Optional; import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; /** @@ -125,6 +125,10 @@ public final class VersionSetting implements Cloneable { return defaultJavaPathProperty.get(); } + public StringProperty defaultJavaPathPropertyProperty() { + return defaultJavaPathProperty; + } + public void setDefaultJavaPath(String defaultJavaPath) { defaultJavaPathProperty.set(defaultJavaPath); } @@ -581,19 +585,19 @@ public final class VersionSetting implements Cloneable { launcherVisibilityProperty.set(launcherVisibility); } - public CompletableFuture getJavaVersion(String gameVersion, Version version) { + public Task getJavaVersion(VersionNumber gameVersion, Version version) { return getJavaVersion(gameVersion, version, true); } - public CompletableFuture getJavaVersion(String gameVersion, Version version, boolean checkJava) { - return CompletableFuture.supplyAsync(() -> { + public Task getJavaVersion(VersionNumber gameVersion, Version version, boolean checkJava) { + return Task.supplyAsync(() -> { try { if (StringUtils.isBlank(getJava())) setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom"); if ("Default".equals(getJava())) { return JavaVersion.fromCurrentEnvironment(); } else if (isJavaAutoSelected()) { - return HMCLJavaVersion.findSuitableJavaVersion(VersionNumber.asVersion(gameVersion), version); + return JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version); } else if (isUsesCustomJavaDir()) { try { if (checkJava) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java index eed9edcaa..cd15677c5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java @@ -116,6 +116,7 @@ public class MultiFileItem extends VBox { protected String subtitle; protected final T data; protected final BooleanProperty selected = new SimpleBooleanProperty(); + protected final JFXRadioButton left = new JFXRadioButton(); public Option(String title, T data) { this.title = title; @@ -140,15 +141,15 @@ public class MultiFileItem extends VBox { } public boolean isSelected() { - return selected.get(); + return left.isSelected(); } public BooleanProperty selectedProperty() { - return selected; + return left.selectedProperty(); } public void setSelected(boolean selected) { - this.selected.set(selected); + left.setSelected(selected); } protected Node createItem(ToggleGroup group) { @@ -156,11 +157,10 @@ public class MultiFileItem extends VBox { pane.setPadding(new Insets(3)); FXUtils.setLimitHeight(pane, 30); - JFXRadioButton left = new JFXRadioButton(title); + left.setText(title); BorderPane.setAlignment(left, Pos.CENTER_LEFT); left.setToggleGroup(group); left.setUserData(data); - selected.bind(left.selectedProperty()); pane.setLeft(left); if (StringUtils.isNotBlank(subtitle)) { @@ -218,11 +218,10 @@ public class MultiFileItem extends VBox { pane.setPadding(new Insets(3)); FXUtils.setLimitHeight(pane, 30); - JFXRadioButton left = new JFXRadioButton(title); + left.setText(title); BorderPane.setAlignment(left, Pos.CENTER_LEFT); left.setToggleGroup(group); left.setUserData(data); - selected.bind(left.selectedProperty()); pane.setLeft(left); BorderPane.setAlignment(customField, Pos.CENTER_RIGHT); @@ -277,11 +276,10 @@ public class MultiFileItem extends VBox { pane.setPadding(new Insets(3)); FXUtils.setLimitHeight(pane, 30); - JFXRadioButton left = new JFXRadioButton(title); + left.setText(title); BorderPane.setAlignment(left, Pos.CENTER_LEFT); left.setToggleGroup(group); left.setUserData(data); - selected.bind(left.selectedProperty()); pane.setLeft(left); selector.disableProperty().bind(left.selectedProperty().not()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java index 7d969c548..6aec2d2bf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionSettingsPage.java @@ -31,10 +31,7 @@ import javafx.scene.image.Image; import javafx.scene.layout.*; import javafx.scene.text.Text; import javafx.stage.FileChooser; -import org.jackhuang.hmcl.game.GameDirectoryType; -import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.NativesDirectoryType; -import org.jackhuang.hmcl.game.ProcessPriority; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.setting.LauncherVisibility; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; @@ -53,6 +50,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.javafx.SafeStringConverter; import org.jackhuang.hmcl.util.platform.JavaVersion; import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.versioning.VersionNumber; import java.io.File; import java.io.IOException; @@ -674,6 +672,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag }); versionSetting.javaDirProperty().addListener(javaListener); + versionSetting.defaultJavaPathPropertyProperty().addListener(javaListener); versionSetting.javaProperty().addListener(javaListener); gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty()); @@ -686,7 +685,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag lastVersionSetting = versionSetting; - initializeSelectedJava(); initJavaSubtitle(); loadIcon(); @@ -699,26 +697,44 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag } if (lastVersionSetting.isUsesCustomJavaDir()) { - javaItem.getGroup().getToggles().stream() - .filter(java -> java.getUserData() == null) - .findFirst().get() - .setSelected(true); + javaCustomOption.setSelected(true); + } else if (lastVersionSetting.isJavaAutoSelected()) { + javaAutoDeterminedOption.setSelected(true); } else { - try { - javaItem.setSelectedData(lastVersionSetting.getJavaVersion()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } +// javaLoading.set(true); + lastVersionSetting.getJavaVersion(null, null) + .thenAcceptAsync(Schedulers.javafx(), javaVersion -> { + javaItem.setSelectedData(javaVersion); +// javaLoading.set(false); + }).start(); } } private void initJavaSubtitle() { + FXUtils.checkFxUserThread(); + initializeSelectedJava(); VersionSetting versionSetting = lastVersionSetting; if (versionSetting == null) return; - Task.fromCompletableFuture(versionSetting.getJavaVersion()) - .thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion) - .map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path"))) + Profile profile = this.profile; + String versionId = this.versionId; + boolean autoSelected = versionSetting.isJavaAutoSelected(); + + if (autoSelected && versionId == null) { + javaSublist.setSubtitle(i18n("settings.game.java_directory.auto")); + return; + } + + Task.composeAsync(Schedulers.javafx(), () -> { + if (versionId == null) { + return versionSetting.getJavaVersion(VersionNumber.asVersion("Unknown"), null); + } else { + return versionSetting.getJavaVersion( + VersionNumber.asVersion(GameVersion.minecraftVersion(profile.getRepository().getVersionJar(versionId)).orElse("Unknown")), + profile.getRepository().getVersion(versionId)); + } + }).thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion) + .map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path"))) .start(); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 39ef5efe9..6abad1c08 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -405,6 +405,8 @@ install.success=successfully installed lang=English lang.default=Use system language +launch.advice.java.auto=Currently selected Java VM does not fulfill the requirement of game. Do you allow us to selecting a suitable Java VM? Or you can select a proper Java VM in game settings. +launch.advice.java.modded_java_7=Minecraft 1.7.2 or earlier requires Java 7 or earlier version. launch.advice.corrected=We have already fixed the JVM selection. If you want to keep your choice of Java version, you can disable the Java VM check in game settings. launch.advice.uncorrected=If you are sure that the game can be started normally, you can disable the Java VM check in game settings. launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index e25c98656..4d73f3014 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -405,6 +405,8 @@ install.success=安裝成功 lang=正體中文 lang.default=使用系統語言 +launch.advice.java.auto=當前選擇的 Java 虛擬機版本不滿足遊戲要求,是否自動選擇合適的 Java 虛擬機版本?或者你可以到遊戲設置中選擇一個合適的 Java 虛擬機版本。 +launch.advice.java.modded_java_7=Minecraft 1.7.2 及以下版本需要 Java 7 及以下版本。 launch.advice.corrected=我們已經修正了問題。如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。 launch.advice.uncorrected=如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。 launch.advice.different_platform=您的系統為 64 位元,但是 Java 是 32 位元,建議您安裝 64 位 Java。 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 d6b311ef9..2cab61e92 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -349,7 +349,7 @@ game.crash.reason.openj9=当前游戏无法运行在 OpenJ9 虚拟机上,请 game.crash.reason.out_of_memory=当前游戏因为内存不足,无法继续运行。\n这可能是内存分配太小,或者 Mod 数量过多导致的。\n你可以在游戏设置中调大游戏内存分配值以允许游戏在更大的内存下运行。\n如果仍然出现该错误,你可能需要换一台更好的电脑。 game.crash.reason.resolution_too_high=当前游戏因为材质包分辨率过高,无法继续运行\n你可以更换一个分辨率更低的材质,或者更换一个显存更大的显卡。 game.crash.reason.stacktrace=原因未知,请点击日志按钮查看详细信息。\n下面是一些关键词,其中可能包含 Mod 名称,你可以通过搜索的方式查找有关信息。\n%s -game.crash.reason.too_old_java=当前游戏因为 Java 虚拟机版本过低,无法继续运行。\n你需要在游戏设置中更换 %1$s 或更新版本的 Java 虚拟机,并重新启动游戏。如果没有下载安装,你可以在网上自行下载。 +game.crash.reason.too_old_java=当前游戏因为 Java 虚拟机版本过低,无法继续运行。\n你需要在游戏设置中更换 Java %1$s 或更新版本的 Java 虚拟机,并重新启动游戏。如果没有下载安装,你可以在 https://adoptopenjdk.net 上自行下载。 game.crash.reason.unknown=原因未知,请点击日志按钮查看详细信息。 game.crash.title=游戏意外退出 game.directory=游戏路径 @@ -405,7 +405,9 @@ install.success=安装成功 lang=简体中文 lang.default=跟随系统语言 -launch.advice.corrected=我们已经修复了问题。如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。 +launch.advice.java.auto=当前选择的 Java 虚拟机版本不满足游戏要求。是否自动选择合适的 Java 虚拟机版本?或者你可以到游戏设置中选择一个合适的 Java 虚拟机版本。 +launch.advice.java.modded_java_7=Minecraft 1.7.2 及以下版本需要 Java 7 及以下版本。 +launch.advice.corrected=我们已经修复了 Java 虚拟机版本问题。如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。 launch.advice.uncorrected=如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。 launch.advice.different_platform=您的系统是 64 位,但是 Java 是 32 位的,建议您安装 64 位 Java。 launch.advice.forge2760_liteloader=Forge 2760 与 LiteLoader 不兼容。请删除 LiteLoader 或者更换 Forge 至 2773 或更新的版本。是否继续启动? @@ -768,6 +770,7 @@ settings.game.java_directory=Java 路径 settings.game.java_directory.auto=自动选择合适的 Java settings.game.java_directory.bit=,%s 位 settings.game.java_directory.choose=选择 Java 路径 +settings.game. settings.game.management=管理 settings.game.working_directory=版本隔离(修改后请自行移动相关游戏文件,如存档模组配置等) settings.game.working_directory.choose=选择运行路径 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java new file mode 100644 index 000000000..dc3590910 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java @@ -0,0 +1,162 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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.util.Pair; +import org.jackhuang.hmcl.util.Range; +import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.util.versioning.VersionNumber; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +import static org.jackhuang.hmcl.download.LibraryAnalyzer.LAUNCH_WRAPPER_MAIN; +import static org.jackhuang.hmcl.util.Pair.pair; + +public enum JavaVersionConstraint { + + // Minecraft>=1.17 requires Java 16 + VANILLA_JAVA_16(JavaVersionConstraint.RULE_MANDATORY, versionRange("1.17", JavaVersionConstraint.MAX), versionRange("16", JavaVersionConstraint.MAX)), + // Minecraft>=1.13 requires Java 8 + VANILLA_JAVA_8(JavaVersionConstraint.RULE_MANDATORY, versionRange("1.13", JavaVersionConstraint.MAX), versionRange("1.8", JavaVersionConstraint.MAX)), + // Minecraft>=1.7.10+Forge accepts Java 8 + MODDED_JAVA_8(JavaVersionConstraint.RULE_SUGGESTED, versionRange("1.7.10", JavaVersionConstraint.MAX), versionRange("1.8", JavaVersionConstraint.MAX)), + // Minecraft<=1.7.2+Forge requires Java<=7 + MODDED_JAVA_7(JavaVersionConstraint.RULE_SUGGESTED, versionRange(JavaVersionConstraint.MIN, "1.7.2"), versionRange(JavaVersionConstraint.MIN, "1.7.999")) { + @Override + public boolean appliesToVersion(@Nullable VersionNumber gameVersion, @Nullable Version version) { + if (version == null) return false; + return LAUNCH_WRAPPER_MAIN.equals(version.getMainClass()); + } + }, + // LaunchWrapper<=1.12 will crash because of assuming the system class loader is an instance of URLClassLoader (Java 8) + LAUNCH_WRAPPER(JavaVersionConstraint.RULE_MANDATORY, versionRange("0", "1.12"), versionRange("0", "1.8")) { + @Override + public boolean appliesToVersion(VersionNumber gameVersion, Version version) { + if (version == null) return false; + return LAUNCH_WRAPPER_MAIN.equals(version.getMainClass()) && + version.getLibraries().stream() + .filter(library -> "launchwrapper".equals(library.getArtifactId())) + .anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0); + } + }, + // Minecraft>=1.13 may crash when generating world on Java [1.8,1.8.0_51) + VANILLA_JAVA_8_51(JavaVersionConstraint.RULE_SUGGESTED, versionRange("1.13", JavaVersionConstraint.MAX), versionRange("1.8.0_51", JavaVersionConstraint.MAX)), + // Minecraft with suggested java version recorded in game json is restrictedly constrained. + GAME_JSON(JavaVersionConstraint.RULE_MANDATORY, versionRange(JavaVersionConstraint.MIN, JavaVersionConstraint.MAX), versionRange(JavaVersionConstraint.MIN, JavaVersionConstraint.MAX)) { + @Override + public boolean appliesToVersion(VersionNumber gameVersion, Version version) { + if (gameVersion == null || version == null) return false; + // We only checks for 1.7.10 and above, since 1.7.2 with Forge can only run on Java 7, but it is recorded Java 8 in game json, which is not correct. + return gameVersion.compareTo(VersionNumber.asVersion("1.7.10")) >= 0 && version.getJavaVersion() != null; + } + + @Override + public Range getJavaVersion(Version version) { + String javaVersion; + if (Objects.requireNonNull(version.getJavaVersion()).getMajorVersion() >= 9) { + javaVersion = "" + version.getJavaVersion().getMajorVersion(); + } else { + javaVersion = "1." + version.getJavaVersion().getMajorVersion(); + } + return JavaVersionConstraint.versionRange(javaVersion, JavaVersionConstraint.MAX); + } + }, + ; + + private final int type; + private final Range gameVersion; + private final Range javaVersion; + + JavaVersionConstraint(int type, Range gameVersion, Range javaVersion) { + this.type = type; + this.gameVersion = gameVersion; + this.javaVersion = javaVersion; + } + + public int getType() { + return type; + } + + public Range getGameVersion() { + return gameVersion; + } + + public Range getJavaVersion(Version version) { + return javaVersion; + } + + public boolean appliesToVersion(@Nullable VersionNumber gameVersion, @Nullable Version version) { + return true; + } + + public static Pair, Range> findSuitableJavaVersionRange(VersionNumber gameVersion, Version version) { + Range mandatoryJavaRange = versionRange(MIN, MAX); + Range suggestedJavaRange = versionRange(MIN, MAX); + for (JavaVersionConstraint java : values()) { + if (java.gameVersion.contains(gameVersion) && java.appliesToVersion(gameVersion, version)) { + Range javaVersionRange = java.getJavaVersion(version); + if (java.type == RULE_MANDATORY) { + mandatoryJavaRange = mandatoryJavaRange.intersectionWith(javaVersionRange); + suggestedJavaRange = suggestedJavaRange.intersectionWith(javaVersionRange); + } else if (java.type == RULE_SUGGESTED) { + suggestedJavaRange = suggestedJavaRange.intersectionWith(javaVersionRange); + } + } + } + return pair(mandatoryJavaRange, suggestedJavaRange); + } + + @Nullable + public static JavaVersion findSuitableJavaVersion(VersionNumber gameVersion, Version version) throws InterruptedException { + Pair, Range> range = findSuitableJavaVersionRange(gameVersion, version); + Range mandatoryJavaRange = range.getKey(); + Range suggestedJavaRange = range.getValue(); + + JavaVersion mandatory = null; + JavaVersion suggested = null; + for (JavaVersion javaVersion : JavaVersion.getJavas()) { + // select the latest java version that this version accepts. + if (mandatoryJavaRange.contains(javaVersion.getVersionNumber())) { + if (mandatory == null) mandatory = javaVersion; + else if (javaVersion.getVersionNumber().compareTo(mandatory.getVersionNumber()) > 0) { + mandatory = javaVersion; + } + } + if (suggestedJavaRange.contains(javaVersion.getVersionNumber())) { + if (suggested == null) suggested = javaVersion; + else if (javaVersion.getVersionNumber().compareTo(suggested.getVersionNumber()) > 0) { + suggested = javaVersion; + } + } + } + + if (suggested != null) return suggested; + else return mandatory; + } + + public static final int RULE_MANDATORY = 1; + public static final int RULE_SUGGESTED = 2; + + public static final String MIN = "0"; + public static final String MAX = "10000"; + + private static Range versionRange(String fromInclusive, String toExclusive) { + return Range.between(VersionNumber.asVersion(fromInclusive), VersionNumber.asVersion(toExclusive)); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java index 7423692d6..e8103b2d9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -550,7 +550,7 @@ public abstract class Task { public final Task thenComposeAsync(Task other) { return thenComposeAsync(() -> other); } - + /** * Returns a new Task that, when this task completes * normally, is executed. @@ -560,7 +560,20 @@ public abstract class Task { * @return the Task */ public final Task thenComposeAsync(ExceptionalSupplier, ?> fn) { - return new UniCompose<>(fn, true); + return thenComposeAsync(Schedulers.defaultScheduler(), fn); + } + + /** + * Returns a new Task that, when this task completes + * normally, is executed. + * + * @param fn the function returning a new Task + * @param executor the executor to use for asynchronous execution + * @param the type of the returned Task's result + * @return the Task + */ + public final Task thenComposeAsync(Executor executor, ExceptionalSupplier, ?> fn) { + return new UniCompose<>(fn, true).setExecutor(executor); } /** @@ -573,7 +586,21 @@ public abstract class Task { * @return the Task */ public Task thenComposeAsync(ExceptionalFunction, E> fn) { - return new UniCompose<>(fn, true); + return thenComposeAsync(Schedulers.defaultScheduler(), fn); + } + + /** + * Returns a new Task that, when this task completes + * normally, is executed with result of this task as the argument + * to the supplied function. + * + * @param fn the function returning a new Task + * @param executor the executor to use for asynchronous execution + * @param the type of the returned Task's result + * @return the Task + */ + public Task thenComposeAsync(Executor executor, ExceptionalFunction, E> fn) { + return new UniCompose<>(fn, true).setExecutor(executor); } public final Task withComposeAsync(Task other) { @@ -840,6 +867,10 @@ public abstract class Task { }.setName(name); } + public static Task composeAsync(Executor executor, ExceptionalSupplier, ?> fn) { + return composeAsync(fn).setExecutor(executor); + } + public static Task supplyAsync(Callable callable) { return supplyAsync(getCaller(), callable).setSignificance(TaskSignificance.MODERATE); } @@ -856,6 +887,10 @@ public abstract class Task { return new SimpleTask<>(callable).setExecutor(executor).setName(name); } + public static Task completed(V value) { + return fromCompletableFuture(CompletableFuture.completedFuture(value)); + } + /** * Returns a new Task that is completed when all of the given Tasks * complete. If any of the given Tasks complete exceptionally, diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/JavaVersionConstraintTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/JavaVersionConstraintTest.java new file mode 100644 index 000000000..16b4aa80c --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/JavaVersionConstraintTest.java @@ -0,0 +1,39 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2021 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.util.Pair; +import org.jackhuang.hmcl.util.Range; +import org.jackhuang.hmcl.util.versioning.VersionNumber; +import org.junit.Assert; +import org.junit.Test; + +public class JavaVersionConstraintTest { + + @Test + public void vanillaJava16() { + Pair, Range> range = JavaVersionConstraint.findSuitableJavaVersionRange( + VersionNumber.asVersion("1.17"), + null + ); + + Assert.assertEquals( + Range.between(VersionNumber.asVersion("16"), VersionNumber.asVersion(JavaVersionConstraint.MAX)), + range.getKey()); + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/VersionNumberTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/VersionNumberTest.java index 2615d9a9c..98c9ea3ca 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/VersionNumberTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/VersionNumberTest.java @@ -67,6 +67,10 @@ public class VersionNumberTest { v = VersionNumber.asVersion("1.8.0_11"); Assert.assertTrue(u.compareTo(v) < 0); + u = VersionNumber.asVersion("1.7.0_22"); + v = VersionNumber.asVersion("1.7.99"); + Assert.assertTrue(u.compareTo(v) < 0); + u = VersionNumber.asVersion("1.12.2-14.23.5.2760"); v = VersionNumber.asVersion("1.12.2-14.23.4.2739"); Assert.assertTrue(u.compareTo(v) > 0);