From 37d6857b82cbb70d9b2ebcb9f7b90328ffd1668b Mon Sep 17 00:00:00 2001 From: Zkitefly <2573874409@qq.com> Date: Thu, 3 Oct 2024 00:31:06 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E8=B0=83=E6=95=B4=E4=B8=8B=E8=BD=BD=E9=A1=B9?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=20(#3306)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 3440c928e9..200bfee6b5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -381,8 +381,6 @@ private static final class ModItem extends StackPane { { StackPane graphicPane = new StackPane(); - graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24)); - TwoLineListItem content = new TwoLineListItem(); HBox.setHgrow(content, Priority.ALWAYS); content.setTitle(dataItem.getName()); @@ -390,11 +388,16 @@ private static final class ModItem extends StackPane { switch (dataItem.getVersionType()) { case Alpha: + content.getTags().add(i18n("version.game.snapshot")); + graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24)); + break; case Beta: content.getTags().add(i18n("version.game.snapshot")); + graphicPane.getChildren().setAll(SVG.BETA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24)); break; case Release: content.getTags().add(i18n("version.game.release")); + graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24)); break; } From 7e4d437a1d7c4d98fd1c6566acb226dca938b84c Mon Sep 17 00:00:00 2001 From: Glavo Date: Sat, 5 Oct 2024 23:50:14 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E9=87=8D=E6=9E=84=20Java=20=E7=AE=A1?= =?UTF-8?q?=E7=90=86=20(#2988)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update * update * Update task name * update * update * update * update * update * update * update * update * update * update * Update logo --- .../main/java/org/jackhuang/hmcl/Main.java | 4 +- .../hmcl/game/HMCLGameRepository.java | 4 +- .../jackhuang/hmcl/game/LauncherHelper.java | 493 ++++++------- .../hmcl/java/HMCLJavaRepository.java | 221 ++++++ .../jackhuang/hmcl/java/JavaInstallTask.java | 116 +++ .../jackhuang/hmcl/java/JavaLocalFiles.java | 128 ++++ .../org/jackhuang/hmcl/java/JavaManager.java | 697 ++++++++++++++++++ .../org/jackhuang/hmcl/java/JavaManifest.java | 112 +++ .../jackhuang/hmcl/setting/GlobalConfig.java | 38 +- .../hmcl/setting/JavaVersionType.java | 25 + .../hmcl/setting/VersionSetting.java | 225 ++++-- .../org/jackhuang/hmcl/ui/Controllers.java | 4 +- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 13 + .../main/java/org/jackhuang/hmcl/ui/SVG.java | 3 +- .../hmcl/ui/construct/MultiFileItem.java | 6 +- .../hmcl/ui/construct/TaskListPane.java | 7 +- .../hmcl/ui/main/JavaDownloadDialog.java | 423 +++++++++++ .../hmcl/ui/main/JavaInstallPage.java | 190 +++++ .../hmcl/ui/main/JavaManagementPage.java | 322 ++++++++ .../hmcl/ui/main/JavaRestorePage.java | 206 ++++++ .../hmcl/ui/main/LauncherSettingsPage.java | 11 +- .../hmcl/ui/versions/VersionSettingsPage.java | 229 ++++-- .../jackhuang/hmcl/upgrade/UpdateHandler.java | 4 +- .../jackhuang/hmcl/util/NativePatcher.java | 6 +- .../hmcl/util/SelfDependencyPatcher.java | 4 +- .../org/jackhuang/hmcl/util/i18n/Locales.java | 4 +- .../resources/assets/lang/I18N.properties | 40 +- .../resources/assets/lang/I18N_es.properties | 2 - .../resources/assets/lang/I18N_ja.properties | 2 - .../resources/assets/lang/I18N_ru.properties | 2 - .../resources/assets/lang/I18N_zh.properties | 66 +- .../assets/lang/I18N_zh_CN.properties | 40 +- .../hmcl/ui/GameCrashWindowTest.java | 5 +- .../download/forge/ForgeNewInstallTask.java | 4 +- .../hmcl/download/java/JavaDistribution.java | 36 + .../hmcl/download/java/JavaPackageType.java} | 35 +- .../hmcl/download/java/JavaRemoteVersion.java | 29 + .../hmcl/download/java/JavaRepository.java | 168 ----- .../java/disco/DiscoFetchJavaListTask.java | 95 +++ .../java/disco/DiscoJavaDistribution.java | 116 +++ .../java/disco/DiscoJavaRemoteVersion.java | 259 +++++++ .../java/disco/DiscoRemoteFileInfo.java | 68 ++ .../hmcl/download/java/disco/DiscoResult.java | 49 ++ .../java/mojang/MojangJavaDistribution.java | 55 ++ .../MojangJavaDownloadTask.java} | 95 ++- .../MojangJavaDownloads.java} | 16 +- .../MojangJavaRemoteFiles.java} | 6 +- .../java/mojang/MojangJavaRemoteVersion.java | 56 ++ .../neoforge/NeoForgeOldInstallTask.java | 4 +- .../optifine/OptiFineInstallTask.java | 4 +- .../jackhuang/hmcl/game/GameJavaVersion.java | 102 ++- .../hmcl/game/JavaVersionConstraint.java | 193 ++--- .../jackhuang/hmcl/game/LaunchOptions.java | 8 +- .../org/jackhuang/hmcl/java/JavaInfo.java | 280 +++++++ .../jackhuang/hmcl/java/JavaRepository.java | 44 ++ .../org/jackhuang/hmcl/java/JavaRuntime.java | 156 ++++ .../hmcl/launch/DefaultLauncher.java | 8 +- .../hmcl/util/KeyValuePairProperties.java | 94 +++ .../org/jackhuang/hmcl/util/StringUtils.java | 12 +- .../jackhuang/hmcl/util/gson/JsonUtils.java | 8 + .../org/jackhuang/hmcl/util/io/IOUtils.java | 17 + .../hmcl/util/platform/Architecture.java | 3 + .../hmcl/util/platform/JavaVersion.java | 449 ----------- .../hmcl/util/platform/ManagedProcess.java | 3 +- .../hmcl/util/platform/OperatingSystem.java | 10 +- .../hmcl/util/platform/Platform.java | 5 + .../hmcl/util/platform/SystemUtils.java | 5 +- .../UnsupportedPlatformException.java | 30 + .../hmcl/util/tree/ArchiveFileTree.java | 129 ++++ .../jackhuang/hmcl/util/tree/TarFileTree.java | 133 ++++ .../jackhuang/hmcl/util/tree/ZipFileTree.java | 72 ++ .../hmcl/util/KeyValuePairPropertiesTest.java | 31 + .../org/jackhuang/hmcl/util/TaskTest.java | 8 +- .../hmcl/util/platform/JavaRuntimeTest.java | 18 + 74 files changed, 5227 insertions(+), 1338 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/java/HMCLJavaRepository.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/java/JavaInstallTask.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/java/JavaLocalFiles.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManifest.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/setting/JavaVersionType.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaInstallPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaRestorePage.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaDistribution.java rename HMCLCore/src/{test/java/org/jackhuang/hmcl/game/JavaVersionConstraintTest.java => main/java/org/jackhuang/hmcl/download/java/JavaPackageType.java} (51%) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaRemoteVersion.java delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaRepository.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoFetchJavaListTask.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoJavaDistribution.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoJavaRemoteVersion.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoRemoteFileInfo.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoResult.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaDistribution.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/{JavaDownloadTask.java => mojang/MojangJavaDownloadTask.java} (61%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/{JavaDownloads.java => mojang/MojangJavaDownloads.java} (81%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/{RemoteFiles.java => mojang/MojangJavaRemoteFiles.java} (94%) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaRemoteVersion.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaInfo.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaRepository.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaRuntime.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/KeyValuePairProperties.java delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/UnsupportedPlatformException.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/TarFileTree.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ZipFileTree.java create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/util/KeyValuePairPropertiesTest.java create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/JavaRuntimeTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java index 96ea379797..6009288c74 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java @@ -23,7 +23,7 @@ import org.jackhuang.hmcl.util.FractureiserDetector; import org.jackhuang.hmcl.util.SelfDependencyPatcher; import org.jackhuang.hmcl.ui.SwingUtils; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; import javax.net.ssl.HttpsURLConnection; @@ -61,7 +61,7 @@ public static void main(String[] args) { checkDirectoryPath(); - if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9) + if (JavaRuntime.CURRENT_VERSION < 9) // This environment check will take ~300ms thread(Main::fixLetsEncrypt, "CA Certificate Check", true); 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 4cbff8d75e..861301bf26 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -37,7 +37,7 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.versioning.VersionNumber; import org.jetbrains.annotations.Nullable; @@ -383,7 +383,7 @@ public void globalizeVersionSetting(String id) { vs.setUsesGlobal(true); } - public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir, List javaAgents, boolean makeLaunchScript) { + public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List javaAgents, boolean makeLaunchScript) { VersionSetting vs = getVersionSetting(version); LaunchOptions.Builder builder = new LaunchOptions.Builder() 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 2230728e26..c11bb0aa55 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.game; import com.jfoenix.controls.JFXButton; -import javafx.application.Platform; import javafx.stage.Stage; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.auth.*; @@ -28,15 +27,13 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.MaintainTask; import org.jackhuang.hmcl.download.game.*; -import org.jackhuang.hmcl.download.java.JavaRepository; +import org.jackhuang.hmcl.java.JavaManager; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.launch.*; import org.jackhuang.hmcl.mod.ModpackCompletionException; import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.mod.ModpackProvider; -import org.jackhuang.hmcl.setting.DownloadProviders; -import org.jackhuang.hmcl.setting.LauncherVisibility; -import org.jackhuang.hmcl.setting.Profile; -import org.jackhuang.hmcl.setting.VersionSetting; +import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.ui.*; import org.jackhuang.hmcl.ui.construct.*; @@ -59,10 +56,13 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; +import static javafx.application.Platform.runLater; +import static javafx.application.Platform.setImplicitExit; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.Lang.resolveException; import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.platform.Platform.*; public final class LauncherHelper { @@ -127,12 +127,12 @@ private void launch0() { CountDownLatch launchingLatch = new CountDownLatch(1); List javaAgents = new ArrayList<>(0); - AtomicReference javaVersionRef = new AtomicReference<>(); + AtomicReference javaVersionRef = new AtomicReference<>(); TaskExecutor executor = checkGameState(profile, setting, version.get()) - .thenComposeAsync(javaVersion -> { - javaVersionRef.set(Objects.requireNonNull(javaVersion)); - version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), javaVersion, setting)); + .thenComposeAsync(java -> { + javaVersionRef.set(Objects.requireNonNull(java)); + version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), java, setting)); if (setting.isNotCheckGame()) return null; return Task.allOf( @@ -150,7 +150,7 @@ private void launch0() { Task.composeAsync(() -> { Renderer renderer = setting.getRenderer(); if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { - Library lib = NativePatcher.getMesaLoader(javaVersion, renderer); + Library lib = NativePatcher.getMesaLoader(java, renderer); if (lib == null) return null; File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib); @@ -174,9 +174,7 @@ private void launch0() { }) ); }).withStage("launch.state.dependencies") - .thenComposeAsync(() -> { - return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null); - }) + .thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null)) .thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in")) .thenComposeAsync(authInfo -> Task.supplyAsync(() -> { LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, scriptFile != null); @@ -209,7 +207,7 @@ private void launch0() { it.fireEvent(new DialogCloseEvent()); })); } else { - Platform.runLater(() -> { + runLater(() -> { launchingStepsPane.fireEvent(new DialogCloseEvent()); Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath())); }); @@ -229,7 +227,7 @@ private void launch0() { @Override public void onStop(boolean success, TaskExecutor executor) { - Platform.runLater(() -> { + runLater(() -> { // Check if the application has stopped // because onStop will be invoked if tasks fail when the executor service shut down. if (!Controllers.isStopped()) { @@ -326,178 +324,181 @@ public void onStop(boolean success, TaskExecutor executor) { executor.start(); } - private static Task checkGameState(Profile profile, VersionSetting setting, Version version) { + private static Task checkGameState(Profile profile, VersionSetting setting, Version version) { LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, profile.getRepository().getGameVersion(version).orElse(null)); GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT)); + Task getJavaTask = Task.supplyAsync(() -> { + try { + return setting.getJava(gameVersion, version); + } catch (InterruptedException e) { + throw new CancellationException(); + } + }); + Task task; if (setting.isNotCheckJVM()) { - return Task.composeAsync(() -> setting.getJavaVersion(gameVersion, version)) - .thenApplyAsync(javaVersion -> Optional.ofNullable(javaVersion).orElseGet(JavaVersion::fromCurrentEnvironment)) - .withStage("launch.state.java"); - } - - return 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()); - - if (setting.isJavaAutoSelected()) { - GameJavaVersion targetJavaVersion = null; - - if (org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) { - JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version); - if (range.getMandatory().contains(VersionNumber.asVersion("21.0.3"))) { - targetJavaVersion = GameJavaVersion.JAVA_21; - } else if (range.getMandatory().contains(VersionNumber.asVersion("17.0.1"))) { - targetJavaVersion = GameJavaVersion.JAVA_17; - } else if (range.getMandatory().contains(VersionNumber.asVersion("16.0.1"))) { - targetJavaVersion = GameJavaVersion.JAVA_16; - } else { - String java8Version; - - if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { - java8Version = "1.8.0_51"; - } else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) { - java8Version = "1.8.0_202"; - } else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) { - java8Version = "1.8.0_74"; - } else { - java8Version = null; - } + task = getJavaTask.thenApplyAsync(java -> Lang.requireNonNullElse(java, JavaRuntime.getDefault())); + } else if (setting.getJavaVersionType() == JavaVersionType.AUTO || setting.getJavaVersionType() == JavaVersionType.VERSION) { + task = getJavaTask.thenComposeAsync(Schedulers.javafx(), java -> { + if (java != null) { + return Task.completed(java); + } - if (java8Version != null && range.getMandatory().contains(VersionNumber.asVersion(java8Version))) - targetJavaVersion = GameJavaVersion.JAVA_8; - else - targetJavaVersion = null; + // Reset invalid java version + CompletableFuture future = new CompletableFuture<>(); + Task result = Task.fromCompletableFuture(future); + Runnable breakAction = () -> future.completeExceptionally(new CancellationException("No accepted java")); + List supportedVersions = GameJavaVersion.getSupportedVersions(SYSTEM_PLATFORM); + + GameJavaVersion targetJavaVersion = null; + if (setting.getJavaVersionType() == JavaVersionType.VERSION) { + try { + int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion()); + GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion); + + if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.getMajorVersion()) { + Controllers.dialog( + i18n("launch.failed.java_version_too_low"), + i18n("message.error"), + MessageType.ERROR, + breakAction + ); + return result; } - } - if (targetJavaVersion == null) { - Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> { - future.completeExceptionally(new CancellationException("No accepted java")); - }); - } else { - downloadJava(gameVersion.toString(), targetJavaVersion, profile) - .thenAcceptAsync(downloadedJavaVersion -> { - future.complete(downloadedJavaVersion); - }) - .exceptionally(throwable -> { - LOG.warning("Failed to download java", throwable); - Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> { - future.completeExceptionally(new CancellationException("No accepted java")); - }); - return null; - }); + targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor); + } catch (NumberFormatException ignored) { } + } else + targetJavaVersion = version.getJavaVersion(); + + if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) { + downloadJava(targetJavaVersion, profile) + .whenCompleteAsync((downloadedJava, exception) -> { + if (exception == null) { + future.complete(downloadedJava); + } else { + LOG.warning("Failed to download java", exception); + Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, + () -> future.complete(JavaRuntime.getDefault()), + breakAction); + } + }, Schedulers.javafx()); } else { - Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction); - - setting.setJava(null); - setting.setDefaultJavaPath(null); - setting.setJavaVersion(JavaVersion.fromCurrentEnvironment()); + Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, + () -> future.complete(JavaRuntime.getDefault()), + breakAction); } - 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); - - JavaVersionConstraint violatedMandatoryConstraint = null; - List violatedSuggestedConstraints = null; - - for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) { - if (constraint.appliesToVersion(gameVersion, version, javaVersion, analyzer)) { - if (!constraint.checkJava(gameVersion, version, javaVersion)) { - if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) { - violatedMandatoryConstraint = constraint; - } else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) { - if (violatedSuggestedConstraints == null) - violatedSuggestedConstraints = new ArrayList<>(1); - violatedSuggestedConstraints.add(constraint); + return result; + }); + } else { + task = getJavaTask.thenComposeAsync(java -> { + Set violatedMandatoryConstraints = EnumSet.noneOf(JavaVersionConstraint.class); + Set violatedSuggestedConstraints = EnumSet.noneOf(JavaVersionConstraint.class); + + if (java != null) { + for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) { + if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) { + if (!constraint.checkJava(gameVersion, version, java)) { + if (constraint.isMandatory()) { + violatedMandatoryConstraints.add(constraint); + } else { + violatedSuggestedConstraints.add(constraint); + } + } } } - } - } - CompletableFuture future = new CompletableFuture<>(); - Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user")); + CompletableFuture future = new CompletableFuture<>(); + Task result = Task.fromCompletableFuture(future); + Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user")); + + if (java == null || !violatedMandatoryConstraints.isEmpty()) { + JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, version); + if (suggestedJava != null) { + FXUtils.runInFX(() -> { + Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> { + setting.setJavaAutoSelected(); + future.complete(suggestedJava); + }, breakAction); + }); + return result; + } else if (java == null) { + FXUtils.runInFX(() -> Controllers.dialog( + i18n("launch.invalid_java"), + i18n("message.error"), + MessageType.ERROR, + breakAction + )); + return result; + } else { + GameJavaVersion gameJavaVersion; + if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON)) + gameJavaVersion = version.getJavaVersion(); + else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA)) + gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion); + else + gameJavaVersion = 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 { - switch (violatedMandatoryConstraint) { - case GAME_JSON: - downloadJava(gameVersion.toString(), version.getJavaVersion(), profile) - .thenAcceptAsync(downloadedJavaVersion -> { - setting.setJavaVersion(downloadedJavaVersion); - future.complete(downloadedJavaVersion); - }, Schedulers.javafx()) - .whenCompleteAsync((result, throwable) -> { - LOG.warning("Failed to download java", throwable); - breakAction.run(); - }, Schedulers.javafx()); - 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(OPENJDK_DOWNLOAD_LINK), null); - breakAction.run(); - return Task.fromCompletableFuture(future); - case VANILLA_JAVA_17: - Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 17), i18n("message.warning"), - () -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null); - breakAction.run(); - return Task.fromCompletableFuture(future); - case VANILLA_JAVA_21: - Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 21), i18n("message.warning"), - () -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null); - breakAction.run(); - return Task.fromCompletableFuture(future); - case VANILLA_JAVA_8: - Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction); - return Task.fromCompletableFuture(future); - case VANILLA_LINUX_JAVA_8: + if (gameJavaVersion != null) { + FXUtils.runInFX(() -> downloadJava(gameJavaVersion, profile).whenCompleteAsync((downloadedJava, throwable) -> { + if (throwable == null) { + setting.setJavaAutoSelected(); + future.complete(downloadedJava); + } else { + LOG.warning("Failed to download java", throwable); + breakAction.run(); + } + }, Schedulers.javafx())); + return result; + } + + if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) { if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) { - Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction); - return Task.fromCompletableFuture(future); + FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction)); + return result; } else { - break; + violatedMandatoryConstraints.remove(JavaVersionConstraint.VANILLA_LINUX_JAVA_8); } - case LAUNCH_WRAPPER: - Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction); - return Task.fromCompletableFuture(future); + } + + if (violatedMandatoryConstraints.contains(JavaVersionConstraint.LAUNCH_WRAPPER)) { + FXUtils.runInFX(() -> Controllers.dialog( + i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), + i18n("message.error"), + MessageType.ERROR, + breakAction + )); + return result; + } + + if (!violatedMandatoryConstraints.isEmpty()) { + FXUtils.runInFX(() -> Controllers.dialog( + i18n("launch.advice.unknown") + "\n" + violatedMandatoryConstraints, + i18n("message.error"), + MessageType.ERROR, + breakAction + )); + return result; + } } } - } - List suggestions = new ArrayList<>(0); + List suggestions = new ArrayList<>(); - if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && javaVersion.getPlatform().getArchitecture() == Architecture.X86) { - suggestions.add(i18n("launch.advice.different_platform")); - } + if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && java.getPlatform().getArchitecture() == Architecture.X86) { + suggestions.add(i18n("launch.advice.different_platform")); + } - // 32-bit JVM cannot make use of too much memory. - if (javaVersion.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) { - // 1.5 * 1024 is an inaccurate number. - // Actual memory limit depends on operating system and memory. - suggestions.add(i18n("launch.advice.too_large_memory_for_32bit")); - } + // 32-bit JVM cannot make use of too much memory. + if (java.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) { + // 1.5 * 1024 is an inaccurate number. + // Actual memory limit depends on operating system and memory. + suggestions.add(i18n("launch.advice.too_large_memory_for_32bit")); + } - if (violatedSuggestedConstraints != null) { for (JavaVersionConstraint violatedSuggestedConstraint : violatedSuggestedConstraints) { switch (violatedSuggestedConstraint) { case MODDED_JAVA_7: @@ -505,7 +506,7 @@ private static Task checkGameState(Profile profile, VersionSetting break; case MODDED_JAVA_8: // Minecraft>=1.7.10+Forge accepts Java 8 - if (javaVersion.getParsedVersion() < 8) + if (java.getParsedVersion() < 8) suggestions.add(i18n("launch.advice.newer_java")); else suggestions.add(i18n("launch.advice.modded_java", 8, gameVersion)); @@ -529,117 +530,91 @@ private static Task checkGameState(Profile profile, VersionSetting break; case VANILLA_X86: if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER - && org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) { + && isCompatibleWithX86Java()) { suggestions.add(i18n("launch.advice.vanilla_x86.translation")); } break; + default: + suggestions.add(violatedSuggestedConstraint.name()); } } - } - // Cannot allocate too much memory exceeding free space. - if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) { - suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY)); - } + // Cannot allocate too much memory exceeding free space. + if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) { + suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY)); + } - VersionNumber forgeVersion = version.getLibraries().stream() - .filter(it -> it.is("net.minecraftforge", "forge")) - .findFirst() - .map(library -> VersionNumber.asVersion(library.getVersion())) - .orElse(null); - - // Forge 2760~2773 will crash game with LiteLoader. - boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("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("1.12.2") == 0) { - suggestions.add(i18n("launch.advice.forge2760_liteloader")); - } + VersionNumber forgeVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE) + .map(VersionNumber::asVersion) + .orElse(null); - // OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions. - boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0); - boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine")); - if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) { - suggestions.add(i18n("launch.advice.forge28_2_2_optifine")); - } + // Forge 2760~2773 will crash game with LiteLoader. + boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("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("1.12.2") == 0) { + suggestions.add(i18n("launch.advice.forge2760_liteloader")); + } - if (suggestions.isEmpty()) { - if (!future.isDone()) { - future.complete(javaVersion); + // OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions. + boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0); + boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine")); + if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) { + suggestions.add(i18n("launch.advice.forge28_2_2_optifine")); } - } else { - String message; - if (suggestions.size() == 1) { - message = i18n("launch.advice", suggestions.get(0)); + + if (suggestions.isEmpty()) { + if (!future.isDone()) { + future.complete(java); + } } else { - message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n"))); + String message; + if (suggestions.size() == 1) { + message = i18n("launch.advice", suggestions.get(0)); + } else { + message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n"))); + } + + FXUtils.runInFX(() -> Controllers.confirm( + message, + i18n("message.warning"), + MessageType.WARNING, + () -> future.complete(java), + breakAction)); } - Controllers.confirm(message, i18n("message.warning"), MessageType.WARNING, () -> future.complete(javaVersion), breakAction); - } + return result; + }); + } - return Task.fromCompletableFuture(future); - }).withStage("launch.state.java"); + return task.withStage("launch.state.java"); } - private static CompletableFuture downloadJava(String gameVersion, GameJavaVersion javaVersion, Profile profile) { - CompletableFuture future = new CompletableFuture<>(); - - JFXHyperlink link = new JFXHyperlink(i18n("download.external_link")); - link.setOnAction(e -> { - if (javaVersion.getMajorVersion() == JavaVersion.JAVA_8) { - FXUtils.openLink(ORACLEJDK_DOWNLOAD_LINK); - } else { - FXUtils.openLink(OPENJDK_DOWNLOAD_LINK); - } - future.completeExceptionally(new CancellationException()); - }); - + private static CompletableFuture downloadJava(GameJavaVersion javaVersion, Profile profile) { + CompletableFuture future = new CompletableFuture<>(); Controllers.dialog(new MessageDialogPane.Builder( - i18n("launch.advice.require_newer_java_version", - gameVersion, - javaVersion.getMajorVersion()), + i18n("launch.advice.require_newer_java_version", javaVersion.getMajorVersion()), i18n("message.warning"), MessageType.QUESTION) - .addAction(link) .yesOrNo(() -> { - downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider()) - .thenAcceptAsync(future::complete) - .exceptionally(throwable -> { - Throwable resolvedException = resolveException(throwable); - LOG.warning("Failed to download java", throwable); - if (!(resolvedException instanceof CancellationException)) { - Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed")); + DownloadProvider downloadProvider = profile.getDependency().getDownloadProvider(); + Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + future.complete(result); + } else { + Throwable resolvedException = resolveException(exception); + LOG.warning("Failed to download java", exception); + if (!(resolvedException instanceof CancellationException)) { + Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed")); + } + future.completeExceptionally(new CancellationException()); } - future.completeExceptionally(new CancellationException()); - return null; - }); + }), i18n("download.java"), new TaskCancellationAction(() -> future.completeExceptionally(new CancellationException()))); }, () -> future.completeExceptionally(new CancellationException())).build()); return future; } - /** - * Directly start java downloading. - * - * @param javaVersion target Java version - * @param downloadProvider download provider - * @return JavaVersion, null if we failed to download java, failed if an error occurred when downloading. - */ - private static CompletableFuture downloadJavaImpl(GameJavaVersion javaVersion, DownloadProvider downloadProvider) { - CompletableFuture future = new CompletableFuture<>(); - - Controllers.taskDialog(JavaRepository.downloadJava(javaVersion, downloadProvider) - .whenComplete(Schedulers.javafx(), (downloadedJava, exception) -> { - if (exception != null) { - future.completeExceptionally(exception); - } else { - future.complete(downloadedJava); - } - }), i18n("download.java"), TaskCancellationAction.NORMAL); - - return future; - } - private static Task logIn(Account account) { return Task.composeAsync(() -> { try { @@ -680,7 +655,7 @@ private static Task logIn(Account account) { private void checkExit() { switch (launcherVisibility) { case HIDE_AND_REOPEN: - Platform.runLater(() -> { + runLater(() -> { Optional.ofNullable(Controllers.getStage()) .ifPresent(Stage::show); }); @@ -691,9 +666,9 @@ private void checkExit() { case CLOSE: throw new Error("Never get to here"); case HIDE: - Platform.runLater(() -> { + runLater(() -> { // Shut down the platform when user closed log window. - Platform.setImplicitExit(true); + setImplicitExit(true); // If we use Launcher.stop(), log window will be halt immediately. Launcher.stopWithoutPlatform(); }); @@ -746,7 +721,7 @@ public void setProcess(ManagedProcess process) { if (showLogs) { CountDownLatch logWindowLatch = new CountDownLatch(1); - Platform.runLater(() -> { + runLater(() -> { logWindow = new LogWindow(process, logs); logWindow.show(); logWindowLatch.countDown(); @@ -760,9 +735,9 @@ public void setProcess(ManagedProcess process) { private void submitLogs() { if (currentLogs.size() == 1) { Log log = currentLogs.get(0); - Platform.runLater(() -> logWindow.logLine(log)); + runLater(() -> logWindow.logLine(log)); } else { - Platform.runLater(() -> { + runLater(() -> { logWindow.logLines(currentLogs); semaphore.release(); }); @@ -803,7 +778,7 @@ public void run() { private void finishLaunch() { switch (launcherVisibility) { case HIDE_AND_REOPEN: - Platform.runLater(() -> { + runLater(() -> { // If application was stopped and execution services did not finish termination, // these codes will be executed. if (Controllers.getStage() != null) { @@ -816,11 +791,11 @@ private void finishLaunch() { // Never come to here. break; case KEEP: - Platform.runLater(launchingLatch::countDown); + runLater(launchingLatch::countDown); break; case HIDE: launchingLatch.countDown(); - Platform.runLater(() -> { + runLater(() -> { // If application was stopped and execution services did not finish termination, // these codes will be executed. if (Controllers.getStage() != null) { @@ -898,7 +873,7 @@ public void onExit(int exitCode, ExitType exitType) { if (exitType != ExitType.NORMAL) { repository.markVersionLaunchedAbnormally(version.getId()); - Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show()); + runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show()); } checkExit(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/java/HMCLJavaRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/java/HMCLJavaRepository.java new file mode 100644 index 0000000000..75d8d3f890 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/java/HMCLJavaRepository.java @@ -0,0 +1,221 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.java; + +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask; +import org.jackhuang.hmcl.download.java.mojang.MojangJavaRemoteFiles; +import org.jackhuang.hmcl.game.DownloadInfo; +import org.jackhuang.hmcl.game.GameJavaVersion; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.platform.Platform; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.util.*; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/** + * @author Glavo + */ +public final class HMCLJavaRepository implements JavaRepository { + public static final String MOJANG_JAVA_PREFIX = "mojang-"; + + private final Path root; + + public HMCLJavaRepository(Path root) { + this.root = root; + } + + public Path getPlatformRoot(Platform platform) { + return root.resolve(platform.toString()); + } + + @Override + public Path getJavaDir(Platform platform, String name) { + return getPlatformRoot(platform).resolve(name); + } + + public Path getJavaDir(Platform platform, GameJavaVersion gameJavaVersion) { + return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent()); + } + + @Override + public Path getManifestFile(Platform platform, String name) { + return getPlatformRoot(platform).resolve(name + ".json"); + } + + public Path getManifestFile(Platform platform, GameJavaVersion gameJavaVersion) { + return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent()); + } + + public boolean isInstalled(Platform platform, String name) { + return Files.exists(getManifestFile(platform, name)); + } + + public boolean isInstalled(Platform platform, GameJavaVersion gameJavaVersion) { + return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent()); + } + + public @Nullable Path getJavaExecutable(Platform platform, String name) { + Path javaDir = getJavaDir(platform, name); + try { + return JavaManager.getExecutable(javaDir).toRealPath(); + } catch (IOException ignored) { + if (platform.getOperatingSystem() == OperatingSystem.OSX) { + try { + return JavaManager.getMacExecutable(javaDir).toRealPath(); + } catch (IOException ignored1) { + } + } + } + + return null; + } + + public @Nullable Path getJavaExecutable(Platform platform, GameJavaVersion gameJavaVersion) { + return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent()); + } + + @Override + public Collection getAllJava(Platform platform) { + Path root = getPlatformRoot(platform); + if (!Files.isDirectory(root)) + return Collections.emptyList(); + + ArrayList list = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(root)) { + for (Path file : stream) { + try { + String name = file.getFileName().toString(); + if (name.endsWith(".json") && Files.isRegularFile(file)) { + Path javaDir = file.resolveSibling(name.substring(0, name.length() - ".json".length())); + Path executable; + try { + executable = JavaManager.getExecutable(javaDir).toRealPath(); + } catch (IOException e) { + if (platform.getOperatingSystem() == OperatingSystem.OSX) + executable = JavaManager.getMacExecutable(javaDir).toRealPath(); + else + throw e; + } + + if (Files.isDirectory(javaDir)) { + JavaManifest manifest; + try (InputStream input = Files.newInputStream(file)) { + manifest = JsonUtils.fromJsonFully(input, JavaManifest.class); + } + + list.add(JavaRuntime.of(executable, manifest.getInfo(), true)); + } + } + } catch (Throwable e) { + LOG.warning("Failed to parse " + file, e); + } + } + + } catch (IOException ignored) { + } + return list; + } + + @Override + public Task getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) { + Path javaDir = getJavaDir(platform, gameJavaVersion); + + return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> { + Path executable; + try { + executable = JavaManager.getExecutable(javaDir).toRealPath(); + } catch (IOException e) { + if (platform.getOperatingSystem() == OperatingSystem.OSX) + executable = JavaManager.getMacExecutable(javaDir).toRealPath(); + else + throw e; + } + + JavaInfo info; + if (JavaManager.isCompatible(platform)) + info = JavaInfo.fromExecutable(executable, false); + else + info = new JavaInfo(platform, result.download.getVersion().getName(), null); + + Map update = new LinkedHashMap<>(); + update.put("provider", "mojang"); + update.put("component", gameJavaVersion.getComponent()); + + Map files = new LinkedHashMap<>(); + result.remoteFiles.getFiles().forEach((path, file) -> { + if (file instanceof MojangJavaRemoteFiles.RemoteFile) { + DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw"); + if (downloadInfo != null) { + files.put(path, new JavaLocalFiles.LocalFile(downloadInfo.getSha1(), downloadInfo.getSize())); + } + } else if (file instanceof MojangJavaRemoteFiles.RemoteDirectory) { + files.put(path, new JavaLocalFiles.LocalDirectory()); + } else if (file instanceof MojangJavaRemoteFiles.RemoteLink) { + files.put(path, new JavaLocalFiles.LocalLink(((MojangJavaRemoteFiles.RemoteLink) file).getTarget())); + } + }); + + JavaManifest manifest = new JavaManifest(info, update, files); + FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest)); + return JavaRuntime.of(executable, info, true); + }); + } + + public Task getInstallJavaTask(Platform platform, String name, Map update, Path archiveFile) { + Path javaDir = getJavaDir(platform, name); + return new JavaInstallTask(javaDir, update, archiveFile).thenApplyAsync(result -> { + if (!result.getInfo().getPlatform().equals(platform)) + throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform()); + + Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath(); + FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result)); + return JavaRuntime.of(executable, result.getInfo(), true); + }); + } + + @Override + public Task getUninstallJavaTask(Platform platform, String name) { + return Task.runAsync(() -> { + Files.deleteIfExists(getManifestFile(platform, name)); + FileUtils.deleteDirectory(getJavaDir(platform, name).toFile()); + }); + } + + @Override + public Task getUninstallJavaTask(JavaRuntime java) { + return Task.runAsync(() -> { + Path root = getPlatformRoot(java.getPlatform()); + Path relativized = root.relativize(java.getBinary()); + + if (relativized.getNameCount() > 1) { + String name = relativized.getName(0).toString(); + Files.deleteIfExists(getManifestFile(java.getPlatform(), name)); + FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile()); + } + }); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaInstallTask.java new file mode 100644 index 0000000000..b26c674d5c --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaInstallTask.java @@ -0,0 +1,116 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.java; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.DigestUtils; +import org.jackhuang.hmcl.util.Hex; +import org.jackhuang.hmcl.util.io.IOUtils; +import org.jackhuang.hmcl.util.tree.ArchiveFileTree; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * @author Glavo + */ +public final class JavaInstallTask extends Task { + + private final Path targetDir; + private final Map update; + private final Path archiveFile; + + private final Map files = new LinkedHashMap<>(); + private final ArrayList nameStack = new ArrayList<>(); + private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE]; + private final MessageDigest messageDigest = DigestUtils.getDigest("SHA-1"); + + public JavaInstallTask(Path targetDir, Map update, Path archiveFile) { + this.targetDir = targetDir; + this.update = update; + this.archiveFile = archiveFile; + } + + @Override + public void execute() throws Exception { + JavaInfo info; + + try (ArchiveFileTree tree = ArchiveFileTree.open(archiveFile)) { + info = JavaInfo.fromArchive(tree); + copyDirContent(tree, targetDir); + } + + setResult(new JavaManifest(info, update, files)); + } + + private void copyDirContent(ArchiveFileTree tree, Path targetDir) throws IOException { + copyDirContent(tree, tree.getRoot().getSubDirs().values().iterator().next(), targetDir); + } + + private void copyDirContent(ArchiveFileTree tree, ArchiveFileTree.Dir dir, Path targetDir) throws IOException { + Files.createDirectories(targetDir); + + for (Map.Entry pair : dir.getFiles().entrySet()) { + Path path = targetDir.resolve(pair.getKey()); + E entry = pair.getValue(); + + nameStack.add(pair.getKey()); + if (tree.isLink(entry)) { + String linkTarget = tree.getLink(entry); + files.put(String.join("/", nameStack), new JavaLocalFiles.LocalLink(linkTarget)); + Files.createSymbolicLink(path, Paths.get(linkTarget)); + } else { + long size = 0L; + + try (InputStream input = tree.getInputStream(entry); + OutputStream output = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + messageDigest.reset(); + + int c; + while ((c = input.read(buffer)) > 0) { + size += c; + output.write(buffer, 0, c); + messageDigest.update(buffer, 0, c); + } + } + + if (tree.isExecutable(entry)) + //noinspection ResultOfMethodCallIgnored + path.toFile().setExecutable(true); + + files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size)); + } + nameStack.remove(nameStack.size() - 1); + } + + for (Map.Entry> pair : dir.getSubDirs().entrySet()) { + nameStack.add(pair.getKey()); + files.put(String.join("/", nameStack), new JavaLocalFiles.LocalDirectory()); + copyDirContent(tree, pair.getValue(), targetDir.resolve(pair.getKey())); + nameStack.remove(nameStack.size() - 1); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaLocalFiles.java b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaLocalFiles.java new file mode 100644 index 0000000000..592acec56e --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaLocalFiles.java @@ -0,0 +1,128 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 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.java; + +import com.google.gson.*; +import com.google.gson.annotations.JsonAdapter; + +import java.lang.reflect.Type; + +/** + * @author Glavo + */ +public final class JavaLocalFiles { + @JsonAdapter(Serializer.class) + public abstract static class Local { + private final String type; + + Local(String type) { + this.type = type; + } + + public String getType() { + return type; + } + } + + public static final class LocalFile extends Local { + private final String sha1; + private final long size; + + public LocalFile(String sha1, long size) { + super("file"); + this.sha1 = sha1; + this.size = size; + } + + public String getSha1() { + return sha1; + } + + public long getSize() { + return size; + } + } + + public static final class LocalDirectory extends Local { + public LocalDirectory() { + super("directory"); + } + } + + public static final class LocalLink extends Local { + private final String target; + + public LocalLink(String target) { + super("link"); + this.target = target; + } + + public String getTarget() { + return target; + } + } + + public static class Serializer implements JsonSerializer, JsonDeserializer { + + @Override + public JsonElement serialize(Local src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject obj = new JsonObject(); + obj.addProperty("type", src.getType()); + if (src instanceof LocalFile) { + obj.addProperty("sha1", ((LocalFile) src).getSha1()); + obj.addProperty("size", ((LocalFile) src).getSize()); + } else if (src instanceof LocalLink) { + obj.addProperty("target", ((LocalLink) src).getTarget()); + } + return obj; + } + + @Override + public Local deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (!json.isJsonObject()) + throw new JsonParseException(json.toString()); + + JsonObject obj = json.getAsJsonObject(); + if (!obj.has("type")) + throw new JsonParseException(json.toString()); + + String type = obj.getAsJsonPrimitive("type").getAsString(); + + try { + switch (type) { + case "file": { + String sha1 = obj.getAsJsonPrimitive("sha1").getAsString(); + long size = obj.getAsJsonPrimitive("size").getAsLong(); + return new LocalFile(sha1, size); + } + case "directory": { + return new LocalDirectory(); + } + case "link": { + String target = obj.getAsJsonPrimitive("target").getAsString(); + return new LocalLink(target); + } + default: + throw new AssertionError("unknown type: " + type); + } + } catch (Throwable e) { + throw new JsonParseException(json.toString()); + } + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java new file mode 100644 index 0000000000..3b9e86e815 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java @@ -0,0 +1,697 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.java; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.game.GameJavaVersion; +import org.jackhuang.hmcl.game.JavaVersionConstraint; +import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.setting.ConfigHolder; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.util.CacheRepository; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.platform.Architecture; +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.platform.Platform; +import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/** + * @author Glavo + */ +public final class JavaManager { + + private JavaManager() { + } + + public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_DIRECTORY.resolve("java")); + + public static String getMojangJavaPlatform(Platform platform) { + if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) { + if (Architecture.SYSTEM_ARCH == Architecture.X86) { + return "windows-x86"; + } else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) { + return "windows-x64"; + } else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) { + return "windows-arm64"; + } + } else if (platform.getOperatingSystem() == OperatingSystem.LINUX) { + if (Architecture.SYSTEM_ARCH == Architecture.X86) { + return "linux-i386"; + } else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) { + return "linux"; + } + } else if (platform.getOperatingSystem() == OperatingSystem.OSX) { + if (Architecture.SYSTEM_ARCH == Architecture.X86_64) { + return "mac-os"; + } else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) { + return "mac-os-arm64"; + } + } + + return null; + } + + public static Path getExecutable(Path javaHome) { + return javaHome.resolve("bin").resolve(OperatingSystem.CURRENT_OS.getJavaExecutable()); + } + + public static Path getMacExecutable(Path javaHome) { + return javaHome.resolve("jre.bundle/Contents/Home/bin/java"); + } + + public static boolean isCompatible(Platform platform) { + if (platform.getOperatingSystem() != OperatingSystem.CURRENT_OS) + return false; + + Architecture architecture = platform.getArchitecture(); + if (architecture == Architecture.SYSTEM_ARCH || architecture == Architecture.CURRENT_ARCH) + return true; + + switch (OperatingSystem.CURRENT_OS) { + case WINDOWS: + if (Architecture.SYSTEM_ARCH == Architecture.X86_64) + return architecture == Architecture.X86; + if (Architecture.SYSTEM_ARCH == Architecture.ARM64) + return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86; + break; + case LINUX: + if (Architecture.SYSTEM_ARCH == Architecture.X86_64) + return architecture == Architecture.X86; + break; + case OSX: + if (Architecture.SYSTEM_ARCH == Architecture.ARM64) + return architecture == Architecture.X86_64; + break; + } + + return false; + } + + private static volatile Map allJava; + private static final CountDownLatch LATCH = new CountDownLatch(1); + + private static final ObjectProperty> allJavaProperty = new SimpleObjectProperty<>(); + + private static Map getAllJavaMap() throws InterruptedException { + Map map = allJava; + if (map == null) { + LATCH.await(); + map = allJava; + } + return map; + } + + private static void updateAllJavaProperty(Map javaRuntimes) { + JavaRuntime[] array = javaRuntimes.values().toArray(new JavaRuntime[0]); + Arrays.sort(array); + allJavaProperty.set(Arrays.asList(array)); + } + + public static boolean isInitialized() { + return allJava != null; + } + + public static Collection getAllJava() throws InterruptedException { + return getAllJavaMap().values(); + } + + public static ObjectProperty> getAllJavaProperty() { + return allJavaProperty; + } + + public static JavaRuntime getJava(Path executable) throws IOException, InterruptedException { + executable = executable.toRealPath(); + + JavaRuntime javaRuntime = getAllJavaMap().get(executable); + if (javaRuntime != null) { + return javaRuntime; + } + + JavaInfo info = JavaInfo.fromExecutable(executable); + return JavaRuntime.of(executable, info, false); + } + + public static void refresh() { + Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (result != null) { + LATCH.await(); + allJava = result; + updateAllJavaProperty(result); + } + }).start(); + } + + public static Task getAddJavaTask(Path binary) { + return Task.supplyAsync("Get Java", () -> JavaManager.getJava(binary)) + .thenApplyAsync(Schedulers.javafx(), javaRuntime -> { + if (!JavaManager.isCompatible(javaRuntime.getPlatform())) { + throw new UnsupportedPlatformException("Incompatible platform: " + javaRuntime.getPlatform()); + } + + String pathString = javaRuntime.getBinary().toString(); + + ConfigHolder.globalConfig().getDisabledJava().remove(pathString); + if (ConfigHolder.globalConfig().getUserJava().add(pathString)) { + addJava(javaRuntime); + } + return javaRuntime; + }); + } + + public static Task getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) { + return REPOSITORY.getDownloadJavaTask(downloadProvider, platform, gameJavaVersion) + .thenApplyAsync(Schedulers.javafx(), java -> { + addJava(java); + return java; + }); + } + + public static Task getInstallJavaTask(Platform platform, String name, Map update, Path archiveFile) { + return REPOSITORY.getInstallJavaTask(platform, name, update, archiveFile) + .thenApplyAsync(Schedulers.javafx(), java -> { + addJava(java); + return java; + }); + } + + public static Task getUninstallJavaTask(JavaRuntime java) { + assert java.isManaged(); + Path root = REPOSITORY.getPlatformRoot(java.getPlatform()); + Path relativized = root.relativize(java.getBinary()); + + if (relativized.getNameCount() > 1) { + FXUtils.runInFX(() -> { + try { + removeJava(java); + } catch (InterruptedException e) { + throw new AssertionError("Unreachable code", e); + } + }); + + String name = relativized.getName(0).toString(); + return REPOSITORY.getUninstallJavaTask(java.getPlatform(), name); + } else { + return Task.completed(null); + } + } + + // FXThread + public static void addJava(JavaRuntime java) throws InterruptedException { + Map oldMap = getAllJavaMap(); + if (!oldMap.containsKey(java.getBinary())) { + HashMap newMap = new HashMap<>(oldMap); + newMap.put(java.getBinary(), java); + allJava = newMap; + updateAllJavaProperty(newMap); + } + } + + // FXThread + public static void removeJava(JavaRuntime java) throws InterruptedException { + removeJava(java.getBinary()); + } + + // FXThread + public static void removeJava(Path realPath) throws InterruptedException { + Map oldMap = getAllJavaMap(); + if (oldMap.containsKey(realPath)) { + HashMap newMap = new HashMap<>(oldMap); + newMap.remove(realPath); + allJava = newMap; + updateAllJavaProperty(newMap); + } + } + + private static int compareJavaVersion(JavaRuntime java1, JavaRuntime java2, GameJavaVersion suggestedJavaVersion) { + if (suggestedJavaVersion != null) { + boolean b1 = java1.getParsedVersion() == suggestedJavaVersion.getMajorVersion(); + boolean b2 = java2.getParsedVersion() == suggestedJavaVersion.getMajorVersion(); + + if (b1 != b2) + return b1 ? 1 : -1; + } + + return java1.getVersionNumber().compareTo(java2.getVersionNumber()); + } + + @Nullable + public static JavaRuntime findSuitableJava(GameVersionNumber gameVersion, Version version) throws InterruptedException { + return findSuitableJava(getAllJava(), gameVersion, version); + } + + @Nullable + public static JavaRuntime findSuitableJava(Collection javaRuntimes, GameVersionNumber gameVersion, Version version) { + LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null; + + boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64 + && (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) + && (gameVersion == null || gameVersion.compareTo("1.6") < 0); + + GameJavaVersion suggestedJavaVersion = + (version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null; + + JavaRuntime mandatory = null; + JavaRuntime suggested = null; + for (JavaRuntime java : javaRuntimes) { + if (forceX86) { + if (!java.getArchitecture().isX86()) + continue; + } else { + if (java.getArchitecture() != Architecture.SYSTEM_ARCH) + continue; + } + + boolean violationMandatory = false; + boolean violationSuggested = false; + + for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) { + if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) { + if (!constraint.checkJava(gameVersion, version, java)) { + if (constraint.isMandatory()) { + violationMandatory = true; + } else { + violationSuggested = true; + } + } + } + } + + if (!violationMandatory) { + if (mandatory == null) mandatory = java; + else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0) + mandatory = java; + + if (!violationSuggested) { + if (suggested == null) suggested = java; + else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0) + suggested = java; + } + } + } + + return suggested != null ? suggested : mandatory; + } + + public static void initialize() { + Map allJava = searchPotentialJavaExecutables(); + JavaManager.allJava = allJava; + LATCH.countDown(); + FXUtils.runInFX(() -> updateAllJavaProperty(allJava)); + } + + // search java + + private static Map searchPotentialJavaExecutables() { + Map javaRuntimes = new HashMap<>(); + searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM); + switch (OperatingSystem.CURRENT_OS) { + case WINDOWS: + if (Architecture.SYSTEM_ARCH == Architecture.X86_64) + searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86); + if (Architecture.SYSTEM_ARCH == Architecture.ARM64) { + if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) + searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86_64); + searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86); + } + break; + case OSX: + if (Architecture.SYSTEM_ARCH == Architecture.ARM64) + searchAllJavaInRepository(javaRuntimes, Platform.OSX_X86_64); + break; + } + + switch (OperatingSystem.CURRENT_OS) { + case WINDOWS: + queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\"); + queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\"); + queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\"); + queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\"); + + searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files"); + searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)"); + if (Architecture.SYSTEM_ARCH == Architecture.ARM64) { + searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(ARM)", "C:\\Program Files (ARM)"); + } + break; + case LINUX: + searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/java")); // Oracle RPMs + searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib/jvm")); // General locations + searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib32/jvm")); // General locations + searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib64/jvm")); // General locations + searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN! + break; + case OSX: + tryAddJavaHome(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines/Contents/Home")); + tryAddJavaHome(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines/Contents/Home")); + tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java")); + tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java")); + // Homebrew + tryAddJavaExecutable(javaRuntimes, Paths.get("/opt/homebrew/opt/java/bin/java")); + searchAllJavaInDirectory(javaRuntimes, Paths.get("/opt/homebrew/Cellar/openjdk")); + try (DirectoryStream dirs = Files.newDirectoryStream(Paths.get("/opt/homebrew/Cellar"), "openjdk@*")) { + for (Path dir : dirs) { + searchAllJavaInDirectory(javaRuntimes, dir); + } + } catch (IOException e) { + LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar"); + } + break; + + default: + break; + } + + // Search Minecraft bundled runtimes + if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH.isX86()) { + FileUtils.tryGetPath(System.getenv("localappdata"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime") + .ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false)); + + FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime") + .ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false)); + } else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) { + searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home")).resolve(".minecraft/runtime"), false); + } else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) { + searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home")).resolve("Library/Application Support/minecraft/runtime"), false); + } + searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true); + + // Search in PATH. + if (System.getenv("PATH") != null) { + String[] paths = System.getenv("PATH").split(OperatingSystem.PATH_SEPARATOR); + for (String path : paths) { + try { + tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable())); + } catch (InvalidPathException ignored) { + } + } + } + + if (System.getenv("HMCL_JRES") != null) { + String[] paths = System.getenv("HMCL_JRES").split(OperatingSystem.PATH_SEPARATOR); + for (String path : paths) { + try { + tryAddJavaHome(javaRuntimes, Paths.get(path)); + } catch (InvalidPathException ignored) { + } + } + } + + for (String javaPath : ConfigHolder.globalConfig().getUserJava()) { + try { + tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath)); + } catch (InvalidPathException e) { + LOG.warning("Invalid Java path: " + javaPath); + } + } + + JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA; + if (currentJava != null + && !javaRuntimes.containsKey(currentJava.getBinary()) + && !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) { + javaRuntimes.put(currentJava.getBinary(), currentJava); + } + + LOG.trace(javaRuntimes.values().stream().sorted() + .map(it -> String.format(" - %s %s (%s, %s): %s", + it.isJDK() ? "JDK" : "JRE", + it.getVersion(), + it.getPlatform().getArchitecture().getDisplayName(), + Lang.requireNonNullElse(it.getVendor(), "Unknown"), + it.getBinary())) + .collect(Collectors.joining("\n", "Finished Java lookup, found " + javaRuntimes.size() + "\n", ""))); + + return javaRuntimes; + } + + private static void tryAddJavaHome(Map javaRuntimes, Path javaHome) { + Path executable = getExecutable(javaHome); + if (!Files.isRegularFile(executable)) { + return; + } + + try { + executable = executable.toRealPath(); + } catch (IOException e) { + LOG.warning("Failed to resolve path " + executable, e); + return; + } + + if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) { + return; + } + + JavaInfo info = null; + + Path releaseFile = javaHome.resolve("release"); + if (Files.exists(releaseFile)) { + try { + info = JavaInfo.fromReleaseFile(releaseFile); + } catch (IOException e) { + try { + info = JavaInfo.fromExecutable(executable, false); + } catch (IOException e2) { + e2.addSuppressed(e); + LOG.warning("Failed to lookup Java executable at " + executable, e2); + } + } + } + + if (info != null && isCompatible(info.getPlatform())) + javaRuntimes.put(executable, JavaRuntime.of(executable, info, false)); + } + + private static void tryAddJavaExecutable(Map javaRuntimes, Path executable) { + try { + executable = executable.toRealPath(); + } catch (IOException e) { + return; + } + + if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) { + return; + } + + JavaInfo info = null; + try { + info = JavaInfo.fromExecutable(executable); + } catch (IOException e) { + LOG.warning("Failed to lookup Java executable at " + executable, e); + } + + if (info != null && isCompatible(info.getPlatform())) { + javaRuntimes.put(executable, JavaRuntime.of(executable, info, false)); + } + } + + private static void tryAddJavaInComponentDir(Map javaRuntimes, String platform, Path component, boolean verify) { + Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1"); + if (!Files.isRegularFile(sha1File)) + return; + + Path dir = component.resolve(platform).resolve(component.getFileName()); + + if (verify) { + try (BufferedReader reader = Files.newBufferedReader(sha1File)) { + String line; + while ((line = reader.readLine()) != null) { + if (line.isEmpty()) continue; + + int idx = line.indexOf(" /#//"); + if (idx <= 0) + throw new IOException("Illegal line: " + line); + + Path file = dir.resolve(line.substring(0, idx)); + + // Should we check the sha1 of files? This will take a lot of time. + if (Files.notExists(file)) + throw new NoSuchFileException(file.toAbsolutePath().toString()); + } + } catch (IOException e) { + LOG.warning("Failed to verify Java in " + component, e); + return; + } + } + + if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) { + Path macPath = dir.resolve("jre.bundle/Contents/Home"); + if (Files.exists(macPath)) { + tryAddJavaHome(javaRuntimes, macPath); + return; + } else + LOG.warning("The Java is not in 'jre.bundle/Contents/Home'"); + } + + tryAddJavaHome(javaRuntimes, dir); + } + + private static void searchAllJavaInRepository(Map javaRuntimes, Platform platform) { + for (JavaRuntime java : REPOSITORY.getAllJava(platform)) { + javaRuntimes.put(java.getBinary(), java); + } + } + + private static void searchAllOfficialJava(Map javaRuntimes, Path directory, boolean verify) { + if (!Files.isDirectory(directory)) + return; + // Examples: + // $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home + // $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta + + String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM); + if (javaPlatform != null) { + searchAllOfficialJava(javaRuntimes, directory, javaPlatform, verify); + } + + if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { + if (Architecture.SYSTEM_ARCH == Architecture.X86_64) { + searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify); + } else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) { + if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) { + searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify); + } + searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify); + } + } else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.CURRENT_ARCH == Architecture.ARM64) { + searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.OSX_X86_64), verify); + } + } + + private static void searchAllOfficialJava(Map javaRuntimes, Path directory, String platform, boolean verify) { + try (DirectoryStream dir = Files.newDirectoryStream(directory)) { + // component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future. + for (Path component : dir) { + tryAddJavaInComponentDir(javaRuntimes, platform, component, verify); + } + } catch (IOException e) { + LOG.warning("Failed to list java-runtime directory " + directory, e); + } + } + + private static void searchAllJavaInDirectory(Map javaRuntimes, Path directory) { + if (!Files.isDirectory(directory)) { + return; + } + + try (DirectoryStream stream = Files.newDirectoryStream(directory)) { + for (Path subDir : stream) { + tryAddJavaHome(javaRuntimes, subDir); + } + } catch (IOException e) { + LOG.warning("Failed to find Java in " + directory, e); + } + } + + private static void searchJavaInProgramFiles(Map javaRuntimes, String env, String defaultValue) { + String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue); + Path path; + try { + path = Paths.get(programFiles); + } catch (InvalidPathException ignored) { + return; + } + + for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) { + searchAllJavaInDirectory(javaRuntimes, path.resolve(vendor)); + } + } + + // ==== Windows Registry Support ==== + private static void queryJavaInRegistryKey(Map javaRuntimes, String location) { + for (String java : querySubFolders(location)) { + if (!querySubFolders(java).contains(java + "\\MSI")) + continue; + String home = queryRegisterValue(java, "JavaHome"); + if (home != null) { + try { + tryAddJavaHome(javaRuntimes, Paths.get(home)); + } catch (InvalidPathException e) { + LOG.warning("Invalid Java path in system registry: " + home); + } + } + } + } + + private static List querySubFolders(String location) { + List res = new ArrayList<>(); + + try { + Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) { + for (String line; (line = reader.readLine()) != null; ) { + if (line.startsWith(location) && !line.equals(location)) { + res.add(line); + } + } + } + } catch (IOException e) { + LOG.warning("Failed to query sub folders of " + location, e); + } + return res; + } + + private static String queryRegisterValue(String location, String name) { + boolean last = false; + + try { + Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location, "/v", name}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) { + for (String line; (line = reader.readLine()) != null; ) { + if (StringUtils.isNotBlank(line)) { + if (last && line.trim().startsWith(name)) { + int begins = line.indexOf(name); + if (begins > 0) { + String s2 = line.substring(begins + name.length()); + begins = s2.indexOf("REG_SZ"); + if (begins > 0) { + return s2.substring(begins + "REG_SZ".length()).trim(); + } + } + } + if (location.equals(line.trim())) { + last = true; + } + } + } + } + } catch (IOException e) { + LOG.warning("Failed to query register value of " + location, e); + } + + return null; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManifest.java b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManifest.java new file mode 100644 index 0000000000..974ed92cd1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManifest.java @@ -0,0 +1,112 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.java; + +import com.google.gson.*; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.reflect.TypeToken; +import org.jackhuang.hmcl.util.platform.Architecture; +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.platform.Platform; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.Optional; + +/** + * @author Glavo + */ +@JsonAdapter(JavaManifest.Serializer.class) +public final class JavaManifest { + + private final JavaInfo info; + + @Nullable + private final Map update; + + @Nullable + private final Map files; + + public JavaManifest(JavaInfo info, @Nullable Map update, @Nullable Map files) { + this.info = info; + this.update = update; + this.files = files; + } + + public JavaInfo getInfo() { + return info; + } + + public Map getUpdate() { + return update; + } + + public Map getFiles() { + return files; + } + + public static final class Serializer implements JsonSerializer, JsonDeserializer { + + private static final Type LOCAL_FILES_TYPE = new TypeToken>() { + }.getType(); + + @Override + public JsonElement serialize(JavaManifest src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject res = new JsonObject(); + res.addProperty("os.name", src.getInfo().getPlatform().getOperatingSystem().getCheckedName()); + res.addProperty("os.arch", src.getInfo().getPlatform().getArchitecture().getCheckedName()); + res.addProperty("java.version", src.getInfo().getVersion()); + res.addProperty("java.vendor", src.getInfo().getVendor()); + + if (src.getUpdate() != null) + res.add("update", context.serialize(src.getUpdate())); + + if (src.getFiles() != null) + res.add("files", context.serialize(src.getFiles(), LOCAL_FILES_TYPE)); + + return res; + } + + @Override + public JavaManifest deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (!json.isJsonObject()) + throw new JsonParseException(json.toString()); + + try { + JsonObject jsonObject = json.getAsJsonObject(); + OperatingSystem osName = OperatingSystem.parseOSName(jsonObject.getAsJsonPrimitive("os.name").getAsString()); + Architecture osArch = Architecture.parseArchName(jsonObject.getAsJsonPrimitive("os.arch").getAsString()); + String javaVersion = jsonObject.getAsJsonPrimitive("java.version").getAsString(); + String javaVendor = Optional.ofNullable(jsonObject.get("java.vendor")).map(JsonElement::getAsString).orElse(null); + + Map update = jsonObject.has("update") ? context.deserialize(jsonObject.get("update"), Map.class) : null; + Map files = jsonObject.has("files") ? context.deserialize(jsonObject.get("files"), LOCAL_FILES_TYPE) : null; + + if (osName == null || osArch == null || javaVersion == null) + throw new JsonParseException(json.toString()); + + return new JavaManifest(new JavaInfo(Platform.getPlatform(osName, osArch), javaVersion, javaVendor), update, files); + } catch (JsonParseException e) { + throw e; + } catch (Throwable e) { + throw new JsonParseException(e); + } + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java index 07153f3996..7979f17fde 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java @@ -23,6 +23,8 @@ import javafx.beans.Observable; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleIntegerProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableSet; import org.jackhuang.hmcl.util.javafx.ObservableHelper; import org.jackhuang.hmcl.util.javafx.PropertyUtils; import org.jetbrains.annotations.Nullable; @@ -51,6 +53,10 @@ public static GlobalConfig fromJson(String json) throws JsonParseException { private final IntegerProperty logRetention = new SimpleIntegerProperty(); + private final ObservableSet userJava = FXCollections.observableSet(new LinkedHashSet<>()); + + private final ObservableSet disabledJava = FXCollections.observableSet(new LinkedHashSet<>()); + private final Map unknownFields = new HashMap<>(); private final transient ObservableHelper helper = new ObservableHelper(this); @@ -114,11 +120,21 @@ public void setLogRetention(int logRetention) { this.logRetention.set(logRetention); } + public ObservableSet getUserJava() { + return userJava; + } + + public ObservableSet getDisabledJava() { + return disabledJava; + } + public static final class Serializer implements JsonSerializer, JsonDeserializer { private static final Set knownFields = new HashSet<>(Arrays.asList( "agreementVersion", "platformPromptVersion", - "logRetention" + "logRetention", + "userJava", + "disabledJava" )); @Override @@ -131,6 +147,12 @@ public JsonElement serialize(GlobalConfig src, Type typeOfSrc, JsonSerialization jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion())); jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion())); jsonObject.add("logRetention", context.serialize(src.getLogRetention())); + if (!src.getUserJava().isEmpty()) + jsonObject.add("userJava", context.serialize(src.getUserJava())); + + if (!src.getDisabledJava().isEmpty()) + jsonObject.add("disabledJava", context.serialize(src.getDisabledJava())); + for (Map.Entry entry : src.unknownFields.entrySet()) { jsonObject.add(entry.getKey(), context.serialize(entry.getValue())); } @@ -149,6 +171,20 @@ public GlobalConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializat config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0)); config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20)); + JsonElement userJava = obj.get("userJava"); + if (userJava != null && userJava.isJsonArray()) { + for (JsonElement element : userJava.getAsJsonArray()) { + config.userJava.add(element.getAsString()); + } + } + + JsonElement disabledJava = obj.get("disabledJava"); + if (disabledJava != null && disabledJava.isJsonArray()) { + for (JsonElement element : disabledJava.getAsJsonArray()) { + config.disabledJava.add(element.getAsString()); + } + } + for (Map.Entry entry : obj.entrySet()) { if (!knownFields.contains(entry.getKey())) { config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/JavaVersionType.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/JavaVersionType.java new file mode 100644 index 0000000000..e56df50da6 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/JavaVersionType.java @@ -0,0 +1,25 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.setting; + +/** + * @author Glavo + */ +public enum JavaVersionType { + DEFAULT, AUTO, VERSION, DETECTED, CUSTOM +} 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 0d4feb2f8d..aa084d119b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java @@ -22,29 +22,23 @@ import javafx.beans.InvalidationListener; import javafx.beans.property.*; import org.jackhuang.hmcl.game.*; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.java.JavaManager; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.platform.Architecture; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; -import org.jackhuang.hmcl.util.platform.Platform; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.io.IOException; import java.lang.reflect.Type; import java.nio.file.InvalidPathException; import java.nio.file.Paths; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.concurrent.CancellationException; +import java.util.*; import java.util.stream.Collectors; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + /** - * * @author huangyuhui */ @JsonAdapter(VersionSetting.Serializer.class) @@ -84,38 +78,43 @@ public void setUsesGlobal(boolean usesGlobal) { // java - private final StringProperty javaProperty = new SimpleStringProperty(this, "java", ""); + private final ObjectProperty javaVersionTypeProperty = new SimpleObjectProperty<>(this, "javaVersionType", JavaVersionType.AUTO); - public StringProperty javaProperty() { - return javaProperty; + public ObjectProperty javaVersionTypeProperty() { + return javaVersionTypeProperty; } - /** - * Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on. - */ - public String getJava() { - return javaProperty.get(); + public JavaVersionType getJavaVersionType() { + return javaVersionTypeProperty.get(); } - public void setJava(String java) { - javaProperty.set(java); + public void setJavaVersionType(JavaVersionType javaVersionType) { + javaVersionTypeProperty.set(javaVersionType); } - public boolean isUsesCustomJavaDir() { - return "Custom".equals(getJava()); + private final StringProperty javaVersionProperty = new SimpleStringProperty(this, "javaVersion", ""); + + public StringProperty javaVersionProperty() { + return javaVersionProperty; } - public void setUsesCustomJavaDir() { - setJava("Custom"); - setDefaultJavaPath(null); + public String getJavaVersion() { + return javaVersionProperty.get(); + } + + public void setJavaVersion(String java) { + javaVersionProperty.set(java); } - public boolean isJavaAutoSelected() { - return "Auto".equals(getJava()); + public void setUsesCustomJavaDir() { + setJavaVersionType(JavaVersionType.CUSTOM); + setJavaVersion(""); + setDefaultJavaPath(null); } public void setJavaAutoSelected() { - setJava("Auto"); + setJavaVersionType(JavaVersionType.AUTO); + setJavaVersion(""); setDefaultJavaPath(null); } @@ -644,58 +643,74 @@ public void setLauncherVisibility(LauncherVisibility launcherVisibility) { launcherVisibilityProperty.set(launcherVisibility); } - public Task getJavaVersion(GameVersionNumber gameVersion, Version version) { - return getJavaVersion(gameVersion, version, true); - } - - public Task getJavaVersion(GameVersionNumber gameVersion, Version version, boolean checkJava) { - return Task.runAsync(Schedulers.javafx(), () -> { - if (StringUtils.isBlank(getJava())) { - setJava(StringUtils.isBlank(getJavaDir()) ? "Auto" : "Custom"); + public JavaRuntime getJava(GameVersionNumber gameVersion, Version version) throws InterruptedException { + switch (getJavaVersionType()) { + case DEFAULT: + return JavaRuntime.getDefault(); + case AUTO: + return JavaManager.findSuitableJava(gameVersion, version); + case CUSTOM: + try { + return JavaManager.getJava(Paths.get(getJavaDir())); + } catch (IOException | InvalidPathException e) { + return null; // Custom Java not found + } + case VERSION: { + String javaVersion = getJavaVersion(); + if (StringUtils.isBlank(javaVersion)) { + return JavaManager.findSuitableJava(gameVersion, version); + } + + int majorVersion = -1; + try { + majorVersion = Integer.parseInt(javaVersion); + } catch (NumberFormatException ignored) { + } + + if (majorVersion < 0) { + LOG.warning("Invalid Java version: " + javaVersion); + return null; + } + + final int finalMajorVersion = majorVersion; + Collection allJava = JavaManager.getAllJava().stream() + .filter(it -> it.getParsedVersion() == finalMajorVersion) + .collect(Collectors.toList()); + return JavaManager.findSuitableJava(allJava, gameVersion, version); } - }).thenSupplyAsync(() -> { - try { - if ("Default".equals(getJava())) { - return JavaVersion.fromCurrentEnvironment(); - } else if (isJavaAutoSelected()) { - return JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version); - } else if (isUsesCustomJavaDir()) { - try { - if (checkJava) - return JavaVersion.fromExecutable(Paths.get(getJavaDir())); - else - return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.UNKNOWN)); - } catch (IOException | InvalidPathException e) { - return null; // Custom Java Directory not found, + case DETECTED: { + String javaVersion = getJavaVersion(); + if (StringUtils.isBlank(javaVersion)) { + return JavaManager.findSuitableJava(gameVersion, version); + } + + try { + String defaultJavaPath = getDefaultJavaPath(); + if (StringUtils.isNotBlank(defaultJavaPath)) { + JavaRuntime java = JavaManager.getJava(Paths.get(defaultJavaPath).toRealPath()); + if (java != null && java.getVersion().equals(javaVersion)) { + return java; + } } - } else if (StringUtils.isNotBlank(getJava())) { - List matchedJava = JavaVersion.getJavas().stream() - .filter(java -> java.getVersion().equals(getJava())) - .collect(Collectors.toList()); - if (matchedJava.isEmpty()) { - FXUtils.runInFX(() -> setJava("Auto")); - return JavaVersion.fromCurrentEnvironment(); - } else { - return matchedJava.stream() - .filter(java -> java.getBinary().toString().equals(getDefaultJavaPath())) - .findFirst() - .orElse(matchedJava.get(0)); + } catch (IOException | InvalidPathException ignored) { + } + + for (JavaRuntime java : JavaManager.getAllJava()) { + if (java.getVersion().equals(javaVersion)) { + return java; } - } else throw new Error(); - } catch (InterruptedException e) { - throw new CancellationException(); - } - }); - } + } - public void setJavaVersion(JavaVersion java) { - setJava(java.getVersion()); - setDefaultJavaPath(java.getBinary().toString()); + return null; + } + default: + throw new AssertionError("JavaVersionType: " + getJavaVersionType()); + } } public void addPropertyChangedListener(InvalidationListener listener) { usesGlobalProperty.addListener(listener); - javaProperty.addListener(listener); + javaVersionProperty.addListener(listener); javaDirProperty.addListener(listener); wrapperProperty.addListener(listener); permSizeProperty.addListener(listener); @@ -733,7 +748,8 @@ public void addPropertyChangedListener(InvalidationListener listener) { public VersionSetting clone() { VersionSetting versionSetting = new VersionSetting(); versionSetting.setUsesGlobal(isUsesGlobal()); - versionSetting.setJava(getJava()); + versionSetting.setJavaVersionType(getJavaVersionType()); + versionSetting.setJavaVersion(getJavaVersion()); versionSetting.setDefaultJavaPath(getDefaultJavaPath()); versionSetting.setJavaDir(getJavaDir()); versionSetting.setWrapper(getWrapper()); @@ -787,7 +803,6 @@ public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializati obj.addProperty("precalledCommand", src.getPreLaunchCommand()); obj.addProperty("postExitCommand", src.getPostExitCommand()); obj.addProperty("serverIp", src.getServerIp()); - obj.addProperty("java", src.getJava()); obj.addProperty("wrapper", src.getWrapper()); obj.addProperty("fullscreen", src.isFullscreen()); obj.addProperty("noJVMArgs", src.isNoJVMArgs()); @@ -806,6 +821,24 @@ public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializati obj.addProperty("nativesDirType", src.getNativesDirType().ordinal()); obj.addProperty("versionIcon", src.getVersionIcon().ordinal()); + obj.addProperty("javaVersionType", src.getJavaVersionType().name()); + String java; + switch (src.getJavaVersionType()) { + case DEFAULT: + java = "Default"; + break; + case AUTO: + java = "Auto"; + break; + case CUSTOM: + java = "Custom"; + break; + default: + java = src.getJavaVersion(); + break; + } + obj.addProperty("java", java); + obj.addProperty("renderer", src.getRenderer().name()); if (src.getRenderer() == Renderer.LLVMPIPE) obj.addProperty("useSoftwareRenderer", true); @@ -846,7 +879,6 @@ public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializ vs.setPreLaunchCommand(Optional.ofNullable(obj.get("precalledCommand")).map(JsonElement::getAsString).orElse("")); vs.setPostExitCommand(Optional.ofNullable(obj.get("postExitCommand")).map(JsonElement::getAsString).orElse("")); vs.setServerIp(Optional.ofNullable(obj.get("serverIp")).map(JsonElement::getAsString).orElse("")); - vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse("")); vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse("")); vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("")); vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse("")); @@ -865,6 +897,27 @@ public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializ vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER)); vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT)); + if (obj.get("javaVersionType") != null) { + JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO); + vs.setJavaVersionType(javaVersionType); + vs.setJavaVersion(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(null)); + } else { + String java = Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""); + switch (java) { + case "Default": + vs.setJavaVersionType(JavaVersionType.DEFAULT); + break; + case "Auto": + vs.setJavaVersionType(JavaVersionType.AUTO); + break; + case "Custom": + vs.setJavaVersionType(JavaVersionType.CUSTOM); + break; + default: + vs.setJavaVersion(java); + } + } + vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString) .flatMap(name -> { try { @@ -892,5 +945,25 @@ else if (primitive.isNumber()) else return Lang.parseInt(primitive.getAsString(), defaultValue); } + + private > E parseJsonPrimitive(JsonPrimitive primitive, Class clazz, E defaultValue) { + if (primitive == null) + return defaultValue; + else { + E[] enumConstants = clazz.getEnumConstants(); + if (primitive.isNumber()) { + int index = primitive.getAsInt(); + return index >= 0 && index < enumConstants.length ? enumConstants[index] : defaultValue; + } else { + String name = primitive.getAsString(); + for (E enumConstant : enumConstants) { + if (enumConstant.name().equalsIgnoreCase(name)) { + return enumConstant; + } + } + return defaultValue; + } + } + } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index af9c01d4ad..724eb7deff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -37,6 +37,7 @@ import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.game.ModpackHelper; +import org.jackhuang.hmcl.java.JavaManager; import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; @@ -53,7 +54,6 @@ import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.Architecture; -import org.jackhuang.hmcl.util.platform.JavaVersion; import org.jackhuang.hmcl.util.platform.OperatingSystem; import java.io.File; @@ -252,7 +252,7 @@ public static void initialize(Stage stage) { dialog(i18n("launcher.cache_directory.invalid")); } - Task.runAsync(JavaVersion::initialize).start(); + Lang.thread(JavaManager::initialize, "Search Java", true); scene = new Scene(decorator.getDecorator()); scene.setFill(Color.TRANSPARENT); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 79bc0a8970..b7394c40e7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -31,6 +31,7 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; +import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.*; import javafx.scene.image.Image; @@ -57,6 +58,7 @@ import org.jackhuang.hmcl.ui.construct.JFXHyperlink; import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.ResourceNotFoundError; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.javafx.ExtendedProperties; import org.jackhuang.hmcl.util.javafx.SafeStringConverter; @@ -837,6 +839,17 @@ public static JFXButton newBorderButton(String text) { return button; } + public static Label truncatedLabel(String text, int limit) { + Label label = new Label(); + if (text.length() <= limit) { + label.setText(text); + } else { + label.setText(StringUtils.truncate(text, limit)); + installFastTooltip(label, text); + } + return label; + } + public static void applyDragListener(Node node, FileFilter filter, Consumer> callback) { applyDragListener(node, filter, callback, null); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index fa80d8cedf..9231c4b37f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -110,7 +110,8 @@ public enum SVG { LAN("M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z"), THUMB_UP_OUTLINE("M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z"), THUMB_DOWN_OUTLINE("M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z"), - SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z"); + SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z"), + JAVA("m 12.400746,23.498132 c 0,0 -1.047682,0.609185 0.745753,0.816159 2.173248,0.247515 3.284943,0.212301 5.680103,-0.241112 0,0 0.629462,0.394742 1.509642,0.737207 -5.369636,2.301245 -12.1518319,-0.133351 -7.934431,-1.312254 z m -0.656135,-3.003246 c 0,0 -1.175708,0.870569 0.619861,1.056206 2.321545,0.238977 4.155521,0.25925 7.32844,-0.35207 0,0 0.43849,0.444887 1.128765,0.688135 -6.492002,1.897965 -13.7233551,0.149351 -9.077066,-1.392271 z m 5.531806,-5.094318 c 1.322937,1.523496 -0.347807,2.894427 -0.347807,2.894427 0,0 3.359626,-1.733668 1.816908,-3.905821 -1.441364,-2.024926 -2.545591,-3.030987 3.43644,-6.5004569 0,0 -9.389665,2.3449859 -4.905541,7.5129179 z m 7.101193,10.318794 c 0,0 0.775626,0.639057 -0.854578,1.133019 C 20.42373,27.79123 10.623314,28.075018 7.9006204,26.88973 6.9222859,26.464048 8.75733,25.873001 9.3345153,25.749244 9.9362392,25.619093 10.280844,25.642568 10.280844,25.642568 9.1926195,24.875486 3.2457961,27.147927 7.2604887,27.798718 18.208875,29.573994 27.21766,26.999632 24.37761,25.718316 Z M 12.904316,17.383886 c 0,0 -4.9855587,1.184229 -1.765696,1.614178 1.359213,0.182426 4.069103,0.140825 6.594422,-0.07042 2.063359,-0.173899 4.135252,-0.544104 4.135252,-0.544104 0,0 -0.727616,0.311527 -1.253592,0.671064 -5.062375,1.331456 -14.8414526,0.711604 -12.02594,-0.649727 2.38129,-1.151156 4.316621,-1.019929 4.316621,-1.019929 z m 8.943706,4.998298 c 5.146658,-2.673583 2.766435,-5.24368 1.106361,-4.898013 -0.407551,0.08428 -0.58892,0.1579 -0.58892,0.1579 0,0 0.151501,-0.236845 0.439557,-0.339265 3.284942,-1.155424 5.812393,3.406524 -1.060485,5.213807 0,0 0.08003,-0.07147 0.103478,-0.134425 z M 18.744451,2.2855 c 0,0 2.849653,2.8506848 -2.703489,7.2344656 -4.453183,3.5164124 -1.015676,5.5221324 -0.0021,7.8127104 C 13.439893,14.987688 11.532301,12.92329 12.811497,11.001852 14.690284,8.1810394 19.893487,6.8143757 18.743384,2.2855 Z M 13.41002,29.628384 c 4.939684,0.315794 12.525242,-0.174974 12.70448,-2.512485 0,0 -0.345672,0.886571 -4.081906,1.589641 -4.216334,0.793754 -9.416337,0.700936 -12.4996379,0.192025 0,0 0.6315969,0.522768 3.8781309,0.730807 z"); private final String path; 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 cd6506fb15..d7e224baa0 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 @@ -57,7 +57,7 @@ public MultiFileItem() { if (toggleSelectedListener != null) toggleSelectedListener.accept(newValue); - selectedData.set((T) newValue.getUserData()); + selectedData.set(newValue != null ? (T) newValue.getUserData() : null); }); selectedData.addListener((a, b, newValue) -> { Optional selecting = group.getToggles().stream() @@ -183,6 +183,10 @@ public StringOption(String title, T data) { super(title, data); } + public JFXTextField getCustomField() { + return customField; + } + public String getValue() { return customField.getText(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index 0a846af836..9260cbf778 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -34,7 +34,7 @@ import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask; import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; import org.jackhuang.hmcl.download.game.GameInstallTask; -import org.jackhuang.hmcl.download.java.JavaDownloadTask; +import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask; import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask; import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask; import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask; @@ -42,6 +42,7 @@ import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask; import org.jackhuang.hmcl.download.quilt.QuiltInstallTask; import org.jackhuang.hmcl.game.HMCLModpackInstallTask; +import org.jackhuang.hmcl.java.JavaInstallTask; import org.jackhuang.hmcl.mod.MinecraftInstanceTask; import org.jackhuang.hmcl.mod.ModpackInstallTask; import org.jackhuang.hmcl.mod.ModpackUpdateTask; @@ -159,8 +160,10 @@ public void onRunning(Task task) { task.setName(i18n("modpack.export")); } else if (task instanceof MinecraftInstanceTask) { task.setName(i18n("modpack.scan")); - } else if (task instanceof JavaDownloadTask) { + } else if (task instanceof MojangJavaDownloadTask) { task.setName(i18n("download.java")); + } else if (task instanceof JavaInstallTask) { + task.setName(i18n("java.install")); } Platform.runLater(() -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java new file mode 100644 index 0000000000..7e4e80f66d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaDownloadDialog.java @@ -0,0 +1,423 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.ui.main; + +import com.jfoenix.controls.*; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.control.Label; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.download.java.JavaDistribution; +import org.jackhuang.hmcl.download.java.JavaPackageType; +import org.jackhuang.hmcl.download.java.JavaRemoteVersion; +import org.jackhuang.hmcl.download.java.disco.*; +import org.jackhuang.hmcl.game.GameJavaVersion; +import org.jackhuang.hmcl.java.JavaInfo; +import org.jackhuang.hmcl.java.JavaManager; +import org.jackhuang.hmcl.setting.DownloadProviders; +import org.jackhuang.hmcl.task.*; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; +import org.jackhuang.hmcl.ui.construct.DialogPane; +import org.jackhuang.hmcl.ui.construct.JFXHyperlink; +import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider; +import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.platform.Platform; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CancellationException; + +import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +import static org.jackhuang.hmcl.util.Lang.resolveException; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/** + * @author Glavo + */ +public final class JavaDownloadDialog extends StackPane { + + public static Runnable showDialogAction(DownloadProvider downloadProvider) { + Platform platform = Platform.SYSTEM_PLATFORM; + + List supportedVersions = GameJavaVersion.getSupportedVersions(platform); + + EnumSet distributions = EnumSet.noneOf(DiscoJavaDistribution.class); + for (DiscoJavaDistribution distribution : DiscoJavaDistribution.values()) { + if (distribution.isSupport(platform)) { + distributions.add(distribution); + } + } + + return supportedVersions.isEmpty() && distributions.isEmpty() + ? null + : () -> Controllers.dialog(new JavaDownloadDialog(downloadProvider, platform, supportedVersions, distributions)); + } + + private final DownloadProvider downloadProvider; + private final Platform platform; + private final List supportedGameJavaVersions; + private final EnumSet distributions; + + private JavaDownloadDialog(DownloadProvider downloadProvider, Platform platform, List supportedGameJavaVersions, EnumSet distributions) { + this.downloadProvider = downloadProvider; + this.platform = platform; + this.supportedGameJavaVersions = supportedGameJavaVersions; + this.distributions = distributions; + + if (!supportedGameJavaVersions.isEmpty()) { + this.getChildren().add(new DownloadMojangJava()); + } else { + this.getChildren().add(new DownloadDiscoJava()); + } + } + + private final class DownloadMojangJava extends DialogPane { + private final ToggleGroup toggleGroup = new ToggleGroup(); + + DownloadMojangJava() { + setTitle(i18n("java.download")); + validProperty().bind(toggleGroup.selectedToggleProperty().isNotNull()); + + VBox vbox = new VBox(16); + Label prompt = new Label(i18n("java.download.prompt")); + vbox.getChildren().add(prompt); + + for (GameJavaVersion version : supportedGameJavaVersions) { + JFXRadioButton button = new JFXRadioButton("Java " + version.getMajorVersion()); + button.setUserData(version); + vbox.getChildren().add(button); + toggleGroup.getToggles().add(button); + } + + setBody(vbox); + + if (!distributions.isEmpty()) { + JFXHyperlink more = new JFXHyperlink(i18n("java.download.more")); + more.setOnAction(event -> JavaDownloadDialog.this.getChildren().setAll(new DownloadDiscoJava())); + setActions(warningLabel, more, acceptPane, cancelButton); + } else + setActions(warningLabel, acceptPane, cancelButton); + } + + private Task downloadTask(GameJavaVersion javaVersion) { + return JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null) { + Throwable resolvedException = resolveException(exception); + LOG.warning("Failed to download java", exception); + if (!(resolvedException instanceof CancellationException)) { + Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed")); + } + } + }); + } + + @Override + protected void onAccept() { + fireEvent(new DialogCloseEvent()); + + GameJavaVersion javaVersion = (GameJavaVersion) toggleGroup.getSelectedToggle().getUserData(); + if (javaVersion == null) + return; + + if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion)) + Controllers.confirm(i18n("download.java.override"), null, () -> { + Controllers.taskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion)) + .thenComposeAsync(Schedulers.javafx(), realPath -> { + if (realPath != null) { + JavaManager.removeJava(realPath); + } + return downloadTask(javaVersion); + }), i18n("download.java"), TaskCancellationAction.NORMAL); + }, null); + else + Controllers.taskDialog(downloadTask(javaVersion), i18n("download.java"), TaskCancellationAction.NORMAL); + } + } + + private final class DownloadDiscoJava extends JFXDialogLayout { + + private boolean isLTS(int major) { + if (major <= 8) { + return true; + } + + if (major < 21) { + return major == 11 || major == 17; + } + + return major % 4 == 1; + } + + private final JFXComboBox distributionBox; + private final JFXComboBox remoteVersionBox; + private final JFXComboBox packageTypeBox; + private final Label warningLabel = new Label(); + + private final JFXButton downloadButton; + private final StackPane downloadButtonPane = new StackPane(); + + private final DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider(); + + private final ObjectProperty currentDiscoJavaVersionList = new SimpleObjectProperty<>(); + + private final Map, DiscoJavaVersionList> javaVersionLists = new HashMap<>(); + + private boolean changingDistribution = false; + + DownloadDiscoJava() { + assert !distributions.isEmpty(); + + this.distributionBox = new JFXComboBox<>(); + this.distributionBox.setConverter(FXUtils.stringConverter(JavaDistribution::getDisplayName)); + + this.remoteVersionBox = new JFXComboBox<>(); + this.remoteVersionBox.setConverter(FXUtils.stringConverter(JavaRemoteVersion::getDistributionVersion)); + + this.packageTypeBox = new JFXComboBox<>(); + + this.downloadButton = new JFXButton(i18n("download")); + downloadButton.setOnAction(e -> onDownload()); + downloadButton.getStyleClass().add("dialog-accept"); + downloadButton.disableProperty().bind(Bindings.isNull(remoteVersionBox.getSelectionModel().selectedItemProperty())); + downloadButtonPane.getChildren().setAll(downloadButton); + + JFXButton cancelButton = new JFXButton(i18n("button.cancel")); + cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); + cancelButton.getStyleClass().add("dialog-cancel"); + onEscPressed(this, cancelButton::fire); + + GridPane body = new GridPane(); + body.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing()); + body.setVgap(8); + body.setHgap(16); + + body.addRow(0, new Label(i18n("java.download.distribution")), distributionBox); + body.addRow(1, new Label(i18n("java.download.version")), remoteVersionBox); + body.addRow(2, new Label(i18n("java.download.packageType")), packageTypeBox); + + distributionBox.setItems(FXCollections.observableList(new ArrayList<>(distributions))); + ChangeListener updateStatusListener = (observable, oldValue, newValue) -> updateStatus(newValue); + this.currentDiscoJavaVersionList.addListener((observable, oldValue, newValue) -> { + if (oldValue != null) { + oldValue.status.removeListener(updateStatusListener); + } + + if (newValue != null) { + newValue.status.addListener(updateStatusListener); + updateStatus(newValue.status.get()); + } else { + updateStatus(null); + } + }); + + packageTypeBox.getSelectionModel().selectedItemProperty().addListener(ignored -> updateVersions()); + FXUtils.onChangeAndOperate(distributionBox.getSelectionModel().selectedItemProperty(), distribution -> { + if (distribution != null) { + changingDistribution = true; + packageTypeBox.setItems(FXCollections.observableList(new ArrayList<>(distribution.getSupportedPackageTypes()))); + packageTypeBox.getSelectionModel().select(0); + changingDistribution = false; + updateVersions(); + packageTypeBox.setDisable(false); + remoteVersionBox.setDisable(false); + } else { + packageTypeBox.setItems(null); + updateVersions(); + remoteVersionBox.setItems(null); + packageTypeBox.setDisable(true); + remoteVersionBox.setDisable(true); + } + }); + + setHeading(new Label(i18n("java.download"))); + setBody(body); + setActions(warningLabel, downloadButtonPane, cancelButton); + } + + private void updateStatus(DiscoJavaVersionList.Status status) { + if (status == DiscoJavaVersionList.Status.LOADING) { + downloadButtonPane.getChildren().setAll(new JFXSpinner()); + remoteVersionBox.setDisable(true); + warningLabel.setText(null); + } else { + downloadButtonPane.getChildren().setAll(downloadButton); + if (status == DiscoJavaVersionList.Status.SUCCESS || status == null) { + remoteVersionBox.setDisable(false); + warningLabel.setText(null); + } else if (status == DiscoJavaVersionList.Status.FAILED) { + remoteVersionBox.setDisable(true); + warningLabel.setText(i18n("java.download.load_list.failed")); + } + } + } + + private void onDownload() { + fireEvent(new DialogCloseEvent()); + + DiscoJavaDistribution distribution = distributionBox.getSelectionModel().getSelectedItem(); + DiscoJavaRemoteVersion version = remoteVersionBox.getSelectionModel().getSelectedItem(); + + if (version == null) + return; + + Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().getPkgInfoUri())) + .setExecutor(Schedulers.io()) + .thenComposeAsync(json -> { + DiscoResult result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoRemoteFileInfo.class)); + if (result.getResult().size() != 1) + throw new IOException("Illegal result: " + json); + + DiscoRemoteFileInfo fileInfo = result.getResult().get(0); + if (!fileInfo.getChecksumType().equals("sha1") && !fileInfo.getChecksumType().equals("sha256")) + throw new IOException("Unsupported checksum type: " + fileInfo.getChecksumType()); + if (StringUtils.isBlank(fileInfo.getDirectDownloadUri())) + throw new IOException("Missing download URI: " + json); + + File targetFile = File.createTempFile("hmcl-java-", "." + version.getArchiveType()); + targetFile.deleteOnExit(); + + Task getIntegrityCheck; + if (StringUtils.isNotBlank(fileInfo.getChecksum())) + getIntegrityCheck = Task.completed(new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), fileInfo.getChecksum())); + else if (StringUtils.isNotBlank(fileInfo.getChecksumUri())) + getIntegrityCheck = new GetTask(downloadProvider.injectURLWithCandidates(fileInfo.getChecksumUri())) + .thenApplyAsync(checksum -> + new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), checksum.trim())); + else + throw new IOException("Unable to get checksum for file"); + + return getIntegrityCheck + .thenComposeAsync(integrityCheck -> + new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()), + targetFile, integrityCheck).setName(fileInfo.getFileName())) + .thenSupplyAsync(targetFile::toPath); + }) + .whenComplete(Schedulers.javafx(), ((result, exception) -> { + if (exception == null) { + String javaVersion = version.getJavaVersion(); + JavaInfo info = new JavaInfo(platform, javaVersion, distribution.getVendor()); + + Map updateInfo = new LinkedHashMap<>(); + updateInfo.put("type", "disco"); + updateInfo.put("info", version); + + int idx = javaVersion.indexOf('+'); + if (idx > 0) { + javaVersion = javaVersion.substring(0, idx); + } + String defaultName = distribution.getApiParameter() + "-" + javaVersion; + Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> + new JavaInstallPage(controller::onFinish, info, version, updateInfo, defaultName, result))); + } else { + LOG.warning("Failed to download java", exception); + Throwable resolvedException = resolveException(exception); + if (!(resolvedException instanceof CancellationException)) { + Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed")); + } + } + })), i18n("java.download"), TaskCancellationAction.NORMAL); + + } + + private void updateVersions() { + if (changingDistribution) return; + + DiscoJavaDistribution distribution = distributionBox.getSelectionModel().getSelectedItem(); + if (distribution == null) { + this.currentDiscoJavaVersionList.set(null); + return; + } + + JavaPackageType packageType = packageTypeBox.getSelectionModel().getSelectedItem(); + + DiscoJavaVersionList list = javaVersionLists.computeIfAbsent(Pair.pair(distribution, packageType), pair -> { + DiscoJavaVersionList res = new DiscoJavaVersionList(); + new DiscoFetchJavaListTask(downloadProvider, distribution, platform, packageType).setExecutor(Schedulers.io()).thenApplyAsync(versions -> { + if (versions.isEmpty()) return Collections.emptyList(); + + int lastLTS = -1; + for (int v : versions.keySet()) { + if (isLTS(v)) { + lastLTS = v; + } + } + + ArrayList remoteVersions = new ArrayList<>(); + for (Map.Entry entry : versions.entrySet()) { + int v = entry.getKey(); + if (v >= lastLTS || isLTS(v) || v == 16) { + remoteVersions.add(entry.getValue()); + } + } + Collections.reverse(remoteVersions); + return remoteVersions; + }).whenComplete(Schedulers.javafx(), ((result, exception) -> { + if (exception == null) { + res.status.set(DiscoJavaVersionList.Status.SUCCESS); + res.versions.setAll(result); + selectLTS(res); + } else { + LOG.warning("Failed to load java list", exception); + res.status.set(DiscoJavaVersionList.Status.FAILED); + } + })).start(); + return res; + }); + this.currentDiscoJavaVersionList.set(list); + this.remoteVersionBox.setItems(list.versions); + selectLTS(list); + } + + private void selectLTS(DiscoJavaVersionList list) { + if (remoteVersionBox.getItems() == list.versions) { + for (int i = 0; i < list.versions.size(); i++) { + JavaRemoteVersion item = list.versions.get(i); + if (item.getJdkVersion() == GameJavaVersion.LATEST.getMajorVersion()) { + remoteVersionBox.getSelectionModel().select(i); + break; + } + } + } + } + } + + private static final class DiscoJavaVersionList { + enum Status { + LOADING, SUCCESS, FAILED + } + + final ObservableList versions = FXCollections.observableArrayList(); + final ObjectProperty status = new SimpleObjectProperty<>(Status.LOADING); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaInstallPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaInstallPage.java new file mode 100644 index 0000000000..90018444d1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaInstallPage.java @@ -0,0 +1,190 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.ui.main; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXTextField; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.download.java.JavaRemoteVersion; +import org.jackhuang.hmcl.download.java.disco.DiscoJavaDistribution; +import org.jackhuang.hmcl.download.java.disco.DiscoJavaRemoteVersion; +import org.jackhuang.hmcl.java.HMCLJavaRepository; +import org.jackhuang.hmcl.java.JavaInfo; +import org.jackhuang.hmcl.java.JavaManager; +import org.jackhuang.hmcl.java.JavaRuntime; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.wizard.WizardSinglePage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class JavaInstallPage extends WizardSinglePage { + + private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9.\\-_]+"); + + private final Path file; + + private final JavaInfo info; + private final JavaRemoteVersion remoteVersion; + private final Map update; + private final StringProperty nameProperty = new SimpleStringProperty(); + + public JavaInstallPage(Runnable onFinish, JavaInfo info, JavaRemoteVersion remoteVersion, Map update, String defaultName, Path file) { + super(onFinish); + this.info = info; + this.remoteVersion = remoteVersion; + this.update = update; + this.file = file; + this.nameProperty.set(defaultName); + } + + @Override + protected SkinBase createDefaultSkin() { + return new Skin(this); + } + + @Override + protected Object finish() { + Task installTask = JavaManager.getInstallJavaTask(info.getPlatform(), nameProperty.get(), update, file); + return remoteVersion == null ? installTask : installTask.whenComplete(exception -> { + try { + Files.delete(file); + } catch (IOException e) { + LOG.warning("Failed to delete file: " + file, e); + } + }); + } + + @Override + public String getTitle() { + return i18n("java.install"); + } + + private static final class Skin extends SkinBase { + + private final ComponentList componentList = new ComponentList(); + + private final JFXTextField nameField; + + private final Set usedNames = new HashSet<>(); + + Skin(JavaInstallPage control) { + super(control); + + VBox borderPane = new VBox(); + borderPane.setAlignment(Pos.CENTER); + FXUtils.setLimitWidth(borderPane, 500); + + + { + BorderPane namePane = new BorderPane(); + { + Label label = new Label(i18n("java.install.name")); + BorderPane.setAlignment(label, Pos.CENTER_LEFT); + namePane.setLeft(label); + + nameField = new JFXTextField(); + nameField.textProperty().bindBidirectional(control.nameProperty); + FXUtils.setLimitWidth(nameField, 200); + BorderPane.setAlignment(nameField, Pos.CENTER_RIGHT); + BorderPane.setMargin(nameField, new Insets(0, 0, 12, 0)); + namePane.setRight(nameField); + nameField.setValidators( + new RequiredValidator(), + new Validator(i18n("java.install.warning.invalid_character"), + text -> !text.startsWith(HMCLJavaRepository.MOJANG_JAVA_PREFIX) && NAME_PATTERN.matcher(text).matches()), + new Validator(i18n("java.install.failed.exists"), text -> !usedNames.contains(text)) + ); + String defaultName = control.nameProperty.get(); + if (JavaManager.REPOSITORY.isInstalled(control.info.getPlatform(), defaultName)) { + usedNames.add(defaultName); + } + nameField.textProperty().addListener(o -> nameField.validate()); + nameField.validate(); + + componentList.getContent().add(namePane); + } + + String vendor = JavaInfo.normalizeVendor(control.info.getVendor()); + if (vendor != null) + addInfo(i18n("java.info.vendor"), vendor); + + if (control.remoteVersion instanceof DiscoJavaRemoteVersion) { + String distributionName = ((DiscoJavaRemoteVersion) control.remoteVersion).getDistribution(); + DiscoJavaDistribution distribution = DiscoJavaDistribution.of(distributionName); + addInfo(i18n("java.info.disco.distribution"), distribution != null ? distribution.getDisplayName() : distributionName); + } else + addInfo(i18n("java.install.archive"), control.file.toAbsolutePath().toString()); + + addInfo(i18n("java.info.version"), control.info.getVersion()); + addInfo(i18n("java.info.architecture"), control.info.getPlatform().getArchitecture().getDisplayName()); + + BorderPane installPane = new BorderPane(); + { + JFXButton installButton = FXUtils.newRaisedButton(i18n("button.install")); + installButton.setOnAction(e -> { + String name = control.nameProperty.get(); + if (JavaManager.REPOSITORY.isInstalled(control.info.getPlatform(), name)) { + Controllers.dialog(i18n("java.install.failed.exists"), null, MessageDialogPane.MessageType.WARNING); + usedNames.add(name); + nameField.validate(); + } else + control.onFinish.run(); + }); + installButton.disableProperty().bind(nameField.activeValidatorProperty().isNotNull()); + installPane.setRight(installButton); + + componentList.getContent().add(installPane); + } + } + + borderPane.getChildren().setAll(componentList); + this.getChildren().setAll(borderPane); + } + + private void addInfo(String name, String value) { + BorderPane pane = new BorderPane(); + + pane.setLeft(new Label(name)); + + Label valueLabel = FXUtils.truncatedLabel(value, 60); + BorderPane.setAlignment(valueLabel, Pos.CENTER_RIGHT); + pane.setCenter(valueLabel); + + this.componentList.getContent().add(pane); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java new file mode 100644 index 0000000000..86d3a0c2ea --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaManagementPage.java @@ -0,0 +1,322 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.ui.main; + +import com.jfoenix.controls.JFXButton; +import javafx.beans.binding.Bindings; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Skin; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.stage.FileChooser; +import org.jackhuang.hmcl.java.JavaInfo; +import org.jackhuang.hmcl.java.JavaManager; +import org.jackhuang.hmcl.java.JavaRuntime; +import org.jackhuang.hmcl.setting.ConfigHolder; +import org.jackhuang.hmcl.setting.DownloadProviders; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; +import org.jackhuang.hmcl.ui.construct.RipplerContainer; +import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider; +import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException; +import org.jackhuang.hmcl.util.tree.ArchiveFileTree; +import org.jackhuang.hmcl.util.platform.Architecture; +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.platform.Platform; +import org.jackhuang.hmcl.util.tree.TarFileTree; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/** + * @author Glavo + */ +public final class JavaManagementPage extends ListPageBase { + + @SuppressWarnings("FieldCanBeLocal") + private final ChangeListener> listener; + + private final Runnable onInstallJava; + + public JavaManagementPage() { + this.listener = FXUtils.onWeakChangeAndOperate(JavaManager.getAllJavaProperty(), this::loadJava); + + if (Platform.SYSTEM_PLATFORM.equals(OperatingSystem.LINUX, Architecture.LOONGARCH64_OW)) { + onInstallJava = () -> FXUtils.openLink("https://www.loongnix.cn/zh/api/java/"); + } else { + onInstallJava = JavaDownloadDialog.showDialogAction(DownloadProviders.getDownloadProvider()); + } + + FXUtils.applyDragListener(this, it -> { + String name = it.getName(); + return it.isDirectory() || name.endsWith(".zip") || name.endsWith(".tar.gz") || name.equals(OperatingSystem.CURRENT_OS.getJavaExecutable()); + }, files -> { + for (File file : files) { + if (file.isDirectory()) { + onAddJavaHome(file.toPath()); + } else { + String fileName = file.getName(); + + if (fileName.equals(OperatingSystem.CURRENT_OS.getJavaExecutable())) { + onAddJavaBinary(file.toPath()); + } else if (fileName.endsWith(".zip") || fileName.endsWith(".tar.gz")) { + onInstallArchive(file.toPath()); + } else { + throw new AssertionError("Unreachable code"); + } + } + } + }); + } + + @Override + protected Skin createDefaultSkin() { + return new JavaPageSkin(this); + } + + void onAddJava() { + FileChooser chooser = new FileChooser(); + if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe")); + chooser.setTitle(i18n("settings.game.java_directory.choose")); + File file = chooser.showOpenDialog(Controllers.getStage()); + if (file != null) { + JavaManager.getAddJavaTask(file.toPath()).whenComplete(Schedulers.javafx(), exception -> { + if (exception != null) { + LOG.warning("Failed to add java", exception); + Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + } + }).start(); + } + } + + void onShowRestoreJavaPage() { + Controllers.navigate(new JavaRestorePage(ConfigHolder.globalConfig().getDisabledJava())); + } + + private void onAddJavaBinary(Path file) { + JavaManager.getAddJavaTask(file).whenComplete(Schedulers.javafx(), exception -> { + if (exception != null) { + LOG.warning("Failed to add java", exception); + Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + } + }).start(); + } + + private void onAddJavaHome(Path file) { + Task.composeAsync(() -> { + Path releaseFile = file.resolve("release"); + if (Files.notExists(releaseFile)) + throw new IOException("Missing release file " + releaseFile); + return JavaManager.getAddJavaTask(file.resolve("bin").resolve(OperatingSystem.CURRENT_OS.getJavaExecutable())); + }).whenComplete(Schedulers.javafx(), exception -> { + if (exception != null) { + LOG.warning("Failed to add java", exception); + Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + } + }).start(); + } + + private void onInstallArchive(Path file) { + Task.supplyAsync(() -> { + try (ArchiveFileTree tree = TarFileTree.open(file)) { + JavaInfo info = JavaInfo.fromArchive(tree); + + if (!JavaManager.isCompatible(info.getPlatform())) + throw new UnsupportedPlatformException(info.getPlatform().toString()); + + return Pair.pair(tree.getRoot().getSubDirs().keySet().iterator().next(), info); + } + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> + new JavaInstallPage(controller::onFinish, result.getValue(), null, null, result.getKey(), file))); + } else { + if (exception instanceof UnsupportedPlatformException) { + Controllers.dialog(i18n("java.install.failed.unsupported_platform"), null, MessageDialogPane.MessageType.WARNING); + } else { + Controllers.dialog(i18n("java.install.failed.invalid"), null, MessageDialogPane.MessageType.WARNING); + } + } + }).start(); + } + + // FXThread + private void loadJava(Collection javaRuntimes) { + if (javaRuntimes != null) { + List items = new ArrayList<>(); + for (JavaRuntime java : javaRuntimes) { + items.add(new JavaItem(java)); + } + this.setItems(FXCollections.observableList(items)); + this.setLoading(false); + } else + this.setLoading(true); + } + + static final class JavaItem extends Control { + private final JavaRuntime java; + + public JavaItem(JavaRuntime java) { + this.java = java; + } + + public JavaRuntime getJava() { + return java; + } + + public void onReveal() { + Path target; + Path parent = java.getBinary().getParent(); + if (parent != null + && parent.getParent() != null + && parent.getFileName() != null + && parent.getFileName().toString().equals("bin") + && Files.exists(parent.getParent().resolve("release"))) { + target = parent.getParent(); + } else { + target = java.getBinary(); + } + + FXUtils.showFileInExplorer(target); + } + + public void onRemove() { + if (java.isManaged()) { + Controllers.taskDialog(JavaManager.getUninstallJavaTask(java), i18n("java.uninstall"), TaskCancellationAction.NORMAL); + } else { + String path = java.getBinary().toString(); + ConfigHolder.globalConfig().getUserJava().remove(path); + ConfigHolder.globalConfig().getDisabledJava().add(path); + try { + JavaManager.removeJava(java); + } catch (InterruptedException ignored) { + } + } + } + + @Override + protected Skin createDefaultSkin() { + return new JavaRuntimeItemSkin(this); + } + + } + + private static final class JavaRuntimeItemSkin extends SkinBase { + + JavaRuntimeItemSkin(JavaItem control) { + super(control); + JavaRuntime java = control.getJava(); + String vendor = JavaInfo.normalizeVendor(java.getVendor()); + + BorderPane root = new BorderPane(); + + HBox center = new HBox(); + center.setMouseTransparent(true); + center.setSpacing(8); + center.setAlignment(Pos.CENTER_LEFT); + + TwoLineListItem item = new TwoLineListItem(); + item.setTitle((java.isJDK() ? "JDK" : "JRE") + " " + java.getVersion()); + item.setSubtitle(java.getBinary().toString()); + item.getTags().add(i18n("java.info.architecture") + ": " + java.getArchitecture().getDisplayName()); + if (vendor != null) + item.getTags().add(i18n("java.info.vendor") + ": " + vendor); + BorderPane.setAlignment(item, Pos.CENTER); + center.getChildren().setAll(item); + root.setCenter(center); + + HBox right = new HBox(); + right.setAlignment(Pos.CENTER_RIGHT); + { + JFXButton revealButton = new JFXButton(); + revealButton.getStyleClass().add("toggle-icon4"); + revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24)); + revealButton.setOnAction(e -> control.onReveal()); + FXUtils.installFastTooltip(revealButton, i18n("java.reveal")); + + JFXButton removeButton = new JFXButton(); + removeButton.getStyleClass().add("toggle-icon4"); + removeButton.setOnAction(e -> Controllers.confirm( + java.isManaged() ? i18n("java.uninstall.confirm") : i18n("java.disable.confirm"), + i18n("message.warning"), + control::onRemove, + null + )); + if (java.isManaged()) { + removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24)); + FXUtils.installFastTooltip(removeButton, i18n("java.uninstall")); + if (JavaRuntime.CURRENT_JAVA != null && java.getBinary().equals(JavaRuntime.CURRENT_JAVA.getBinary())) + removeButton.setDisable(true); + } else { + removeButton.setGraphic(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 24, 24), 24, 24)); + FXUtils.installFastTooltip(removeButton, i18n("java.disable")); + } + + right.getChildren().setAll(revealButton, removeButton); + } + root.setRight(right); + + root.getStyleClass().add("md-list-cell"); + root.setPadding(new Insets(8)); + + getChildren().setAll(new RipplerContainer(root)); + } + } + + private static final class JavaPageSkin extends ToolbarListPageSkin { + + JavaPageSkin(JavaManagementPage skinnable) { + super(skinnable); + } + + @Override + protected List initializeToolbar(JavaManagementPage skinnable) { + ArrayList res = new ArrayList<>(4); + + res.add(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, JavaManager::refresh)); + if (skinnable.onInstallJava != null) { + res.add(createToolbarButton2(i18n("java.download"), SVG.DOWNLOAD_OUTLINE, skinnable.onInstallJava)); + } + res.add(createToolbarButton2(i18n("java.add"), SVG.PLUS, skinnable::onAddJava)); + + JFXButton disableJava = createToolbarButton2(i18n("java.disabled.management"), SVG.VIEW_LIST, skinnable::onShowRestoreJavaPage); + disableJava.disableProperty().bind(Bindings.isEmpty(ConfigHolder.globalConfig().getDisabledJava())); + res.add(disableJava); + + return res; + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaRestorePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaRestorePage.java new file mode 100644 index 0000000000..7a09b8d022 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/JavaRestorePage.java @@ -0,0 +1,206 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.ui.main; + +import com.jfoenix.controls.JFXButton; +import javafx.beans.InvalidationListener; +import javafx.beans.WeakInvalidationListener; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableSet; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import org.jackhuang.hmcl.java.JavaManager; +import org.jackhuang.hmcl.setting.Theme; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; +import org.jackhuang.hmcl.ui.construct.RipplerContainer; +import org.jackhuang.hmcl.ui.decorator.DecoratorPage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/** + * @author Glavo + */ +public final class JavaRestorePage extends ListPageBase implements DecoratorPage { + + private final ObjectProperty state = new SimpleObjectProperty<>(State.fromTitle(i18n("java.disabled.management"))); + + @SuppressWarnings("FieldCanBeLocal") + private final InvalidationListener listener; + + public JavaRestorePage(ObservableSet disabledJava) { + this.listener = o -> { + ArrayList result = new ArrayList<>(disabledJava.size()); + for (String path : disabledJava) { + Path realPath = null; + + try { + realPath = Paths.get(path).toRealPath(); + } catch (IOException ignored) { + } + + result.add(new DisabledJavaItem(disabledJava, path, realPath)); + } + result.sort((a, b) -> { + if (a.realPath == null && b.realPath != null) + return -1; + if (a.realPath != null && b.realPath == null) + return 1; + return a.path.compareTo(b.path); + }); + this.setItems(FXCollections.observableList(result)); + }; + disabledJava.addListener(new WeakInvalidationListener(listener)); + listener.invalidated(disabledJava); + } + + @Override + protected Skin createDefaultSkin() { + return new JavaRestorePageSkin(this); + } + + @Override + public ReadOnlyObjectProperty stateProperty() { + return state; + } + + static final class DisabledJavaItem extends Control { + final ObservableSet disabledJava; + final String path; + final Path realPath; + + DisabledJavaItem(ObservableSet disabledJava, String path, Path realPath) { + this.disabledJava = disabledJava; + this.path = path; + this.realPath = realPath; + } + + @Override + protected Skin createDefaultSkin() { + return new DisabledJavaItemSkin(this); + } + + void onReveal() { + if (realPath != null) { + Path target; + Path parent = realPath.getParent(); + if (parent != null + && parent.getParent() != null + && parent.getFileName() != null + && parent.getFileName().toString().equals("bin") + && Files.exists(parent.getParent().resolve("release"))) { + target = parent.getParent(); + } else { + target = realPath; + } + + FXUtils.showFileInExplorer(target); + } + } + + void onRestore() { + disabledJava.remove(path); + JavaManager.getAddJavaTask(realPath).whenComplete(Schedulers.javafx(), exception -> { + if (exception != null) { + LOG.warning("Failed to add java", exception); + Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); + } + }).start(); + } + + void onRemove() { + disabledJava.remove(path); + } + } + + private static final class DisabledJavaItemSkin extends SkinBase { + DisabledJavaItemSkin(DisabledJavaItem skinnable) { + super(skinnable); + + BorderPane root = new BorderPane(); + + Label label = new Label(skinnable.path); + BorderPane.setAlignment(label, Pos.CENTER_LEFT); + root.setCenter(label); + + HBox right = new HBox(); + right.setAlignment(Pos.CENTER_RIGHT); + { + JFXButton revealButton = new JFXButton(); + revealButton.getStyleClass().add("toggle-icon4"); + revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24)); + revealButton.setOnAction(e -> skinnable.onReveal()); + FXUtils.installFastTooltip(revealButton, i18n("java.reveal")); + + if (skinnable.realPath == null) { + revealButton.setDisable(true); + + JFXButton removeButton = new JFXButton(); + removeButton.getStyleClass().add("toggle-icon4"); + removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24)); + removeButton.setOnAction(e -> skinnable.onRemove()); + FXUtils.installFastTooltip(removeButton, i18n("java.disabled.management.remove")); + + right.getChildren().setAll(revealButton, removeButton); + } else { + JFXButton restoreButton = new JFXButton(); + restoreButton.getStyleClass().add("toggle-icon4"); + restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(Theme.blackFill(), 24, 24), 24, 24)); + restoreButton.setOnAction(e -> skinnable.onRestore()); + FXUtils.installFastTooltip(restoreButton, i18n("java.disabled.management.restore")); + + right.getChildren().setAll(revealButton, restoreButton); + } + } + root.setRight(right); + + root.getStyleClass().add("md-list-cell"); + root.setPadding(new Insets(8)); + + this.getChildren().setAll(new RipplerContainer(root)); + } + } + + private static final class JavaRestorePageSkin extends ToolbarListPageSkin { + JavaRestorePageSkin(JavaRestorePage skinnable) { + super(skinnable); + } + + @Override + protected List initializeToolbar(JavaRestorePage skinnable) { + return Collections.emptyList(); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java index dbcba03c0c..155459d376 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.AdvancedListBox; import org.jackhuang.hmcl.ui.construct.PageAware; +import org.jackhuang.hmcl.ui.construct.TabControl; import org.jackhuang.hmcl.ui.construct.TabHeader; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; @@ -40,6 +41,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings"))); private final TabHeader tab; private final TabHeader.Tab gameTab = new TabHeader.Tab<>("versionSettingsPage"); + private final TabControl.Tab javaManagementTab = new TabControl.Tab<>("javaManagementPage"); private final TabHeader.Tab settingsTab = new TabHeader.Tab<>("settingsPage"); private final TabHeader.Tab personalizationTab = new TabHeader.Tab<>("personalizationPage"); private final TabHeader.Tab downloadTab = new TabHeader.Tab<>("downloadSettingsPage"); @@ -50,13 +52,14 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor public LauncherSettingsPage() { gameTab.setNodeSupplier(() -> new VersionSettingsPage(true)); + javaManagementTab.setNodeSupplier(JavaManagementPage::new); settingsTab.setNodeSupplier(SettingsPage::new); personalizationTab.setNodeSupplier(PersonalizationPage::new); downloadTab.setNodeSupplier(DownloadSettingsPage::new); helpTab.setNodeSupplier(HelpPage::new); feedbackTab.setNodeSupplier(FeedbackPage::new); aboutTab.setNodeSupplier(AboutPage::new); - tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab); + tab = new TabHeader(gameTab, javaManagementTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab); tab.select(gameTab); gameTab.initializeIfNeeded(); @@ -74,6 +77,12 @@ public LauncherSettingsPage() { runInFX(() -> FXUtils.installFastTooltip(settingsItem, i18n("settings.type.global.manage"))); settingsItem.setOnAction(e -> tab.select(gameTab)); }) + .addNavigationDrawerItem(javaItem -> { + javaItem.setTitle(i18n("java.management")); + javaItem.setLeftGraphic(wrap(SVG.WRENCH_OUTLINE)); + javaItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(javaManagementTab)); + javaItem.setOnAction(e -> tab.select(javaManagementTab)); + }) .startCategory(i18n("launcher")) .addNavigationDrawerItem(settingsItem -> { settingsItem.setTitle(i18n("settings.launcher.general")); 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 42ffc4c3d8..337d24ae01 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 @@ -22,20 +22,22 @@ import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.property.*; +import javafx.beans.value.ChangeListener; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; +import javafx.scene.control.Toggle; 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.ProcessPriority; +import org.jackhuang.hmcl.game.*; +import org.jackhuang.hmcl.java.JavaManager; import org.jackhuang.hmcl.setting.*; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.WeakListenerHolder; @@ -46,18 +48,13 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping; import org.jackhuang.hmcl.util.javafx.SafeStringConverter; import org.jackhuang.hmcl.util.platform.Architecture; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Optional; +import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; import static org.jackhuang.hmcl.util.Pair.pair; @@ -72,7 +69,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag private Profile profile; private WeakListenerHolder listenerHolder; private String versionId; - private boolean javaItemsLoaded; private final VBox rootPane; private final JFXTextField txtWidth; @@ -83,9 +79,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag private final JFXCheckBox chkAutoAllocate; private final JFXCheckBox chkFullscreen; private final ComponentSublist javaSublist; - private final MultiFileItem> javaItem; - private final MultiFileItem.Option> javaAutoDeterminedOption; - private final MultiFileItem.FileOption> javaCustomOption; + private final MultiFileItem> javaItem; + private final MultiFileItem.Option> javaAutoDeterminedOption; + private final MultiFileItem.StringOption> javaVersionOption; + private final MultiFileItem.FileOption> javaCustomOption; + private final ComponentSublist gameDirSublist; private final MultiFileItem gameDirItem; private final MultiFileItem.FileOption gameDirCustomOption; @@ -93,9 +91,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag private final OptionToggleButton showLogsPane; private final ImagePickerItem iconPickerItem; + private final ChangeListener> javaListChangeListener; private final InvalidationListener specificSettingsListener; - private final InvalidationListener javaListener = any -> initJavaSubtitle(); + private boolean updatingJavaSetting = false; + private boolean updatingSelectedJava = false; private final StringProperty selectedVersion = new SimpleStringProperty(); private final BooleanProperty navigateToSpecificSettings = new SimpleBooleanProperty(false); @@ -179,8 +179,36 @@ public VersionSettingsPage(boolean globalSetting) { javaSublist.setTitle(i18n("settings.game.java_directory")); javaSublist.setHasSubtitle(true); javaAutoDeterminedOption = new MultiFileItem.Option<>(i18n("settings.game.java_directory.auto"), pair(JavaVersionType.AUTO, null)); - javaCustomOption = new MultiFileItem.FileOption>(i18n("settings.custom"), pair(JavaVersionType.CUSTOM, null)) + javaVersionOption = new MultiFileItem.StringOption<>(i18n("settings.game.java_directory.version"), pair(JavaVersionType.VERSION, null)); + javaVersionOption.setValidators(new NumberValidator(true)); + FXUtils.setLimitWidth(javaVersionOption.getCustomField(), 40); + javaCustomOption = new MultiFileItem.FileOption>(i18n("settings.custom"), pair(JavaVersionType.CUSTOM, null)) .setChooserTitle(i18n("settings.game.java_directory.choose")); + if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) + javaCustomOption.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe")); + + javaListChangeListener = FXUtils.onWeakChangeAndOperate(JavaManager.getAllJavaProperty(), allJava -> { + List>> options = new ArrayList<>(); + options.add(javaAutoDeterminedOption); + options.add(javaVersionOption); + if (allJava != null) { + boolean isX86 = Architecture.SYSTEM_ARCH.isX86() && allJava.stream().allMatch(java -> java.getArchitecture().isX86()); + + for (JavaRuntime java : allJava) { + options.add(new MultiFileItem.Option<>( + i18n("settings.game.java_directory.template", + java.getVersion(), + isX86 ? i18n("settings.game.java_directory.bit", java.getBits().getBit()) + : java.getPlatform().getArchitecture().getDisplayName()), + pair(JavaVersionType.DETECTED, java)) + .setSubtitle(java.getBinary().toString())); + } + } + + options.add(javaCustomOption); + javaItem.loadChildren(options); + initializeSelectedJava(); + }); gameDirItem = new MultiFileItem<>(); gameDirSublist = new ComponentSublist(); @@ -430,30 +458,6 @@ public VersionSettingsPage(boolean globalSetting) { private void initialize() { memoryStatus.set(OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID)); - - Task.supplyAsync(JavaVersion::getJavas).thenAcceptAsync(Schedulers.javafx(), list -> { - boolean isX86 = Architecture.SYSTEM_ARCH.isX86() && list.stream().allMatch(java -> java.getArchitecture().isX86()); - - List>> options = list.stream() - .map(javaVersion -> new MultiFileItem.Option<>( - i18n("settings.game.java_directory.template", - javaVersion.getVersion(), - isX86 ? i18n("settings.game.java_directory.bit", javaVersion.getBits().getBit()) - : javaVersion.getPlatform().getArchitecture().getDisplayName()), - pair(JavaVersionType.DETECTED, javaVersion)) - .setSubtitle(javaVersion.getBinary().toString())) - .collect(Collectors.toList()); - options.add(0, javaAutoDeterminedOption); - options.add(javaCustomOption); - javaItem.loadChildren(options); - javaItemsLoaded = true; - initializeSelectedJava(); - }).start(); - - javaItem.setSelectedData(null); - if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) - javaCustomOption.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe")); - enableSpecificSettings.addListener((a, b, newValue) -> { if (versionId == null) return; @@ -472,7 +476,6 @@ private void initialize() { } @Override - @SuppressWarnings("unchecked") public void loadVersion(Profile profile, String versionId) { this.profile = profile; this.versionId = versionId; @@ -517,8 +520,10 @@ public void loadVersion(Profile profile, String versionId) { FXUtils.unbindEnum(cboProcessPriority); lastVersionSetting.usesGlobalProperty().removeListener(specificSettingsListener); + lastVersionSetting.javaVersionTypeProperty().removeListener(javaListener); lastVersionSetting.javaDirProperty().removeListener(javaListener); - lastVersionSetting.javaProperty().removeListener(javaListener); + lastVersionSetting.defaultJavaPathPropertyProperty().removeListener(javaListener); + lastVersionSetting.javaVersionProperty().removeListener(javaListener); gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty()); gameDirSublist.subtitleProperty().unbind(); @@ -531,6 +536,7 @@ public void loadVersion(Profile profile, String versionId) { // unbind data fields javaItem.setToggleSelectedListener(null); + javaVersionOption.valueProperty().unbind(); // bind new data fields FXUtils.bindInt(txtWidth, versionSetting.widthProperty()); @@ -551,18 +557,42 @@ public void loadVersion(Profile profile, String versionId) { enableSpecificSettings.set(!versionSetting.isUsesGlobal()); javaItem.setToggleSelectedListener(newValue -> { + if (javaItem.getSelectedData() == null || updatingSelectedJava) + return; + + updatingJavaSetting = true; + + if (javaVersionOption.isSelected()) { + javaVersionOption.valueProperty().bindBidirectional(versionSetting.javaVersionProperty()); + } else { + javaVersionOption.valueProperty().unbind(); + javaVersionOption.setValue(""); + } + if (javaCustomOption.isSelected()) { versionSetting.setUsesCustomJavaDir(); } else if (javaAutoDeterminedOption.isSelected()) { versionSetting.setJavaAutoSelected(); + } else if (javaVersionOption.isSelected()) { + if (versionSetting.getJavaVersionType() != JavaVersionType.VERSION) + versionSetting.setJavaVersion(""); + versionSetting.setJavaVersionType(JavaVersionType.VERSION); + versionSetting.setDefaultJavaPath(null); } else { - versionSetting.setJavaVersion(((Pair) newValue.getUserData()).getValue()); + @SuppressWarnings("unchecked") + JavaRuntime java = ((Pair) newValue.getUserData()).getValue(); + versionSetting.setJavaVersionType(JavaVersionType.DETECTED); + versionSetting.setJavaVersion(java.getVersion()); + versionSetting.setDefaultJavaPath(java.getBinary().toString()); } + + updatingJavaSetting = false; }); + versionSetting.javaVersionTypeProperty().addListener(javaListener); versionSetting.javaDirProperty().addListener(javaListener); versionSetting.defaultJavaPathPropertyProperty().addListener(javaListener); - versionSetting.javaProperty().addListener(javaListener); + versionSetting.javaVersionProperty().addListener(javaListener); gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty()); gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(), @@ -576,52 +606,101 @@ public void loadVersion(Profile profile, String versionId) { } private void initializeSelectedJava() { - if (lastVersionSetting == null - || !javaItemsLoaded /* JREs are still being loaded */) { + if (lastVersionSetting == null || updatingJavaSetting) return; - } - if (lastVersionSetting.isUsesCustomJavaDir()) { - javaCustomOption.setSelected(true); - } else if (lastVersionSetting.isJavaAutoSelected()) { - javaAutoDeterminedOption.setSelected(true); - } else { -// javaLoading.set(true); - lastVersionSetting.getJavaVersion(null, null) - .thenAcceptAsync(Schedulers.javafx(), javaVersion -> { - javaItem.setSelectedData(pair(JavaVersionType.DETECTED, javaVersion)); -// javaLoading.set(false); - }).start(); + updatingSelectedJava = true; + switch (lastVersionSetting.getJavaVersionType()) { + case CUSTOM: + javaCustomOption.setSelected(true); + break; + case VERSION: + javaVersionOption.setSelected(true); + javaVersionOption.setValue(lastVersionSetting.getJavaVersion()); + break; + case AUTO: + javaAutoDeterminedOption.setSelected(true); + break; + default: + Toggle toggle = null; + if (JavaManager.isInitialized()) { + try { + JavaRuntime java = lastVersionSetting.getJava(null, null); + if (java != null) { + for (Toggle t : javaItem.getGroup().getToggles()) { + if (t.getUserData() != null) { + @SuppressWarnings("unchecked") + Pair userData = (Pair) t.getUserData(); + if (userData.getValue() != null && java.getBinary().equals(userData.getValue().getBinary())) { + toggle = t; + break; + + } + } + } + } + } catch (InterruptedException ignored) { + } + } + + if (toggle != null) { + toggle.setSelected(true); + } else { + Toggle selectedToggle = javaItem.getGroup().getSelectedToggle(); + if (selectedToggle != null) { + selectedToggle.setSelected(false); + } + } + break; } + updatingSelectedJava = false; } private void initJavaSubtitle() { FXUtils.checkFxUserThread(); - initializeSelectedJava(); - VersionSetting versionSetting = lastVersionSetting; - if (versionSetting == null) + if (lastVersionSetting == null) return; - Profile profile = this.profile; + initializeSelectedJava(); + HMCLGameRepository repository = this.profile.getRepository(); String versionId = this.versionId; - boolean autoSelected = versionSetting.isJavaAutoSelected(); + JavaVersionType javaVersionType = lastVersionSetting.getJavaVersionType(); + boolean autoSelected = javaVersionType == JavaVersionType.AUTO || javaVersionType == JavaVersionType.VERSION; - if (autoSelected && versionId == null) { + if (versionId == null && autoSelected) { javaSublist.setSubtitle(i18n("settings.game.java_directory.auto")); return; } - Task.composeAsync(Schedulers.javafx(), () -> { + Pair selectedData = javaItem.getSelectedData(); + if (selectedData != null && selectedData.getValue() != null) { + javaSublist.setSubtitle(selectedData.getValue().getBinary().toString()); + return; + } + + if (JavaManager.isInitialized()) { + GameVersionNumber gameVersionNumber; + Version version; if (versionId == null) { - return versionSetting.getJavaVersion(GameVersionNumber.unknown(), null); + gameVersionNumber = GameVersionNumber.unknown(); + version = null; } else { - return versionSetting.getJavaVersion( - GameVersionNumber.asGameVersion(profile.getRepository().getGameVersion(versionId)), - profile.getRepository().getVersion(versionId)); + gameVersionNumber = GameVersionNumber.asGameVersion(repository.getGameVersion(versionId)); + version = repository.getVersion(versionId); } - }).thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion) - .map(JavaVersion::getBinary).map(Path::toString).orElseGet(() -> - autoSelected ? i18n("settings.game.java_directory.auto.not_found") : i18n("settings.game.java_directory.invalid")))) - .start(); + + try { + JavaRuntime java = lastVersionSetting.getJava(gameVersionNumber, version); + if (java != null) { + javaSublist.setSubtitle(java.getBinary().toString()); + } else { + javaSublist.setSubtitle(autoSelected ? i18n("settings.game.java_directory.auto.not_found") : i18n("settings.game.java_directory.invalid")); + } + return; + } catch (InterruptedException ignored) { + } + } + + javaSublist.setSubtitle(""); } private void editSpecificSettings() { @@ -664,10 +743,4 @@ private void loadIcon() { public ReadOnlyObjectProperty stateProperty() { return state.getReadOnlyProperty(); } - - private enum JavaVersionType { - DETECTED, - CUSTOM, - AUTO, - } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java index d0dccc4f7b..ddcd5342fa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java @@ -33,7 +33,7 @@ import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.JarUtils; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; import java.io.IOException; @@ -177,7 +177,7 @@ private static void requestUpdate(Path updateTo, Path self) throws IOException { private static void startJava(Path jar, String... appArgs) throws IOException { List commandline = new ArrayList<>(); - commandline.add(JavaVersion.fromCurrentEnvironment().getBinary().toString()); + commandline.add(JavaRuntime.getDefault().getBinary().toString()); for (Map.Entry entry : System.getProperties().entrySet()) { Object key = entry.getKey(); if (key instanceof String && ((String) key).startsWith("hmcl.")) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java index ea8914d8c8..79d21fbb4a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java @@ -22,7 +22,7 @@ import org.jackhuang.hmcl.setting.VersionSetting; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.platform.Architecture; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.Platform; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -62,7 +62,7 @@ private static Map getNatives(Platform platform) { }); } - public static Version patchNative(Version version, String gameVersion, JavaVersion javaVersion, VersionSetting settings) { + public static Version patchNative(Version version, String gameVersion, JavaRuntime javaVersion, VersionSetting settings) { if (settings.getNativesDirType() == NativesDirectoryType.CUSTOM) { if (gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") < 0) return version; @@ -154,7 +154,7 @@ public static Version patchNative(Version version, String gameVersion, JavaVersi return version.setLibraries(newLibraries); } - public static Library getMesaLoader(JavaVersion javaVersion, Renderer renderer) { + public static Library getMesaLoader(JavaRuntime javaVersion, Renderer renderer) { return getNatives(javaVersion.getPlatform()).get(renderer == Renderer.LLVMPIPE ? "software-renderer-loader" : "mesa-loader"); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java index d6a8aa77ed..cde4fcda0d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/SelfDependencyPatcher.java @@ -48,6 +48,7 @@ import org.jackhuang.hmcl.util.io.ChecksumMismatchException; import org.jackhuang.hmcl.util.io.IOUtils; import org.jackhuang.hmcl.util.io.JarUtils; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.Platform; import javax.swing.*; @@ -69,7 +70,6 @@ import static org.jackhuang.hmcl.Metadata.HMCL_DIRECTORY; import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.platform.JavaVersion.CURRENT_JAVA; // From: https://github.com/Col-E/Recaf/blob/7378b397cee664ae81b7963b0355ef8ff013c3a7/src/main/java/me/coley/recaf/util/self/SelfDependencyPatcher.java public final class SelfDependencyPatcher { @@ -188,7 +188,7 @@ public static void patch() throws PatchException, IncompatibleVersionException, // So the problem with Java 8 is that some distributions DO NOT BUNDLE JAVAFX // Why is this a problem? OpenJFX does not come in public bundles prior to Java 11 // So you're out of luck unless you change your JDK or update Java. - if (CURRENT_JAVA.getParsedVersion() < 11) { + if (JavaRuntime.CURRENT_VERSION < 11) { throw new IncompatibleVersionException(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java index 3da9e612cd..6f5309d3e1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/i18n/Locales.java @@ -20,8 +20,8 @@ import com.google.gson.annotations.JsonAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; +import org.jackhuang.hmcl.java.JavaInfo; import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.platform.JavaVersion; import java.io.IOException; import java.util.List; @@ -116,7 +116,7 @@ public ResourceBundle getResourceBundle() { if (resourceBundle == null) { if (this != DEFAULT && this.locale == DEFAULT.locale) { bundle = DEFAULT.getResourceBundle(); - } else if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9) { + } else if (JavaInfo.CURRENT_ENVIRONMENT.getParsedVersion() < 9) { bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, UTF8Control.INSTANCE); } else { // Java 9+ uses UTF-8 as the default encoding for resource bundles diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index a0b1cc636e..5515c3da13 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -334,6 +334,7 @@ download.provider.official=From Official Sources download.provider.balanced=From Fastest Available download.provider.mirror=From Mirror download.java=Downloading Java +download.java.override=This Java version already exists, do you want to uninstall and reinstall it? download.javafx=Downloading dependencies for the launcher... download.javafx.notes=We are currently downloading dependencies for HMCL from the Internet.\n\ \n\ @@ -646,6 +647,36 @@ install.new_game.malformed=Invalid Name install.select=Select an operation install.success=Installed successfully. +java.add=Add Java +java.add.failed=This Java is invalid or incompatible with the current platform. +java.disable=Disable Java +java.disable.confirm=Are you sure you want to disable this Java? +java.disabled.management=Disabled Java +java.disabled.management.remove=Remove this Java from the list +java.disabled.management.restore=Re-enable this Java +java.download=Download Java +java.download.load_list.failed=Failed to load version list +java.download.more=More Java distributions +java.download.prompt=Please choose the Java version you want to download: +java.download.distribution=Distribution +java.download.version=Version +java.download.packageType=Package Type +java.management=Java Management +java.info.architecture=Architecture +java.info.vendor=Vendor +java.info.version=Version +java.info.disco.distribution=Distribution +java.install=Install Java +java.install.archive=Source Path +java.install.failed.exists=This name is already owned +java.install.failed.invalid=This archive is not a valid Java installation package, so it cannot be installed. +java.install.failed.unsupported_platform=This Java is not compatible with the current platform, so it cannot be installed. +java.install.name=Name +java.install.warning.invalid_character=Illegal character in name +java.reveal=Reveal the Java directory +java.uninstall=Uninstall Java +java.uninstall.confirm=Are you sure you want to uninstall this Java? This action cannot be undone! + lang=English (US) lang.default=Use System Locales launch.advice=%s Do you still want to continue to launch? @@ -664,15 +695,16 @@ launch.advice.java8_1_13=Minecraft 1.13 and later can only be run on Java 8 or l launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 versions earlier than 1.8.0_51. Please install the latest version of Java 8. launch.advice.java9=You cannot launch Minecraft 1.12 or earlier with Java 9 or newer, please use Java 8 instead. launch.advice.modded_java=Some Mods may not be compatible with higher versions of Java. It is recommended to use Java %s to start Minecraft %s. -launch.advice.modlauncher8=The Forge version you are using is not compatible with the current Java version. Please try updating Forge, or launch the game with Java 8u312/11.0.13/17.0.1 or earlier. +launch.advice.modlauncher8=The Forge version you are using is not compatible with the current Java version, please try updating Forge. launch.advice.newer_java=You are using the old Java to start 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 MB 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=Minecraft %1$s requires Java %2$s or later, but we could not find one. Do you want to download one now? +launch.advice.require_newer_java_version=Current game version requires Java %s, but we could not find one. Do you want to download one now? 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 below only supports Java 8 for the Linux x86-64 platform, because later versions cannot load 32-bit native libraries like liblwjgl.so\n\ \n\ Please download it from java.com, or install OpenJDK 8. launch.advice.vanilla_x86.translation=Minecraft is not fully supported for your platform, so you may experience missing functionality, or even be unable to launch the game.\nYou can play through the Rosetta translation environment for a full gaming experience. +launch.advice.unknown=The game cannot be launched due to the following reasons: launch.failed=Failed to launch launch.failed.cannot_create_jvm=We are unable to create a Java virtual machine. It may be caused by incorrect Java VM arguments. You can try fixing it by removing all arguments you added under instance settings. launch.failed.creating_process=We are unable to create a new process, please check your Java path. @@ -686,6 +718,7 @@ launch.failed.execution_policy.hint=The current execution policy prevents the ex \n\ Click on 'OK' to allow the current user to execute PowerShell scripts, or click on 'Cancel' to keep it as it is. launch.failed.exited_abnormally=Game crashed. Please refer to the crash log for more details. +launch.failed.java_version_too_low=The Java version you specified is too low, please reset the Java version. launch.failed.no_accepted_java=Unable to find a compatible Java version, do you want to start the game with the default Java?\nClick on 'Yes' to start the game with the default Java.\nOr, you can go to the instance settings to select one yourself. launch.failed.sigkill=Game was forcibly terminated by the user or system. launch.state.dependencies=Resolving dependencies @@ -694,7 +727,7 @@ launch.state.java=Checking Java version launch.state.logging_in=Logging in launch.state.modpack=Downloading required files launch.state.waiting_launching=Waiting for the game to launch -launch.wrong_javadir=Invalid Java path, falling back to the default one. +launch.invalid_java=Invalid Java path, please reset the Java path. launcher=Launcher launcher.agreement=ToS and EULA @@ -1119,6 +1152,7 @@ settings.game.java_directory.auto.not_found=No suitable Java version was install settings.game.java_directory.bit=%s bit settings.game.java_directory.choose=Select Java path. settings.game.java_directory.invalid=Incorrect Java path. +settings.game.java_directory.version=Specify Java Version settings.game.java_directory.template=%s (%s) settings.game.management=Manage settings.game.working_directory=Working Directory diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 666b0bd011..e3c983843f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -611,7 +611,6 @@ launch.advice.java9=No puedes ejecutar Minecraft 1.12 o anterior con Java 9 o m launch.advice.modlauncher8=La versión de Forge que estás utilizando no es compatible con la versión actual de Java. Por favor, intenta actualizar Forge, o ejecutar el juego con Java 8u312/11.0.13/17.0.1 o anterior. launch.advice.newer_java=Se recomienda Java 8 para una experiencia de juego más fluida. Y para Minecraft 1.12 o superior, y la mayoría de los mods, es obligatorio. launch.advice.not_enough_space=Has asignado un tamaño de memoria mayor que los %d MB 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=Minecraft %1$s requiere Java %2$s o posterior, pero no hemos podido encontrar una versión. ¿Desea descargar una ahora? 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\ \n\ @@ -640,7 +639,6 @@ launch.state.java=Comprobando la versión de Java launch.state.logging_in=Iniciando sesión launch.state.modpack=Descargando dependencias launch.state.waiting_launching=Esperando la ejecución del juego -launch.wrong_javadir=Ruta de Java no válida, volviendo a la predeterminada. launcher=Launcher launcher.agreement=Términos de servicio y EULA diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index e93f3186b7..5aef7a0195 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -463,7 +463,6 @@ launch.advice.java8_51_1_13=Minecraft 1.13は、1.8.0_51より前のJava8でク launch.advice.java9=Java9以降のバージョンのJavaでMinecraft1.12以前を起動することはできません。 ゲームを高速化するには、launch.advice.newer_java=Java8をお勧めします。多くのMinecraft1.12以降、およびほとんどのModには、Java8が必要です。 launch.advice.not_enough_space=割り当てたメモリが多すぎます。物理メモリサイズが%dMBであるため、ゲームがクラッシュする可能性があります。 -launch.advice.require_newer_java_version=Minecraft %1$s にはJava %2$s 以降が必要です。今すぐダウンロードしますか? launch.advice.too_large_memory_for_32bit=32ビットJavaランタイム環境が原因で、割り当てたメモリが多すぎるため、ゲームがクラッシュする可能性があります。32ビットシステムの最大メモリ容量は1024MBです。 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を実行するか、プラットフォームの対応するネイティブライブラリをダウンロードして指定してくださいその配置パス。 @@ -485,7 +484,6 @@ launch.state.java=Javaバージョンの検出 launch.state.logging_in=ログイン launch.state.modpack=modpackを読み込んでいます launch.state.waiting_launching=ゲームの起動 -launch.wrong_javadir=無効なJavaディレクトリ、デフォルトのJavaパスが適用されます。 launcher=ランチャー launcher.agreement=EULA diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index c50bb7178a..1454eeb20b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -487,7 +487,6 @@ launch.advice.java8_51_1_13=Minecraft 1.13 может аварийно заве launch.advice.java9=Вы не сможете запустить Minecraft 1.12 и ниже с Java 9 и более новыми версиями Java. launch.advice.newer_java=Рекомендуется использовать Java 8, чтобы игра работала быстрее. Для многих версий Minecraft 1.12 и большинства модов требуется Java 8. launch.advice.not_enough_space=Вы выделили слишком много памяти, поскольку размер физической памяти составляет %d МБ, ваша игра может рухнуть. -launch.advice.require_newer_java_version=Minecraft %1$s требует Java %2$s или более новую версию, скачать её сейчас? launch.advice.too_large_memory_for_32bit=Вы выделили слишком много памяти, из-за 32-разрядной Java Runtime Environment ваша игра может рухнуть. Максимальный объем памяти для 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 или начните с Java 8u312/11.0.13/17.0.1 или более ранней версии. @@ -512,7 +511,6 @@ launch.state.java=Проверка версии Java launch.state.logging_in=Вход в систему launch.state.modpack=Скачивание зависимостей launch.state.waiting_launching=Ожидание запуска игры -launch.wrong_javadir=Неверный путь Java, возврат к пути по умолчанию. launcher=Лаунчер launcher.agreement=Пользовательское соглашение diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 27b7b87696..3e99c32a43 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -343,6 +343,7 @@ download.provider.official=儘量使用官方源(最新,但可能加載慢 download.provider.balanced=選擇加載速度快的下載源(平衡,但可能不是最新) download.provider.mirror=儘量使用鏡像源(加載快,但可能不是最新) download.java=下載 Java +download.java.override=此 Java 版本已經存在,是否移除並重新安裝? download.javafx=正在下載必要的運行時組件 download.javafx.notes=正在通過網絡下載 HMCL 必要的運行時組件。\n點擊“切換下載源”按鈕查看詳情以及選擇下載源,點擊“取消”按鈕停止並退出。\n注意:如果下載速度過慢,請嘗試切换下載源。 download.javafx.component=正在下載模塊 %s @@ -424,7 +425,7 @@ game.crash.reason.mod_profile_causes_game_crash=當前遊戲因為 Mod 配置文 game.crash.reason.fabric_reports_an_error_and_gives_a_solution=Forge 可能已經提供了錯誤信息。\n你可以查看日誌,並根據錯誤報告中的日誌信息進行對應處。\n如果沒有看到報錯信息,可以查看錯誤報告了解錯誤具體是如何發生的。 game.crash.reason.java_version_is_too_high=當前遊戲因為使用的 Java 版本過高而崩潰了,無法繼續運行。\n請在 全局遊戲設置 或 遊戲特定設置 的 Java 路徑選項卡中改用較低版本的 Java,然後再啟動遊戲。\n如果沒有,可以從 java.com(Java8)BellSoft Liberica Full JRE(Java17) 等平台下載、安裝一個(安裝完後需重啟啟動器)。 game.crash.reason.mod_name=當前遊戲因為 Mod 檔案名稱問題,無法繼續運行。\nMod 檔案名稱應只使用英文全半型的大小寫字母(Aa~Zz)、數位(0~9)、橫線(-)、底線(_)和點(.)。\n請到Mod資料夾中將所有不合規的Mod檔案名稱添加一個上述的合規的字元。 -game.crash.reason.incomplete_forge_installation=當前遊戲因為 Forge / NeoForge 安裝不完整,無法繼續運行。\n請在 版本設置 - 自動安裝 中卸載 Forge / NeoForge 並重新安裝。 +game.crash.reason.incomplete_forge_installation=當前遊戲因為 Forge / NeoForge 安裝不完整,無法繼續運行。\n請在 版本設置 - 自動安裝 中移除 Forge / NeoForge 並重新安裝。 game.crash.reason.file_already_exists=當前遊戲因為文件 %1$s 已經存在,無法繼續運行。\n如果你認為這個文件可以刪除,你可以在備份這個文件後嘗試刪除它,並重新啟動遊戲。 game.crash.reason.file_changed=當前遊戲因為檔案校驗失敗,無法繼續運行。\n如果你手動修改了 Minecraft.jar 檔案,你需要回退修改,或者重新下載遊戲。 game.crash.reason.gl_operation_failure=當前遊戲因為你使用的某些 Mod、光影包、材質包,無法繼續運行。\n請先嘗試禁用你所使用的Mod/光影包/材質包再試。 @@ -451,14 +452,14 @@ game.crash.reason.mod_resolution_missing=當前遊戲因為缺少 Mod 前置, game.crash.reason.mod_resolution_missing_minecraft=當前遊戲因為 Mod 和 Minecraft 遊戲版本不匹配,無法繼續運行。\n%1$s 需要 Minecraft %2$s 才能運行。\n如果你要繼續使用你已經安裝的 Mod,你可以選擇安裝對應的 Minecraft 版本;如果你要繼續使用當前 Minecraft 版本,你需要安裝對應版本的 Mod。 game.crash.reason.mod_resolution_mod_version=%1$s (版本號 %2$s) game.crash.reason.mod_resolution_mod_version.any=%1$s (任意版本) -game.crash.reason.forge_repeat_installation=當前遊戲因為 Forge 重複安裝,無法繼續運行。此為已知問題\n建議將日誌上傳反饋至 GitHub ,以便我們找到更多線索並修復此問題。\n目前你可以到 自動安裝 裡頭卸載 Forge 並重新安裝。 -game.crash.reason.optifine_repeat_installation=當前遊戲因為重複安裝 OptiFine,無法繼續運行。 \n請刪除 Mod 文件夾下的 OptiFine 或前往 遊戲管理-自動安裝 卸載自動安裝的 OptiFine。 +game.crash.reason.forge_repeat_installation=當前遊戲因為 Forge 重複安裝,無法繼續運行。此為已知問題\n建議將日誌上傳反饋至 GitHub ,以便我們找到更多線索並修復此問題。\n目前你可以到 自動安裝 裡頭移除 Forge 並重新安裝。 +game.crash.reason.optifine_repeat_installation=當前遊戲因為重複安裝 OptiFine,無法繼續運行。 \n請刪除 Mod 文件夾下的 OptiFine 或前往 遊戲管理-自動安裝 移除自動安裝的 OptiFine。 game.crash.reason.optifine_is_not_compatible_with_forge=當前遊戲因為OptiFine與當前版本的Forge不相容,導致了遊戲崩潰。\n請前往 OptiFine 官網查看 OptiFine 所相容的 Forge 版本,並嚴格按照對應版本重新安裝遊戲或在版本設定-自動安裝中更換版本。\n經測試,Forge版本過高或過低都可能導致崩潰。 game.crash.reason.night_config_fixes=當前遊戲因為 Night Config 庫的一些問題,無法繼續運行。\n你可以嘗試安裝 Night Config Fixes 模組,這或許能幫助你解決這個問題。\n了解更多,可訪問該模組的 GitHub 倉庫。 game.crash.reason.mod_files_are_decompressed=當前遊戲因為 Mod 檔案被解壓了,無法繼續運行。\n請直接把整個 Mod 檔案放進 Mod 資料夾中即可。\n若解壓就會導致遊戲出錯,請删除Mod資料夾中已被解壓的Mod,然後再啟動遊戲。 game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=當前遊戲因為您所安裝的 Mod 過多,超出了遊戲的ID限制,無法繼續運行。\n請嘗試安裝JEID等修復Mod,或删除部分大型Mod。 game.crash.reason.optifine_causes_the_world_to_fail_to_load=當前遊戲因為 Mod 檔案被解壓了,無法繼續運行。\n請直接把整個Mod檔案放進Mod資料夾中即可!\n若解壓就會導致遊戲出錯,請删除Mod資料夾中已被解壓的Mod,然後再啟動遊戲。 -game.crash.reason.modlauncher_8=當前遊戲因為您所使用的 Forge 版本與當前使用的 Java 衝突崩潰,請嘗試更新 Forge。 +game.crash.reason.modlauncher_8=當前遊戲因為你所使用的 Forge 版本與當前使用的 Java 衝突崩潰,請嘗試更新 Forge。 game.crash.reason.cannot_find_launch_target_fmlclient=當前遊戲因為 Forge 安裝不完整,無法繼續運行。 \n你可嘗試前往 遊戲管理 - 自動安裝 中選擇 Forge 並重新安裝。 game.crash.reason.shaders_mod=當前遊戲因為同時安裝了 OptiFine 和 Shaders Mod,無法繼續運行。 \n因為 OptiFine 已集成 Shaders Mod 的功能,只需刪除 Shaders Mod 即可。 game.crash.reason.rtss_forest_sodium=當前遊戲因為 RivaTuner Statistics Server (RTSS) 與 Sodium 不相容,導致遊戲崩潰。\n點擊 此處 查看詳情。 @@ -530,6 +531,36 @@ install.new_game.malformed=名稱無效 install.select=請選擇安裝方式 install.success=安裝成功 +java.add=添加 Java +java.add.failed=Java 無效或與目前平臺不相容 +java.disable=禁用此 Java +java.disable.confirm=你確定要禁用此 Java 嗎? +java.disabled.management=管理已禁用的 Java +java.disabled.management.remove=從清單中移除此 Java +java.disabled.management.restore=重新啟用此 Java +java.download=下載 Java +java.download.load_list.failed=載入版本清單失敗 +java.download.more=更多發行版 +java.download.prompt=請選擇你要下載的 Java 版本: +java.download.distribution=發行版 +java.download.version=版本 +java.download.packageType=包類型 +java.management=Java 管理 +java.info.architecture=架構 +java.info.vendor=供應商 +java.info.version=版本 +java.info.disco.distribution=發行版本 +java.install=安裝 Java +java.install.archive=源路徑 +java.install.failed.exists=該名稱已被使用 +java.install.failed.invalid=該檔案不是合法的 Java 安裝包,無法繼續安裝。 +java.install.failed.unsupported_platform=此 Java 與當前平臺不相容,無法安裝。 +java.install.name=名稱 +java.install.warning.invalid_character=名稱中包含非法字元 +java.reveal=瀏覽 Java 目錄 +java.uninstall=移除此 Java +java.uninstall.confirm=你確定要移除此 Java 嗎?此操作無法復原! + lang=正體中文 lang.default=使用系統語言 @@ -537,23 +568,24 @@ launch.advice=%s是否繼續啟動? launch.advice.multi=檢測到以下問題:\n\n%s\n\n這些問題可能導致遊戲無法正常啟動或影響遊戲體驗,是否繼續啟動? 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.corrected=我們已經修正了問題。如果確實希望使用你自訂的 Java 虛擬機,你可以在遊戲設定中關閉 Java 虛擬機相容性檢查。 +launch.advice.uncorrected=如果你確實希望使用你自訂的 Java 虛擬機,你可以在遊戲設定中關閉 Java 虛擬機相容性檢查。 launch.advice.different_platform=你正在使用 32 位元 Java 啟動遊戲,建議更換至 64 位元 Java。 launch.advice.forge2760_liteloader=Forge 2760 與 LiteLoader 不相容,請更新 Forge 到 2773 或更新的版本。 launch.advice.forge28_2_2_optifine=Forge 28.2.2 或更高版本與 OptiFine 不相容,請将 Forge 降級至 28.2.1 或更低版本。 launch.advice.forge37_0_60=Forge 低於 37.0.60 的版本不相容 Java 17。請更新 Forge 到 37.0.60 或更高版本,或者使用 Java 16 啟動遊戲。 launch.advice.java8_1_13=Minecraft 1.13 只支援 Java 8 或更高版本,請使用 Java 8 或最新版本。 -launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議您到 https://java.com 安裝最新版的 Java 8。 +launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議你到 https://java.com 安裝最新版的 Java 8。 launch.advice.java9=低於 (包含) 1.13 的有安裝 Mod 的 Minecraft 版本不支援 Java 9 或更高版本,請使用 Java 8。 launch.advice.modded_java=部分 Mod 可能與高版本 Java 不相容,建議使用 Java %s 啟動 Minecraft %s。 -launch.advice.modlauncher8=您所使用的 Forge 版本與當前使用的 Java 不相容。請嘗試更新 Forge,或使用 Java 8u312/11.0.13/17.0.1 及更早版本啟動。是否繼續啟動? +launch.advice.modlauncher8=你所使用的 Forge 版本與當前使用的 Java 不相容,請更新 Forge。 launch.advice.newer_java=偵測到你正在使用舊版本 Java 啟動遊戲,這可能導致部分 Mod 引發遊戲崩潰,建議更新至 Java 8 後再次啟動。 -launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。是否繼續啟動? -launch.advice.require_newer_java_version=Minecraft %1$s 僅能運行在 Java %2$s 或更高版本上,但 HMCL 未能找到該 Java 版本,你可以點擊“是”,HMCL 會自動下載他,是否下載? -launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。 +launch.advice.not_enough_space=你設定的記憶體大小過大,由於超過了系統記憶體大小 %dMB,所以可能影響遊戲體驗或無法啟動遊戲。 +launch.advice.require_newer_java_version=當前遊戲版本需要 Java %s,但 HMCL 未能找到該 Java 版本,你可以點擊“是”,HMCL 會自動下載他,是否下載? +launch.advice.too_large_memory_for_32bit=你設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。 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是否繼續啟動? +launch.advice.vanilla_x86.translation=Minecraft 尚未為你的平臺提供完善支持,所以可能影響遊戲體驗或無法啟動遊戲。\n你可以在 這裡 下載 X86-64 架構的 Java 以獲得更完整的體驗。\n是否繼續啟動? +launch.advice.unknown=由於以下原因,無法繼續啟動遊戲: launch.failed=啟動失敗 launch.failed.cannot_create_jvm=偵測到無法建立 Java 虛擬機,可能是 Java 參數有問題。可以在設定中開啟無參數模式啟動。 launch.failed.creating_process=啟動失敗,在建立新處理程式時發生錯誤。可能是 Java 路徑錯誤。 @@ -563,8 +595,9 @@ launch.failed.download_library=無法下載遊戲相依元件 %s。 launch.failed.executable_permission=無法為啟動檔案新增執行權限。 launch.failed.execution_policy=設定執行策略 launch.failed.execution_policy.failed_to_set=設定執行策略失敗 -launch.failed.execution_policy.hint=當前執行策略封锁您執行 PowerShell 腳本。\n點擊“確定”允許當前用戶執行本地 PowerShell 腳本,或點擊“取消”保持現狀。 +launch.failed.execution_policy.hint=當前執行策略封锁你執行 PowerShell 腳本。\n點擊“確定”允許當前用戶執行本地 PowerShell 腳本,或點擊“取消”保持現狀。 launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。 +launch.failed.java_version_too_low=你所指定的 Java 版本過低,請重新設定 Java 版本。 launch.failed.no_accepted_java=找不到適合當前遊戲使用的 Java,是否使用默認 Java 啟動遊戲?點擊“是”使用默認 Java 繼續啟動遊戲,\n或者請到遊戲設定中選擇一個合適的Java虛擬機器版本。 launch.failed.sigkill=遊戲被用戶或系統強制終止。 launch.state.dependencies=處理遊戲相依元件 @@ -573,7 +606,7 @@ launch.state.java=檢測 Java 版本 launch.state.logging_in=登入 launch.state.modpack=下載必要檔案 launch.state.waiting_launching=等待遊戲啟動 -launch.wrong_javadir=Java 路徑錯誤,將自動重設為預設 Java 路徑。 +launch.invalid_java=當前設定的 Java 路徑無效,請重新設定 Java 路徑。 launcher=啟動器 launcher.agreement=用戶協議與免責聲明 @@ -594,9 +627,9 @@ launcher.cache_directory.disabled=停用 launcher.cache_directory.invalid=無法建立自訂的快取目錄,還原至預設設定 launcher.contact=聯絡我們 launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤,請複製下列內容並透過 MCBBS、貼吧、GitHub 或 Minecraft Forum 回報 bug。 -launcher.crash.java_internal_error=HHello Minecraft! Launcher 由於當前 Java 損壞而無法繼續運行,請卸載當前 Java,點擊 此處 安裝合適的 Java 版本。 +launcher.crash.java_internal_error=HHello Minecraft! Launcher 由於當前 Java 損壞而無法繼續運行,請移除當前 Java,點擊 此處 安裝合適的 Java 版本。 launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤,已偵測到您的啟動器不是最新版本,請更新後重試! -launcher.update_java=請更新您的 Java +launcher.update_java=請更新你的 Java login.empty_username=你還未設定使用者名稱! login.enter_password=請輸入您的密碼 @@ -980,6 +1013,7 @@ settings.game.java_directory.auto.not_found=沒有合適的 Java settings.game.java_directory.bit=%s 位 settings.game.java_directory.choose=選擇 Java 路徑 settings.game.java_directory.invalid=Java 路徑不正確 +settings.game.java_directory.version=指定 Java 版本 settings.game.java_directory.template=%s(%s) settings.game.management=管理 settings.game.working_directory=執行路徑(版本隔離,修改後請自行移動相關遊戲檔案,如存檔模組設定等) 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 df4623147b..27e2d0c2f1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -344,6 +344,7 @@ download.provider.official=尽量使用官方源(最新,但可能加载慢 download.provider.balanced=选择加载速度快的下载源(平衡,但可能不是最新) download.provider.mirror=尽量使用镜像源(加载快,但可能不是最新) download.java=下载 Java +download.java.override=此 Java 版本已经存在,是否卸载并重新安装? download.javafx=正在下载必要的运行时组件…… download.javafx.notes=正在通过网络下载 HMCL 必要的运行时组件。\n点击“切换下载源”按钮查看详情以及选择下载源,点击“取消”按钮停止并退出。\n注意:若下载速度过慢,请尝试切换下载源 download.javafx.component=正在下载模块 %s @@ -529,6 +530,36 @@ install.new_game.malformed=名字不合法 install.select=请选择安装方式 install.success=安装成功 +java.add=添加 Java +java.add.failed=Java 无效或与当前平台不兼容。 +java.disable=禁用此 Java +java.disable.confirm=您确定要禁用此 Java 吗? +java.disabled.management=管理已禁用的 Java +java.disabled.management.remove=从列表中移除此 Java +java.disabled.management.restore=重新启用此 Java +java.download=下载 Java +java.download.load_list.failed=加载版本列表失败 +java.download.more=更多发行版 +java.download.prompt=请选择你要下载的 Java 版本: +java.download.distribution=发行版 +java.download.version=版本 +java.download.packageType=包类型 +java.management=Java 管理 +java.info.architecture=架构 +java.info.vendor=供应商 +java.info.version=版本 +java.info.disco.distribution=发行版 +java.install=安装 Java +java.install.archive=源路径 +java.install.failed.exists=该名称已被使用 +java.install.failed.invalid=该压缩包不是合法的 Java 安装包,无法继续安装。 +java.install.failed.unsupported_platform=此 Java 与当前平台不兼容,无法安装。 +java.install.name=名称 +java.install.warning.invalid_character=名称中包含非法字符 +java.reveal=浏览 Java 目录 +java.uninstall=卸载此 Java +java.uninstall.confirm=您确定要卸载此 Java 吗?此操作无法撤销! + lang=简体中文 lang.default=跟随系统语言 @@ -546,13 +577,14 @@ launch.advice.java8_1_13=Minecraft 1.13 及以上版本只能运行在 Java 8 launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃,建议更新 Java 至 1.8.0_51 或更高版本后再次启动。 launch.advice.java9=低于 1.13 的有安装模组的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。 launch.advice.modded_java=部分模组可能与高版本 Java 不兼容,建议使用 Java %s 启动 Minecraft %s。 -launch.advice.modlauncher8=您所使用的 Forge 版本与当前使用的 Java 不兼容。请尝试更新 Forge,或使用 Java 8u312/11.0.13/17.0.1 及更早版本启动。 +launch.advice.modlauncher8=您所使用的 Forge 版本与当前使用的 Java 不兼容,请更新 Forge。 launch.advice.newer_java=检测到你正在使用旧版本 Java 启动游戏,这可能导致部分模组引发游戏崩溃,建议更新至 Java 8 后再次启动。 launch.advice.not_enough_space=你设置的内存大小过大,超过了系统内存容量 %dMB,可能导致游戏无法启动。 -launch.advice.require_newer_java_version=Minecraft %1$s 仅能运行在 Java %2$s 或更高版本上,但 HMCL 未能找到该 Java 版本,你可以点击“是”,HMCL会自动下载他,是否下载?\n如遇到问题,你可以点击右上角帮助按钮进行求助。 +launch.advice.require_newer_java_version=当前游戏版本需要 Java %s,但 HMCL 未能找到该 Java 版本,你可以点击“是”,HMCL 会自动下载他,是否下载?\n如遇到问题,你可以点击右上角帮助按钮进行求助。 launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小。\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 以获得更完整的体验。 +launch.advice.unknown=由于以下原因,无法继续启动游戏: launch.failed=启动失败 launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。 launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。 @@ -564,6 +596,7 @@ launch.failed.execution_policy=设置执行策略 launch.failed.execution_policy.failed_to_set=设置执行策略失败 launch.failed.execution_policy.hint=当前执行策略阻止您执行 PowerShell 脚本。\n点击“确定”允许当前用户执行本地 PowerShell 脚本,或点击“取消”保持现状。 launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。 +launch.failed.java_version_too_low=你所指定的 Java 版本过低,请重新设置 Java 版本。 launch.failed.no_accepted_java=找不到适合当前游戏使用的 Java,是否使用默认 Java 启动游戏?点击“是”使用默认 Java 继续启动游戏,\n或者请到全局(特定)游戏设置中选择一个合适的 Java 虚拟机版本。\n你可以点击右上角帮助按钮进行求助。 launch.failed.sigkill=游戏被用户或系统强制终止。 launch.state.dependencies=处理游戏依赖 @@ -572,7 +605,7 @@ launch.state.java=检测 Java 版本 launch.state.logging_in=登录 launch.state.modpack=下载必要文件 launch.state.waiting_launching=等待游戏启动 -launch.wrong_javadir=错误的 Java 路径,将自动重置为默认 Java 路径。 +launch.invalid_java=当前设置的 Java 路径无效,请重新设置 Java 路径。 launcher=启动器 launcher.agreement=用户协议与免责声明 @@ -979,6 +1012,7 @@ settings.game.java_directory.auto.not_found=没有合适的 Java settings.game.java_directory.bit=%s 位 settings.game.java_directory.choose=选择 Java 路径 settings.game.java_directory.invalid=Java 路径不正确 +settings.game.java_directory.version=指定 Java 版本 settings.game.java_directory.template=%s(%s) settings.game.management=管理 settings.game.working_directory=版本隔离(建议使用模组时开启“各版本隔离”,改后需移动存档模组等相关游戏文件) diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java index 8734e8367b..36c691e405 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java @@ -20,10 +20,11 @@ import org.jackhuang.hmcl.JavaFXLauncher; import org.jackhuang.hmcl.game.ClassicVersion; import org.jackhuang.hmcl.game.LaunchOptions; +import org.jackhuang.hmcl.java.JavaInfo; import org.jackhuang.hmcl.game.Log; import org.jackhuang.hmcl.launch.ProcessListener; import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.ManagedProcess; import org.jackhuang.hmcl.util.platform.Platform; import org.junit.jupiter.api.Disabled; @@ -51,7 +52,7 @@ public void test() throws Exception { GameCrashWindow window = new GameCrashWindow(process, ProcessListener.ExitType.APPLICATION_ERROR, null, new ClassicVersion(), new LaunchOptions.Builder() - .setJava(new JavaVersion(Paths.get("."), "16", Platform.SYSTEM_PLATFORM)) + .setJava(new JavaRuntime(Paths.get("."), new JavaInfo(Platform.SYSTEM_PLATFORM, "16", null), false, false)) .setGameDir(new File(".")) .create(), Arrays.stream(logs.split("\\n")) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java index 4c63f7e9c2..b2b626e0fa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java @@ -40,7 +40,7 @@ import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.CommandBuilder; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.SystemUtils; import org.jetbrains.annotations.NotNull; @@ -129,7 +129,7 @@ public void execute() throws Exception { throw new Exception("Game processor jar does not have main class " + jar); List command = new ArrayList<>(); - command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString()); + command.add(JavaRuntime.getDefault().getBinary().toString()); command.add("-cp"); List classpath = new ArrayList<>(processor.getClasspath().size() + 1); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaDistribution.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaDistribution.java new file mode 100644 index 0000000000..f4032bebb7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaDistribution.java @@ -0,0 +1,36 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.download.java; + +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.platform.Platform; + +import java.util.Set; +import java.util.TreeMap; + +/** + * @author Glavo + */ +public interface JavaDistribution { + String getDisplayName(); + + Set getSupportedPackageTypes(); + + Task> getFetchJavaVersionsTask(DownloadProvider provider, Platform platform, JavaPackageType packageType); +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/JavaVersionConstraintTest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaPackageType.java similarity index 51% rename from HMCLCore/src/test/java/org/jackhuang/hmcl/game/JavaVersionConstraintTest.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaPackageType.java index ddc1ddfe23..42cc195d8f 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/JavaVersionConstraintTest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaPackageType.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui and contributors + * Copyright (C) 2024 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 @@ -15,23 +15,30 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.game; +package org.jackhuang.hmcl.download.java; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; -import org.jackhuang.hmcl.util.versioning.VersionNumber; -import org.junit.jupiter.api.Test; +/** + * @author Glavo + */ +public enum JavaPackageType { + JDK(true, false), + JRE(false, false), + JDKFX(true, true), + JREFX(false, true); -import static org.junit.jupiter.api.Assertions.*; + private final boolean jdk; + private final boolean javafx; -public class JavaVersionConstraintTest { + JavaPackageType(boolean jdk, boolean javafx) { + this.jdk = jdk; + this.javafx = javafx; + } - @Test - public void vanillaJava16() { - JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange( - GameVersionNumber.asGameVersion("1.17"), - null - ); + public boolean isJDK() { + return jdk; + } - assertEquals(VersionNumber.atLeast("16"), range.getMandatory()); + public boolean isJavaFXBundled() { + return javafx; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaRemoteVersion.java new file mode 100644 index 0000000000..fec207b592 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaRemoteVersion.java @@ -0,0 +1,29 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.download.java; + +/** + * @author Glavo + */ +public interface JavaRemoteVersion { + int getJdkVersion(); + + String getJavaVersion(); + + String getDistributionVersion(); +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaRepository.java deleted file mode 100644 index 89e87fc49b..0000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaRepository.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.jackhuang.hmcl.download.java; - -import org.jackhuang.hmcl.download.DownloadProvider; -import org.jackhuang.hmcl.game.GameJavaVersion; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.util.CacheRepository; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.platform.Architecture; -import org.jackhuang.hmcl.util.platform.JavaVersion; -import org.jackhuang.hmcl.util.platform.OperatingSystem; -import org.jackhuang.hmcl.util.platform.Platform; - -import java.io.BufferedReader; -import java.io.IOException; -import java.nio.file.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.stream.Stream; - -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -public final class JavaRepository { - private JavaRepository() { - } - - public static Task downloadJava(GameJavaVersion javaVersion, DownloadProvider downloadProvider) { - return new JavaDownloadTask(javaVersion, getJavaStoragePath(), downloadProvider) - .thenSupplyAsync(() -> { - String platform = getSystemJavaPlatform().orElseThrow(JavaDownloadTask.UnsupportedPlatformException::new); - return addJava(getJavaHome(javaVersion, platform)); - }); - } - - public static JavaVersion addJava(Path javaHome) throws InterruptedException, IOException { - if (Files.isDirectory(javaHome)) { - Path executable = JavaVersion.getExecutable(javaHome); - if (Files.isRegularFile(executable)) { - JavaVersion javaVersion = JavaVersion.fromExecutable(executable); - JavaVersion.getJavas().add(javaVersion); - return javaVersion; - } - } - - throw new IOException("Incorrect java home " + javaHome); - } - - public static Stream> findMinecraftRuntimeDirs() { - switch (OperatingSystem.CURRENT_OS) { - case WINDOWS: - return Stream.of( - FileUtils.tryGetPath(System.getenv("localappdata"), - "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime"), - FileUtils.tryGetPath( - Optional.ofNullable(System.getenv("ProgramFiles(x86)")).orElse("C:\\Program Files (x86)"), - "Minecraft Launcher\\runtime")); - case LINUX: - case FREEBSD: - return Stream.of(FileUtils.tryGetPath(System.getProperty("user.home"), ".minecraft/runtime")); - case OSX: - return Stream.of(FileUtils.tryGetPath(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime")); - default: - return Stream.empty(); - } - } - - public static Stream findJavaHomeInMinecraftRuntimeDir(Path runtimeDir) { - if (!Files.isDirectory(runtimeDir)) - return Stream.empty(); - // Examples: - // $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home - // $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta - List javaHomes = new ArrayList<>(); - Consumer action = platform -> { - try (DirectoryStream dir = Files.newDirectoryStream(runtimeDir)) { - // component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future. - for (Path component : dir) { - findJavaHomeInComponentDir(platform, component).ifPresent(javaHomes::add); - } - } catch (IOException e) { - LOG.warning("Failed to list java-runtime directory " + runtimeDir, e); - } - }; - getSystemJavaPlatform().ifPresent(action); - - // Workaround, which will be removed in the future - if (Platform.SYSTEM_PLATFORM == Platform.OSX_ARM64) - action.accept("mac-os-arm64"); - - return javaHomes.stream(); - } - - private static Optional findJavaHomeInComponentDir(String platform, Path component) { - Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1"); - if (!Files.isRegularFile(sha1File)) - return Optional.empty(); - Path dir = component.resolve(platform).resolve(component.getFileName()); - - try (BufferedReader reader = Files.newBufferedReader(sha1File)) { - String line; - while ((line = reader.readLine()) != null) { - if (line.isEmpty()) continue; - - int idx = line.indexOf(" /#//"); - if (idx <= 0) - throw new IOException("Illegal line: " + line); - - Path file = dir.resolve(line.substring(0, idx)); - - // Should we check the sha1 of files? This will take a lot of time. - if (Files.notExists(file)) - throw new NoSuchFileException(file.toAbsolutePath().toString()); - } - - if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) { - Path macPath = dir.resolve("jre.bundle/Contents/Home"); - if (Files.exists(macPath)) - return Optional.of(macPath); - else - LOG.warning("The Java is not in 'jre.bundle/Contents/Home'"); - } - - return Optional.of(dir); - } catch (IOException e) { - LOG.warning("Failed to verify Java in " + component, e); - return Optional.empty(); - } - } - - public static Optional getSystemJavaPlatform() { - if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) { - if (Architecture.SYSTEM_ARCH == Architecture.X86) { - return Optional.of("linux-i386"); - } else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) { - return Optional.of("linux"); - } - } else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) { - if (Architecture.SYSTEM_ARCH == Architecture.X86_64 || Architecture.SYSTEM_ARCH == Architecture.ARM64) { - return Optional.of("mac-os"); - } - } else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { - if (Architecture.SYSTEM_ARCH == Architecture.X86) { - return Optional.of("windows-x86"); - } else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) { - return Optional.of("windows-x64"); - } else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) { - if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) { - return Optional.of("windows-x64"); - } else { - return Optional.of("windows-x86"); - } - } - } - return Optional.empty(); - } - - public static Path getJavaStoragePath() { - return CacheRepository.getInstance().getCacheDirectory().resolve("java"); - } - - public static Path getJavaHome(GameJavaVersion javaVersion, String platform) { - Path javaHome = getJavaStoragePath().resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent()); - if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) - javaHome = javaHome.resolve("jre.bundle/Contents/Home"); - return javaHome; - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoFetchJavaListTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoFetchJavaListTask.java new file mode 100644 index 0000000000..036822ce57 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoFetchJavaListTask.java @@ -0,0 +1,95 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.download.java.disco; + +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.download.java.JavaPackageType; +import org.jackhuang.hmcl.task.GetTask; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.io.NetworkUtils; +import org.jackhuang.hmcl.util.platform.Architecture; +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.platform.Platform; +import org.jackhuang.hmcl.util.versioning.VersionNumber; + +import java.util.*; + +/** + * @author Glavo + */ +public final class DiscoFetchJavaListTask extends Task> { + + public static final String API_ROOT = System.getProperty("hmcl.discoapi.override", "https://api.foojay.io/disco/v3.0"); + + private static String getOperatingSystemName(OperatingSystem os) { + return os == OperatingSystem.OSX ? "macos" : os.getCheckedName(); + } + + private static String getArchitectureName(Architecture arch) { + return arch.getCheckedName(); + } + + private final DiscoJavaDistribution distribution; + private final Task fetchPackagesTask; + + public DiscoFetchJavaListTask(DownloadProvider downloadProvider, DiscoJavaDistribution distribution, Platform platform, JavaPackageType packageType) { + this.distribution = distribution; + + HashMap params = new HashMap<>(); + params.put("distribution", distribution.getApiParameter()); + params.put("package", packageType.isJDK() ? "jdk" : "jre"); + params.put("javafx_bundled", Boolean.toString(packageType.isJavaFXBundled())); + params.put("operating_system", getOperatingSystemName(platform.getOperatingSystem())); + params.put("architecture", getArchitectureName(platform.getArchitecture())); + params.put("archive_type", platform.getOperatingSystem() == OperatingSystem.WINDOWS ? "zip" : "tar.gz"); + params.put("directly_downloadable", "true"); + if (platform.getOperatingSystem() == OperatingSystem.LINUX) { + params.put("lib_c_type", "glibc"); + } + + this.fetchPackagesTask = new GetTask(downloadProvider.injectURLWithCandidates(NetworkUtils.withQuery(API_ROOT + "/packages", params))); + } + + @Override + public Collection> getDependents() { + return Collections.singleton(fetchPackagesTask); + } + + @Override + public void execute() throws Exception { + String json = fetchPackagesTask.getResult(); + List result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoJavaRemoteVersion.class)).getResult(); + + TreeMap map = new TreeMap<>(); + + for (DiscoJavaRemoteVersion version : result) { + if (!distribution.getApiParameter().equals(version.getDistribution())) + continue; + + int jdkVersion = version.getJdkVersion(); + DiscoJavaRemoteVersion oldVersion = map.get(jdkVersion); + if (oldVersion == null || VersionNumber.compare(version.getDistributionVersion(), oldVersion.getDistributionVersion()) > 0) { + map.put(jdkVersion, version); + } + } + + setResult(map); + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoJavaDistribution.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoJavaDistribution.java new file mode 100644 index 0000000000..80611d674b --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoJavaDistribution.java @@ -0,0 +1,116 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.download.java.disco; + +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.download.java.JavaDistribution; +import org.jackhuang.hmcl.download.java.JavaPackageType; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.Pair; +import org.jackhuang.hmcl.util.platform.Architecture; +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.platform.Platform; + +import java.util.*; + +import static org.jackhuang.hmcl.download.java.JavaPackageType.*; +import static org.jackhuang.hmcl.util.Pair.pair; +import static org.jackhuang.hmcl.util.platform.Architecture.*; +import static org.jackhuang.hmcl.util.platform.OperatingSystem.*; + +/** + * @author Glavo + */ +public enum DiscoJavaDistribution implements JavaDistribution { + TEMURIN("Eclipse Temurin", "temurin", "Adoptium", + EnumSet.of(JDK, JRE), + pair(WINDOWS, EnumSet.of(X86_64, X86, ARM64)), + pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64, PPC64LE, S390X, SPARCV9)), + pair(OSX, EnumSet.of(X86_64, ARM64))), + LIBERICA("Liberica", "liberica", "BellSoft", + EnumSet.of(JDK, JRE, JDKFX, JREFX), + pair(WINDOWS, EnumSet.of(X86_64, X86, ARM64)), + pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64LE)), + pair(OSX, EnumSet.of(X86_64, ARM64))), + ZULU("Zulu", "zulu", "Azul", + EnumSet.of(JDK, JRE, JDKFX, JREFX), + pair(WINDOWS, EnumSet.of(X86_64, X86, ARM64)), + pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64LE)), + pair(OSX, EnumSet.of(X86_64, ARM64))), + GRAALVM("GraalVM", "graalvm", "Oracle", + EnumSet.of(JDK), + pair(WINDOWS, EnumSet.of(X86_64, X86)), + pair(LINUX, EnumSet.of(X86_64, X86, ARM64, ARM32, RISCV64, PPC64LE)), + pair(OSX, EnumSet.of(X86_64, ARM64))); + + public static DiscoJavaDistribution of(String name) { + for (DiscoJavaDistribution distribution : values()) { + if (distribution.apiParameter.equalsIgnoreCase(name) || distribution.name().equalsIgnoreCase(name)) { + return distribution; + } + } + + return null; + } + + private final String displayName; + private final String apiParameter; + private final String vendor; + private final Set supportedPackageTypes; + private final Map> supportedPlatforms = new EnumMap<>(OperatingSystem.class); + + @SafeVarargs + DiscoJavaDistribution(String displayName, String apiParameter, String vendor, Set supportedPackageTypes, Pair>... supportedPlatforms) { + this.displayName = displayName; + this.apiParameter = apiParameter; + this.vendor = vendor; + this.supportedPackageTypes = supportedPackageTypes; + + for (Pair> platform : supportedPlatforms) { + this.supportedPlatforms.put(platform.getKey(), platform.getValue()); + } + } + + @Override + public String getDisplayName() { + return displayName; + } + + public String getApiParameter() { + return apiParameter; + } + + public String getVendor() { + return vendor; + } + + @Override + public Set getSupportedPackageTypes() { + return supportedPackageTypes; + } + + public boolean isSupport(Platform platform) { + EnumSet architectures = supportedPlatforms.get(platform.getOperatingSystem()); + return architectures != null && architectures.contains(platform.getArchitecture()); + } + + @Override + public Task> getFetchJavaVersionsTask(DownloadProvider provider, Platform platform, JavaPackageType packageType) { + return new DiscoFetchJavaListTask(provider, this, platform, packageType); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoJavaRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoJavaRemoteVersion.java new file mode 100644 index 0000000000..3698183d86 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoJavaRemoteVersion.java @@ -0,0 +1,259 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.download.java.disco; + +import com.google.gson.annotations.SerializedName; +import org.jackhuang.hmcl.download.java.JavaRemoteVersion; +import org.jackhuang.hmcl.util.gson.JsonUtils; + +/** + * @author Glavo + */ +public final class DiscoJavaRemoteVersion implements JavaRemoteVersion { + @SerializedName("id") + private final String id; + + @SerializedName("archive_type") + private final String archiveType; + + @SerializedName("distribution") + private final String distribution; + + @SerializedName("major_version") + private final int majorVersion; + + @SerializedName("java_version") + private final String javaVersion; + + @SerializedName("distribution_version") + private final String distributionVersion; + + @SerializedName("jdk_version") + private final int jdkVersion; + + @SerializedName("latest_build_available") + private final boolean latestBuildAvailable; + + @SerializedName("release_status") + private final String releaseStatus; + + @SerializedName("term_of_support") + private final String termOfSupport; + + @SerializedName("operating_system") + private final String operatingSystem; + + @SerializedName("lib_c_type") + private final String libCType; + + @SerializedName("architecture") + private final String architecture; + + @SerializedName("fpu") + private final String fpu; + + @SerializedName("package_type") + private final String packageType; + + @SerializedName("javafx_bundled") + private final boolean javafxBundled; + + @SerializedName("directly_downloadable") + private final boolean directlyDownloadable; + + @SerializedName("filename") + private final String fileName; + + @SerializedName("links") + private final Links links; + + @SerializedName("free_use_in_production") + private final boolean freeUseInProduction; + + @SerializedName("tck_tested") + private final String tckTested; + + @SerializedName("tck_cert_uri") + private final String tckCertUri; + + @SerializedName("aqavit_certified") + private final String aqavitCertified; + + @SerializedName("aqavit_cert_uri") + private final String aqavitCertUri; + + @SerializedName("size") + private final long size; + + public DiscoJavaRemoteVersion(String id, String archiveType, String distribution, int majorVersion, String javaVersion, String distributionVersion, int jdkVersion, boolean latestBuildAvailable, String releaseStatus, String termOfSupport, String operatingSystem, String libCType, String architecture, String fpu, String packageType, boolean javafxBundled, boolean directlyDownloadable, String fileName, Links links, boolean freeUseInProduction, String tckTested, String tckCertUri, String aqavitCertified, String aqavitCertUri, long size) { + this.id = id; + this.archiveType = archiveType; + this.distribution = distribution; + this.majorVersion = majorVersion; + this.javaVersion = javaVersion; + this.distributionVersion = distributionVersion; + this.jdkVersion = jdkVersion; + this.latestBuildAvailable = latestBuildAvailable; + this.releaseStatus = releaseStatus; + this.termOfSupport = termOfSupport; + this.operatingSystem = operatingSystem; + this.libCType = libCType; + this.architecture = architecture; + this.fpu = fpu; + this.packageType = packageType; + this.javafxBundled = javafxBundled; + this.directlyDownloadable = directlyDownloadable; + this.fileName = fileName; + this.links = links; + this.freeUseInProduction = freeUseInProduction; + this.tckTested = tckTested; + this.tckCertUri = tckCertUri; + this.aqavitCertified = aqavitCertified; + this.aqavitCertUri = aqavitCertUri; + this.size = size; + } + + public String getId() { + return id; + } + + public String getArchiveType() { + return archiveType; + } + + public String getDistribution() { + return distribution; + } + + public int getMajorVersion() { + return majorVersion; + } + + @Override + public String getJavaVersion() { + return javaVersion; + } + + @Override + public String getDistributionVersion() { + return distributionVersion; + } + + @Override + public int getJdkVersion() { + return jdkVersion; + } + + public boolean isLatestBuildAvailable() { + return latestBuildAvailable; + } + + public String getReleaseStatus() { + return releaseStatus; + } + + public String getTermOfSupport() { + return termOfSupport; + } + + public String getOperatingSystem() { + return operatingSystem; + } + + public String getLibCType() { + return libCType; + } + + public String getArchitecture() { + return architecture; + } + + public String getFpu() { + return fpu; + } + + public String getPackageType() { + return packageType; + } + + public boolean isJavafxBundled() { + return javafxBundled; + } + + public boolean isDirectlyDownloadable() { + return directlyDownloadable; + } + + public String getFileName() { + return fileName; + } + + public Links getLinks() { + return links; + } + + public boolean isFreeUseInProduction() { + return freeUseInProduction; + } + + public String getTckTested() { + return tckTested; + } + + public String getTckCertUri() { + return tckCertUri; + } + + public String getAqavitCertified() { + return aqavitCertified; + } + + public String getAqavitCertUri() { + return aqavitCertUri; + } + + public long getSize() { + return size; + } + + @Override + public String toString() { + return "DiscoJavaRemoteVersion " + JsonUtils.GSON.toJson(this); + } + + public static final class Links { + @SerializedName("pkg_info_uri") + private final String pkgInfoUri; + + @SerializedName("pkg_download_redirect") + private final String pkgDownloadRedirect; + + public Links(String pkgInfoUri, String pkgDownloadRedirect) { + this.pkgInfoUri = pkgInfoUri; + this.pkgDownloadRedirect = pkgDownloadRedirect; + } + + public String getPkgInfoUri() { + return pkgInfoUri; + } + + public String getPkgDownloadRedirect() { + return pkgDownloadRedirect; + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoRemoteFileInfo.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoRemoteFileInfo.java new file mode 100644 index 0000000000..2b31610f01 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoRemoteFileInfo.java @@ -0,0 +1,68 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.download.java.disco; + +import com.google.gson.annotations.SerializedName; + +/** + * @author Glavo + */ +public final class DiscoRemoteFileInfo { + @SerializedName("filename") + private final String fileName; + + @SerializedName("direct_download_uri") + private final String directDownloadUri; + + @SerializedName("checksum_type") + private final String checksumType; + + @SerializedName("checksum") + private final String checksum; + + @SerializedName("checksum_uri") + private final String checksumUri; + + public DiscoRemoteFileInfo(String fileName, String directDownloadUri, String checksumType, String checksum, String checksumUri) { + this.fileName = fileName; + this.directDownloadUri = directDownloadUri; + this.checksumType = checksumType; + this.checksum = checksum; + this.checksumUri = checksumUri; + } + + public String getFileName() { + return fileName; + } + + public String getDirectDownloadUri() { + return directDownloadUri; + } + + public String getChecksumType() { + return checksumType; + } + + public String getChecksum() { + return checksum; + } + + public String getChecksumUri() { + return checksumUri; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoResult.java new file mode 100644 index 0000000000..2d40f276ec --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/disco/DiscoResult.java @@ -0,0 +1,49 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.download.java.disco; + +import com.google.gson.reflect.TypeToken; + +import java.util.List; + +/** + * @author Glavo + */ +public final class DiscoResult { + + @SuppressWarnings("unchecked") + public static TypeToken> typeOf(Class argType) { + return (TypeToken>) TypeToken.getParameterized(DiscoResult.class, argType); + } + + private final List result; + private final String message; + + private DiscoResult(List result, String message) { + this.result = result; + this.message = message; + } + + public List getResult() { + return result; + } + + public String getMessage() { + return message; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaDistribution.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaDistribution.java new file mode 100644 index 0000000000..b8de72f728 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaDistribution.java @@ -0,0 +1,55 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.download.java.mojang; + +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.download.java.JavaDistribution; +import org.jackhuang.hmcl.download.java.JavaPackageType; +import org.jackhuang.hmcl.download.java.JavaRemoteVersion; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.platform.Platform; + +import java.util.Collections; +import java.util.Set; +import java.util.TreeMap; + +/** + * @author Glavo + */ +public final class MojangJavaDistribution implements JavaDistribution { + + public static final MojangJavaDistribution DISTRIBUTION = new MojangJavaDistribution(); + + private MojangJavaDistribution() { + } + + @Override + public String getDisplayName() { + return "Mojang"; + } + + @Override + public Set getSupportedPackageTypes() { + return Collections.singleton(JavaPackageType.JRE); + } + + @Override + public Task> getFetchJavaVersionsTask(DownloadProvider provider, Platform platform, JavaPackageType packageType) { + return null; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaDownloadTask.java similarity index 61% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaDownloadTask.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaDownloadTask.java index 9742ac1065..a37ad844d8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaDownloadTask.java @@ -15,20 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.download.java; +package org.jackhuang.hmcl.download.java.mojang; import org.jackhuang.hmcl.download.ArtifactMalformedException; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.game.DownloadInfo; import org.jackhuang.hmcl.game.GameJavaVersion; +import org.jackhuang.hmcl.java.*; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.ChecksumMismatchException; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.platform.OperatingSystem; -import org.jackhuang.hmcl.util.versioning.VersionNumber; +import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException; import org.tukaani.xz.LZMAInputStream; import java.io.File; @@ -40,50 +39,39 @@ import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; -import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class JavaDownloadTask extends Task { - private final GameJavaVersion javaVersion; - private final Path rootDir; - private String platform; - private final Task javaDownloadsTask; - private JavaDownloads.JavaDownload download; - private final List> dependencies = new ArrayList<>(); +public final class MojangJavaDownloadTask extends Task { + private final DownloadProvider downloadProvider; + private final Path target; + private final Task javaDownloadsTask; + private final List> dependencies = new ArrayList<>(); + + private volatile MojangJavaDownloads.JavaDownload download; - public JavaDownloadTask(GameJavaVersion javaVersion, Path rootDir, DownloadProvider downloadProvider) { - this.javaVersion = javaVersion; - this.rootDir = rootDir; + public MojangJavaDownloadTask(DownloadProvider downloadProvider, Path target, GameJavaVersion javaVersion, String platform) { + this.target = target; this.downloadProvider = downloadProvider; this.javaDownloadsTask = new GetTask(downloadProvider.injectURLWithCandidates( "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json")) .thenComposeAsync(javaDownloadsJson -> { - JavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, JavaDownloads.class); - if (!allDownloads.getDownloads().containsKey(platform)) throw new UnsupportedPlatformException(); - Map> osDownloads = allDownloads.getDownloads().get(platform); - if (!osDownloads.containsKey(javaVersion.getComponent())) throw new UnsupportedPlatformException(); - List candidates = osDownloads.get(javaVersion.getComponent()); - for (JavaDownloads.JavaDownload download : candidates) { - if (VersionNumber.compare(download.getVersion().getName(), Integer.toString(javaVersion.getMajorVersion())) >= 0) { + MojangJavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, MojangJavaDownloads.class); + + Map> osDownloads = allDownloads.getDownloads().get(platform); + if (osDownloads == null || !osDownloads.containsKey(javaVersion.getComponent())) + throw new UnsupportedPlatformException("Unsupported platform: " + platform); + List candidates = osDownloads.get(javaVersion.getComponent()); + for (MojangJavaDownloads.JavaDownload download : candidates) { + if (JavaInfo.parseVersion(download.getVersion().getName()) >= javaVersion.getMajorVersion()) { this.download = download; return new GetTask(downloadProvider.injectURLWithCandidates(download.getManifest().getUrl())); } } - throw new UnsupportedPlatformException(); + throw new UnsupportedPlatformException("Candidates: " + JsonUtils.GSON.toJson(candidates)); }) - .thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, RemoteFiles.class)); - } - - @Override - public boolean doPreExecute() { - return true; - } - - @Override - public void preExecute() throws Exception { - this.platform = JavaRepository.getSystemJavaPlatform().orElseThrow(UnsupportedPlatformException::new); + .thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, MojangJavaRemoteFiles.class)); } @Override @@ -93,11 +81,10 @@ public Collection> getDependents() { @Override public void execute() throws Exception { - Path jvmDir = rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent()); - for (Map.Entry entry : javaDownloadsTask.getResult().getFiles().entrySet()) { - Path dest = jvmDir.resolve(entry.getKey()); - if (entry.getValue() instanceof RemoteFiles.RemoteFile) { - RemoteFiles.RemoteFile file = ((RemoteFiles.RemoteFile) entry.getValue()); + for (Map.Entry entry : javaDownloadsTask.getResult().getFiles().entrySet()) { + Path dest = target.resolve(entry.getKey()); + if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteFile) { + MojangJavaRemoteFiles.RemoteFile file = ((MojangJavaRemoteFiles.RemoteFile) entry.getValue()); // Use local file if it already exists try { @@ -115,11 +102,11 @@ public void execute() throws Exception { if (file.getDownloads().containsKey("lzma")) { DownloadInfo download = file.getDownloads().get("lzma"); - File tempFile = jvmDir.resolve(entry.getKey() + ".lzma").toFile(); + File tempFile = target.resolve(entry.getKey() + ".lzma").toFile(); FileDownloadTask task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1())); task.setName(entry.getKey()); dependencies.add(task.thenRunAsync(() -> { - Path decompressed = jvmDir.resolve(entry.getKey() + ".tmp"); + Path decompressed = target.resolve(entry.getKey() + ".tmp"); try (LZMAInputStream input = new LZMAInputStream(new FileInputStream(tempFile))) { Files.copy(input, decompressed, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { @@ -144,10 +131,10 @@ public void execute() throws Exception { } else { continue; } - } else if (entry.getValue() instanceof RemoteFiles.RemoteDirectory) { + } else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteDirectory) { Files.createDirectories(dest); - } else if (entry.getValue() instanceof RemoteFiles.RemoteLink) { - RemoteFiles.RemoteLink link = ((RemoteFiles.RemoteLink) entry.getValue()); + } else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteLink) { + MojangJavaRemoteFiles.RemoteLink link = ((MojangJavaRemoteFiles.RemoteLink) entry.getValue()); Files.deleteIfExists(dest); Files.createSymbolicLink(dest, Paths.get(link.getTarget())); } @@ -166,16 +153,16 @@ public boolean doPostExecute() { @Override public void postExecute() throws Exception { - FileUtils.writeText(rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(".version").toFile(), download.getVersion().getName()); - FileUtils.writeText(rootDir.resolve(javaVersion.getComponent()).resolve(platform).resolve(javaVersion.getComponent() + ".sha1").toFile(), - javaDownloadsTask.getResult().getFiles().entrySet().stream() - .filter(entry -> entry.getValue() instanceof RemoteFiles.RemoteFile) - .map(entry -> { - RemoteFiles.RemoteFile file = (RemoteFiles.RemoteFile) entry.getValue(); - return entry.getKey() + " /#// " + file.getDownloads().get("raw").getSha1() + " " + file.getDownloads().get("raw").getSize(); - }) - .collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR))); + setResult(new Result(download, javaDownloadsTask.getResult())); } - public static class UnsupportedPlatformException extends Exception {} + public static final class Result { + public final MojangJavaDownloads.JavaDownload download; + public final MojangJavaRemoteFiles remoteFiles; + + public Result(MojangJavaDownloads.JavaDownload download, MojangJavaRemoteFiles remoteFiles) { + this.download = download; + this.remoteFiles = remoteFiles; + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaDownloads.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaDownloads.java similarity index 81% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaDownloads.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaDownloads.java index 1c214ea8eb..e549664676 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/JavaDownloads.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaDownloads.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.download.java; +package org.jackhuang.hmcl.download.java.mojang; import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; @@ -27,12 +27,12 @@ import java.util.List; import java.util.Map; -@JsonAdapter(JavaDownloads.Adapter.class) -public class JavaDownloads { +@JsonAdapter(MojangJavaDownloads.Adapter.class) +public class MojangJavaDownloads { private final Map>> downloads; - public JavaDownloads(Map>> downloads) { + public MojangJavaDownloads(Map>> downloads) { this.downloads = downloads; } @@ -40,16 +40,16 @@ public Map>> getDownloads() { return downloads; } - public static class Adapter implements JsonSerializer, JsonDeserializer { + public static class Adapter implements JsonSerializer, JsonDeserializer { @Override - public JsonElement serialize(JavaDownloads src, Type typeOfSrc, JsonSerializationContext context) { + public JsonElement serialize(MojangJavaDownloads src, Type typeOfSrc, JsonSerializationContext context) { return context.serialize(src.downloads); } @Override - public JavaDownloads deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - return new JavaDownloads(context.deserialize(json, new TypeToken>>>() { + public MojangJavaDownloads deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return new MojangJavaDownloads(context.deserialize(json, new TypeToken>>>() { }.getType())); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/RemoteFiles.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaRemoteFiles.java similarity index 94% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/RemoteFiles.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaRemoteFiles.java index d880863ca6..bc6052d169 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/RemoteFiles.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaRemoteFiles.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.download.java; +package org.jackhuang.hmcl.download.java.mojang; import org.jackhuang.hmcl.game.DownloadInfo; import org.jackhuang.hmcl.util.gson.JsonSubtype; @@ -24,10 +24,10 @@ import java.util.Collections; import java.util.Map; -public class RemoteFiles { +public final class MojangJavaRemoteFiles { private final Map files; - public RemoteFiles(Map files) { + public MojangJavaRemoteFiles(Map files) { this.files = files; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaRemoteVersion.java new file mode 100644 index 0000000000..ed8fece9a7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/java/mojang/MojangJavaRemoteVersion.java @@ -0,0 +1,56 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.download.java.mojang; + +import org.jackhuang.hmcl.download.java.JavaRemoteVersion; +import org.jackhuang.hmcl.game.GameJavaVersion; + +/** + * @author Glavo + */ +public final class MojangJavaRemoteVersion implements JavaRemoteVersion { + private final GameJavaVersion gameJavaVersion; + + public MojangJavaRemoteVersion(GameJavaVersion gameJavaVersion) { + this.gameJavaVersion = gameJavaVersion; + } + + public GameJavaVersion getGameJavaVersion() { + return gameJavaVersion; + } + + @Override + public int getJdkVersion() { + return gameJavaVersion.getMajorVersion(); + } + + @Override + public String getJavaVersion() { + return String.valueOf(getJdkVersion()); + } + + @Override + public String getDistributionVersion() { + return String.valueOf(getJdkVersion()); + } + + @Override + public String toString() { + return "MojangJavaRemoteVersion[gameJavaVersion=" + gameJavaVersion + "]"; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java index 838232fe8e..3569a6e11f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java @@ -36,7 +36,7 @@ import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.CommandBuilder; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.SystemUtils; import org.jetbrains.annotations.NotNull; @@ -125,7 +125,7 @@ public void execute() throws Exception { throw new Exception("Game processor jar does not have main class " + jar); List command = new ArrayList<>(); - command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString()); + command.add(JavaRuntime.getDefault().getBinary().toString()); command.add("-cp"); List classpath = new ArrayList<>(processor.getClasspath().size() + 1); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java index ffc73e3f17..1320d1a172 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java @@ -27,7 +27,7 @@ import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.CommandBuilder; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.SystemUtils; import org.jackhuang.hmcl.util.versioning.VersionNumber; import org.jenkinsci.constant_pool_scanner.ConstantPool; @@ -144,7 +144,7 @@ public void execute() throws Exception { Path optiFineLibraryPath = gameRepository.getLibraryFile(version, optiFineLibrary).toPath(); if (Files.exists(fs.getPath("optifine/Patcher.class"))) { String[] command = { - JavaVersion.fromCurrentEnvironment().getBinary().toString(), + JavaRuntime.getDefault().getBinary().toString(), "-cp", dest.toString(), "optifine.Patcher", diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameJavaVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameJavaVersion.java index c77ec06f01..5f5afda69c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameJavaVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameJavaVersion.java @@ -17,7 +17,91 @@ */ package org.jackhuang.hmcl.game; -public class GameJavaVersion { +import org.jackhuang.hmcl.util.platform.Architecture; +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.platform.Platform; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; + +import java.util.*; + +public final class GameJavaVersion { + public static final GameJavaVersion JAVA_21 = new GameJavaVersion("java-runtime-delta", 21); + public static final GameJavaVersion JAVA_17 = new GameJavaVersion("java-runtime-beta", 17); + public static final GameJavaVersion JAVA_16 = new GameJavaVersion("java-runtime-alpha", 16); + public static final GameJavaVersion JAVA_8 = new GameJavaVersion("jre-legacy", 8); + + public static final GameJavaVersion LATEST = JAVA_21; + + public static GameJavaVersion getMinimumJavaVersion(GameVersionNumber gameVersion) { + if (gameVersion.compareTo("1.21") >= 0) + return JAVA_21; + if (gameVersion.compareTo("1.18") >= 0) + return JAVA_17; + if (gameVersion.compareTo("1.17") >= 0) + return JAVA_16; + if (gameVersion.compareTo("1.13") >= 0) + return JAVA_8; + return null; + } + + public static GameJavaVersion get(int major) { + switch (major) { + case 8: + return JAVA_8; + case 16: + return JAVA_16; + case 17: + return JAVA_17; + case 21: + return JAVA_21; + default: + return null; + } + } + + public static boolean isSupportedPlatform(Platform platform) { + OperatingSystem os = platform.getOperatingSystem(); + Architecture arch = platform.getArchitecture(); + switch (arch) { + case X86: + return os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX; + case X86_64: + return os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX || os == OperatingSystem.OSX; + case ARM64: + return os == OperatingSystem.WINDOWS || os == OperatingSystem.OSX; + default: + return false; + } + } + + public static List getSupportedVersions(Platform platform) { + OperatingSystem operatingSystem = platform.getOperatingSystem(); + Architecture architecture = platform.getArchitecture(); + if (architecture == Architecture.X86) { + switch (operatingSystem) { + case WINDOWS: + return Arrays.asList(JAVA_8, JAVA_16, JAVA_17); + case LINUX: + return Collections.singletonList(JAVA_8); + } + } else if (architecture == Architecture.X86_64) { + switch (operatingSystem) { + case WINDOWS: + case LINUX: + case OSX: + return Arrays.asList(JAVA_8, JAVA_16, JAVA_17, JAVA_21); + } + } else if (architecture == Architecture.ARM64) { + switch (operatingSystem) { + case WINDOWS: + case OSX: + return Arrays.asList(JAVA_17, JAVA_21); + } + } + + return Collections.emptyList(); + } + private final String component; private final int majorVersion; @@ -38,8 +122,16 @@ public int getMajorVersion() { return majorVersion; } - public static final GameJavaVersion JAVA_21 = new GameJavaVersion("java-runtime-delta", 21); - public static final GameJavaVersion JAVA_17 = new GameJavaVersion("java-runtime-beta", 17); - public static final GameJavaVersion JAVA_16 = new GameJavaVersion("java-runtime-alpha", 16); - public static final GameJavaVersion JAVA_8 = new GameJavaVersion("jre-legacy", 8); + @Override + public int hashCode() { + return getMajorVersion(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GameJavaVersion)) return false; + GameJavaVersion that = (GameJavaVersion) o; + return majorVersion == that.majorVersion; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java index 9dbbd233c7..dc7abb453c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java @@ -20,7 +20,7 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.platform.Architecture; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jackhuang.hmcl.util.versioning.VersionNumber; @@ -33,48 +33,47 @@ import static org.jackhuang.hmcl.download.LibraryAnalyzer.LAUNCH_WRAPPER_MAIN; public enum JavaVersionConstraint { - // Minecraft>=1.13 requires Java 8 - VANILLA_JAVA_8(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atLeast("1.13"), VersionNumber.atLeast("1.8")), - // Minecraft 1.17 requires Java 16 - VANILLA_JAVA_16(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atLeast("1.17"), VersionNumber.atLeast("16")), - // Minecraft>=1.18 requires Java 17 - VANILLA_JAVA_17(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atLeast("1.18"), VersionNumber.atLeast("17")), - // Minecraft>=1.20.5 requires Java 21 - VANILLA_JAVA_21(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atLeast("1.20.5"), VersionNumber.atLeast("21")), + VANILLA(true, VersionRange.all(), VersionRange.all()) { + @Override + public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) { + GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersionNumber); + return minimumJavaVersion == null || java.getParsedVersion() >= minimumJavaVersion.getMajorVersion(); + } + }, // Minecraft<=1.7.2+Forge requires Java<=7, But LegacyModFixer may fix that problem. So only suggest user using Java 7. - MODDED_JAVA_7(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.atMost("1.7.2"), VersionNumber.atMost("1.7.999")) { + MODDED_JAVA_7(false, GameVersionNumber.atMost("1.7.2"), VersionNumber.atMost("1.7.999")) { @Override protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { + @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return version != null && analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE); } }, - MODDED_JAVA_8(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.between("1.7.10", "1.16.999"), VersionNumber.between("1.8", "1.8.999")) { + MODDED_JAVA_8(false, GameVersionNumber.between("1.7.10", "1.16.999"), VersionNumber.between("1.8", "1.8.999")) { @Override protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { + @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE); } }, - MODDED_JAVA_16(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.between("1.17", "1.17.999"), VersionNumber.between("16", "16.999")) { + MODDED_JAVA_16(false, GameVersionNumber.between("1.17", "1.17.999"), VersionNumber.between("16", "16.999")) { @Override protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { + @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE); } }, - MODDED_JAVA_17(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.atLeast("1.18"), VersionNumber.between("17", "17.999")) { + MODDED_JAVA_17(false, GameVersionNumber.atLeast("1.18"), VersionNumber.between("17", "17.999")) { @Override protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { + @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE); } }, // LaunchWrapper<=1.12 will crash because LaunchWrapper assumes the system class loader is an instance of URLClassLoader (Java 8) - LAUNCH_WRAPPER(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) { + LAUNCH_WRAPPER(true, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) { @Override protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { + @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { if (version == null) return false; return LAUNCH_WRAPPER_MAIN.equals(version.getMainClass()) && version.getLibraries().stream() @@ -83,12 +82,12 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul } }, // Minecraft>=1.13 may crash when generating world on Java [1.8,1.8.0_51) - VANILLA_JAVA_8_51(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.atLeast("1.13"), VersionNumber.atLeast("1.8.0_51")), + VANILLA_JAVA_8_51(false, GameVersionNumber.atLeast("1.13"), VersionNumber.atLeast("1.8.0_51")), // Minecraft with suggested java version recorded in game json is restrictedly constrained. - GAME_JSON(JavaVersionConstraint.RULE_MANDATORY, VersionRange.all(), VersionRange.all()) { + GAME_JSON(true, VersionRange.all(), VersionRange.all()) { @Override protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { + @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { if (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 gameVersionNumber.compareTo("1.7.10") >= 0 && version.getJavaVersion() != null; @@ -107,26 +106,26 @@ public VersionRange getJavaVersionRange(Version version) { }, // On Linux, JDK 9+ cannot launch Minecraft<=1.12.2, since JDK 9+ does not accept loading native library built in different arch. // For example, JDK 9+ 64-bit cannot load 32-bit lwjgl native library. - VANILLA_LINUX_JAVA_8(JavaVersionConstraint.RULE_MANDATORY, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) { + VANILLA_LINUX_JAVA_8(true, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) { @Override protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { + @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64 - && (javaVersion == null || javaVersion.getArchitecture() == Architecture.X86_64); + && (java == null || java.getArchitecture() == Architecture.X86_64); } @Override - public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) { - return javaVersion.getArchitecture() != Architecture.X86_64 || super.checkJava(gameVersionNumber, version, javaVersion); + public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) { + return java.getArchitecture() != Architecture.X86_64 || super.checkJava(gameVersionNumber, version, java); } }, // Minecraft currently does not provide official support for architectures other than x86 and x86-64. - VANILLA_X86(JavaVersionConstraint.RULE_SUGGESTED, VersionRange.all(), VersionRange.all()) { + VANILLA_X86(false, VersionRange.all(), VersionRange.all()) { @Override protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { - if (javaVersion == null || javaVersion.getArchitecture() != Architecture.ARM64) + @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { + if (java == null || java.getArchitecture() != Architecture.ARM64) return false; if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) @@ -136,16 +135,16 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul } @Override - public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) { - return javaVersion.getArchitecture().isX86(); + public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) { + return java.getArchitecture().isX86(); } }, // Minecraft 1.16+Forge with crash because JDK-8273826 - MODLAUNCHER_8(JavaVersionConstraint.RULE_SUGGESTED, GameVersionNumber.between("1.16.3", "1.17.1"), VersionRange.all()) { + MODLAUNCHER_8(false, GameVersionNumber.between("1.16.3", "1.17.1"), VersionRange.all()) { @Override protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { - if (version == null || javaVersion == null || analyzer == null) return false; + @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { + if (version == null || java == null || analyzer == null) return false; VersionNumber forgePatchVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE) .map(VersionNumber::asVersion) .orElse(null); @@ -167,36 +166,36 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul } @Override - public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) { - int parsedJavaVersion = javaVersion.getParsedVersion(); + public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) { + int parsedJavaVersion = java.getParsedVersion(); if (parsedJavaVersion > 17) { return false; } else if (parsedJavaVersion == 8) { - return javaVersion.getVersionNumber().compareTo(VersionNumber.asVersion("1.8.0_321")) < 0; + return java.getVersionNumber().compareTo(VersionNumber.asVersion("1.8.0_321")) < 0; } else if (parsedJavaVersion == 11) { - return javaVersion.getVersionNumber().compareTo(VersionNumber.asVersion("11.0.14")) < 0; + return java.getVersionNumber().compareTo(VersionNumber.asVersion("11.0.14")) < 0; } else if (parsedJavaVersion == 15) { - return javaVersion.getVersionNumber().compareTo(VersionNumber.asVersion("15.0.6")) < 0; + return java.getVersionNumber().compareTo(VersionNumber.asVersion("15.0.6")) < 0; } else if (parsedJavaVersion == 17) { - return javaVersion.getVersionNumber().compareTo(VersionNumber.asVersion("17.0.2")) < 0; + return java.getVersionNumber().compareTo(VersionNumber.asVersion("17.0.2")) < 0; } else { return true; } } }; - private final int type; + private final boolean isMandatory; private final VersionRange gameVersionRange; private final VersionRange javaVersionRange; - JavaVersionConstraint(int type, VersionRange gameVersionRange, VersionRange javaVersionRange) { - this.type = type; + JavaVersionConstraint(boolean isMandatory, VersionRange gameVersionRange, VersionRange javaVersionRange) { + this.isMandatory = isMandatory; this.gameVersionRange = gameVersionRange; this.javaVersionRange = javaVersionRange; } - public int getType() { - return type; + public boolean isMandatory() { + return isMandatory; } public VersionRange getGameVersionRange() { @@ -208,112 +207,20 @@ public VersionRange getJavaVersionRange(Version version) { } public final boolean appliesToVersion(@Nullable GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, LibraryAnalyzer analyzer) { + @Nullable JavaRuntime java, LibraryAnalyzer analyzer) { return gameVersionRange.contains(gameVersionNumber) - && appliesToVersionImpl(gameVersionNumber, version, javaVersion, analyzer); + && appliesToVersionImpl(gameVersionNumber, version, java, analyzer); } protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, - @Nullable JavaVersion javaVersion, @Nullable LibraryAnalyzer analyzer) { + @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return true; } @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) { - return getJavaVersionRange(version).contains(javaVersion.getVersionNumber()); + public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) { + return getJavaVersionRange(version).contains(java.getVersionNumber()); } public static final List ALL = Lang.immutableListOf(values()); - - public static VersionRanges findSuitableJavaVersionRange(GameVersionNumber gameVersion, Version version) { - VersionRange mandatoryJavaRange = VersionRange.all(); - VersionRange suggestedJavaRange = VersionRange.all(); - LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null; - for (JavaVersionConstraint java : ALL) { - if (java.appliesToVersion(gameVersion, version, null, analyzer)) { - VersionRange javaVersionRange = java.getJavaVersionRange(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 new VersionRanges(mandatoryJavaRange, suggestedJavaRange); - } - - @Nullable - public static JavaVersion findSuitableJavaVersion(GameVersionNumber gameVersion, Version version) throws InterruptedException { - VersionRanges range = findSuitableJavaVersionRange(gameVersion, version); - - boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64 - && (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) - && gameVersion.compareTo("1.6") < 0; - - JavaVersion mandatory = null; - JavaVersion suggested = null; - for (JavaVersion javaVersion : JavaVersion.getJavas()) { - // Do not automatically select 32-bit Java - if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && javaVersion.getArchitecture() == Architecture.X86) - continue; - - // select the latest x86 java that this version accepts. - if (forceX86 && !javaVersion.getArchitecture().isX86()) - continue; - - VersionNumber javaVersionNumber = javaVersion.getVersionNumber(); - if (range.getMandatory().contains(javaVersionNumber)) { - if (mandatory == null) mandatory = javaVersion; - else if (compareJavaVersion(javaVersion, mandatory) > 0) { - mandatory = javaVersion; - } - } - if (range.getSuggested().contains(javaVersionNumber)) { - if (suggested == null) suggested = javaVersion; - else if (compareJavaVersion(javaVersion, suggested) > 0) { - suggested = javaVersion; - } - } - } - - if (suggested != null) return suggested; - else return mandatory; - } - - private static int compareJavaVersion(JavaVersion javaVersion1, JavaVersion javaVersion2) { - Architecture arch1 = javaVersion1.getArchitecture(); - Architecture arch2 = javaVersion2.getArchitecture(); - - if (arch1 != arch2) { - if (arch1 == Architecture.X86_64) { - return 1; - } - if (arch2 == Architecture.X86_64) { - return -1; - } - } - return javaVersion1.getVersionNumber().compareTo(javaVersion2.getVersionNumber()); - } - - public static final int RULE_MANDATORY = 1; - public static final int RULE_SUGGESTED = 2; - - public static final class VersionRanges { - private final VersionRange mandatory; - private final VersionRange suggested; - - public VersionRanges(VersionRange mandatory, VersionRange suggested) { - this.mandatory = mandatory; - this.suggested = suggested; - } - - public VersionRange getMandatory() { - return mandatory; - } - - public VersionRange getSuggested() { - return suggested; - } - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java index 40ecbc8692..f524533b22 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.game; -import org.jackhuang.hmcl.util.platform.JavaVersion; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -32,7 +32,7 @@ public class LaunchOptions implements Serializable { private File gameDir; - private JavaVersion java; + private JavaRuntime java; private String versionName; private String versionType; private String profileName; @@ -73,7 +73,7 @@ public File getGameDir() { /** * The Java Environment that Minecraft runs on. */ - public JavaVersion getJava() { + public JavaRuntime getJava() { return java; } @@ -312,7 +312,7 @@ public Builder setGameDir(File gameDir) { return this; } - public Builder setJava(JavaVersion java) { + public Builder setJava(JavaRuntime java) { options.java = java; return this; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaInfo.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaInfo.java new file mode 100644 index 0000000000..160a152bb7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaInfo.java @@ -0,0 +1,280 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.java; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.jackhuang.hmcl.util.KeyValuePairProperties; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.platform.Architecture; +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.platform.Platform; +import org.jackhuang.hmcl.util.tree.ArchiveFileTree; +import org.jackhuang.hmcl.util.versioning.VersionNumber; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * @author Glavo + */ +public final class JavaInfo { + public static int parseVersion(String version) { + try { + int idx = version.indexOf('.'); + if (idx < 0) { + idx = version.indexOf('u'); + return idx > 0 ? Integer.parseInt(version.substring(0, idx)) : Integer.parseInt(version); + } else { + int major = Integer.parseInt(version.substring(0, idx)); + if (major != 1) { + return major; + } else { + int idx2 = version.indexOf('.', idx + 1); + if (idx2 < 0) { + return -1; + } + return Integer.parseInt(version.substring(idx + 1, idx2)); + } + } + } catch (NumberFormatException e) { + return -1; + } + } + + public static JavaInfo fromReleaseFile(BufferedReader reader) throws IOException { + KeyValuePairProperties properties = KeyValuePairProperties.load(reader); + String osName = properties.get("OS_NAME"); + String osArch = properties.get("OS_ARCH"); + String vendor = properties.get("IMPLEMENTOR"); + + OperatingSystem os = "".equals(osName) && "OpenJDK BSD Porting Team".equals(vendor) + ? OperatingSystem.FREEBSD + : OperatingSystem.parseOSName(osName); + + Architecture arch = Architecture.parseArchName(osArch); + String javaVersion = properties.get("JAVA_VERSION"); + + if (os == OperatingSystem.UNKNOWN) + throw new IOException("Unknown operating system: " + osName); + + if (arch == Architecture.UNKNOWN) + throw new IOException("Unknown architecture: " + osArch); + + if (javaVersion == null) + throw new IOException("Missing Java version"); + + return new JavaInfo(Platform.getPlatform(os, arch), javaVersion, vendor); + } + + public static JavaInfo fromReleaseFile(Path releaseFile) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(releaseFile)) { + return fromReleaseFile(reader); + } + } + + public static JavaInfo fromArchive(ArchiveFileTree tree) throws IOException { + if (tree.getRoot().getSubDirs().size() != 1 || !tree.getRoot().getFiles().isEmpty()) + throw new IOException(); + + ArchiveFileTree.Dir jdkRoot = tree.getRoot().getSubDirs().values().iterator().next(); + E releaseEntry = jdkRoot.getFiles().get("release"); + if (releaseEntry == null) + throw new IOException("Missing release file"); + + JavaInfo info; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(tree.getInputStream(releaseEntry), StandardCharsets.UTF_8))) { + info = JavaInfo.fromReleaseFile(reader); + } + + ArchiveFileTree.Dir binDir = jdkRoot.getSubDirs().get("bin"); + if (binDir == null || binDir.getFiles().get(info.getPlatform().getOperatingSystem().getJavaExecutable()) == null) + throw new IOException("Missing java executable file"); + + return info; + } + + public static String normalizeVendor(String vendor) { + if (vendor == null) + return null; + + switch (vendor) { + case "N/A": + return null; + case "Oracle Corporation": + return "Oracle"; + case "Azul Systems, Inc.": + return "Azul"; + case "IBM Corporation": + case "International Business Machines Corporation": + return "IBM"; + case "Eclipse Adoptium": + return "Adoptium"; + default: + return vendor; + } + } + + private static final String OS_ARCH = "os.arch = "; + private static final String JAVA_VERSION = "java.version = "; + private static final String JAVA_VENDOR = "java.vendor = "; + private static final String VERSION_PREFIX = "version \""; + + public static JavaInfo fromExecutable(Path executable) throws IOException { + return fromExecutable(executable, true); + } + + public static JavaInfo fromExecutable(Path executable, boolean tryFindReleaseFile) throws IOException { + assert executable.isAbsolute(); + Path parent = executable.getParent(); + if (tryFindReleaseFile && parent != null && parent.getFileName() != null && parent.getFileName().toString().equals("bin")) { + Path javaHome = parent.getParent(); + if (javaHome != null && javaHome.getFileName() != null) { + Path releaseFile = javaHome.resolve("release"); + String javaHomeName = javaHome.getFileName().toString(); + if ((javaHomeName.contains("jre") || javaHomeName.contains("jdk") || javaHomeName.contains("openj9")) && Files.isRegularFile(releaseFile)) { + try { + return fromReleaseFile(releaseFile); + } catch (IOException ignored) { + } + } + } + } + + String osArch = null; + String version = null; + String vendor = null; + Platform platform = null; + + String executablePath = executable.toString(); + + Process process = new ProcessBuilder(executablePath, "-XshowSettings:properties", "-version").start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), OperatingSystem.NATIVE_CHARSET))) { + for (String line; (line = reader.readLine()) != null; ) { + + int idx = line.indexOf(OS_ARCH); + if (idx >= 0) { + osArch = line.substring(idx + OS_ARCH.length()).trim(); + if (version != null && vendor != null) + break; + else + continue; + } + + idx = line.indexOf(JAVA_VERSION); + if (idx >= 0) { + version = line.substring(idx + JAVA_VERSION.length()).trim(); + if (osArch != null && vendor != null) + break; + else + continue; + } + + idx = line.indexOf(JAVA_VENDOR); + if (idx >= 0) { + vendor = line.substring(idx + JAVA_VENDOR.length()).trim(); + if (osArch != null && version != null) + break; + else + //noinspection UnnecessaryContinue + continue; + } + } + } + + if (osArch != null) + platform = Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.parseArchName(osArch)); + + // Java 6 + if (version == null) { + boolean is64Bit = false; + process = new ProcessBuilder(executablePath, "-version").start(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), OperatingSystem.NATIVE_CHARSET))) { + for (String line; (line = reader.readLine()) != null; ) { + if (version == null) { + int idx = line.indexOf(VERSION_PREFIX); + if (idx >= 0) { + int begin = idx + VERSION_PREFIX.length(); + int end = line.indexOf('"', begin); + if (end >= 0) { + version = line.substring(begin, end); + } + } + } + + if (line.contains("64-Bit")) + is64Bit = true; + } + } + + if (platform == null) + platform = Platform.getPlatform(OperatingSystem.CURRENT_OS, is64Bit ? Architecture.X86_64 : Architecture.X86); + + if (version == null) + throw new IOException("Cannot determine version"); + } + + return new JavaInfo(platform, version, vendor); + } + + public static final JavaInfo CURRENT_ENVIRONMENT = new JavaInfo(Platform.CURRENT_PLATFORM, System.getProperty("java.version"), System.getProperty("java.vendor")); + + private final Platform platform; + private final String version; + private final @Nullable String vendor; + + private final transient int parsedVersion; + private final transient VersionNumber versionNumber; + + public JavaInfo(Platform platform, String version, @Nullable String vendor) { + this.platform = platform; + this.version = version; + this.parsedVersion = parseVersion(version); + this.versionNumber = VersionNumber.asVersion(version); + this.vendor = vendor; + } + + public Platform getPlatform() { + return platform; + } + + public String getVersion() { + return version; + } + + public VersionNumber getVersionNumber() { + return versionNumber; + } + + public int getParsedVersion() { + return parsedVersion; + } + + public @Nullable String getVendor() { + return vendor; + } + + @Override + public String toString() { + return JsonUtils.GSON.toJson(this); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaRepository.java new file mode 100644 index 0000000000..2fda58be71 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaRepository.java @@ -0,0 +1,44 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.java; + +import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.game.GameJavaVersion; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.platform.Platform; + +import java.nio.file.Path; +import java.util.Collection; + +/** + * @author Glavo + */ +public interface JavaRepository { + + Path getJavaDir(Platform platform, String name); + + Path getManifestFile(Platform platform, String name); + + Collection getAllJava(Platform platform); + + Task getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion); + + Task getUninstallJavaTask(Platform platform, String name); + + Task getUninstallJavaTask(JavaRuntime java); +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaRuntime.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaRuntime.java new file mode 100644 index 0000000000..dffdab18ea --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/java/JavaRuntime.java @@ -0,0 +1,156 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.java; + +import org.jackhuang.hmcl.util.platform.Architecture; +import org.jackhuang.hmcl.util.platform.Bits; +import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.util.platform.Platform; +import org.jackhuang.hmcl.util.versioning.VersionNumber; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * @author Glavo + */ +public final class JavaRuntime implements Comparable { + + public static JavaRuntime of(Path binary, JavaInfo info, boolean isManaged) { + String javacName = info.getPlatform().getOperatingSystem() == OperatingSystem.WINDOWS ? "javac.exe" : "javac"; + return new JavaRuntime(binary, info, isManaged, Files.isRegularFile(binary.resolveSibling(javacName))); + } + + private final Path binary; + private final JavaInfo info; + private final boolean isManaged; + private final boolean isJDK; + + public JavaRuntime(Path binary, JavaInfo info, boolean isManaged, boolean isJDK) { + this.binary = binary; + this.info = info; + this.isManaged = isManaged; + this.isJDK = isJDK; + } + + public boolean isManaged() { + return isManaged; + } + + public Path getBinary() { + return binary; + } + + public String getVersion() { + return info.getVersion(); + } + + public Platform getPlatform() { + return info.getPlatform(); + } + + public Architecture getArchitecture() { + return getPlatform().getArchitecture(); + } + + public Bits getBits() { + return getPlatform().getBits(); + } + + public VersionNumber getVersionNumber() { + return info.getVersionNumber(); + } + + /** + * The major version of Java installation. + */ + public int getParsedVersion() { + return info.getParsedVersion(); + } + + public String getVendor() { + return info.getVendor(); + } + + public boolean isJDK() { + return isJDK; + } + + @Override + public int compareTo(@NotNull JavaRuntime that) { + if (this.isManaged != that.isManaged) { + return this.isManaged ? -1 : 1; + } + + int c = Integer.compare(this.getParsedVersion(), that.getParsedVersion()); + if (c != 0) + return c; + + c = this.getVersionNumber().compareTo(that.getVersionNumber()); + if (c != 0) + return c; + + c = this.getArchitecture().compareTo(that.getArchitecture()); + if (c != 0) + return c; + + return this.getBinary().compareTo(that.getBinary()); + } + + @Override + public int hashCode() { + return binary.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof JavaRuntime)) return false; + JavaRuntime that = (JavaRuntime) o; + return this.getBinary().equals(that.getBinary()); + } + + public static final JavaRuntime CURRENT_JAVA; + public static final int CURRENT_VERSION; + + public static JavaRuntime getDefault() { + return CURRENT_JAVA; + } + + static { + String javaHome = System.getProperty("java.home"); + Path executable = null; + if (javaHome != null) { + executable = Paths.get(javaHome, "bin", OperatingSystem.CURRENT_OS.getJavaExecutable()); + try { + executable = executable.toRealPath(); + } catch (IOException ignored) { + } + + if (!Files.isRegularFile(executable)) { + executable = null; + } + } + + CURRENT_JAVA = executable != null ? JavaRuntime.of(executable, JavaInfo.CURRENT_ENVIRONMENT, false) : null; + CURRENT_VERSION = JavaInfo.CURRENT_ENVIRONMENT.getParsedVersion(); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index 2d27b56233..6bb0494739 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -133,7 +133,7 @@ private Command generateCommandLine(File nativeFolder) throws IOException { res.addDefault("-Xms", options.getMinMemory() + "m"); if (options.getMetaspace() != null && options.getMetaspace() > 0) - if (options.getJava().getParsedVersion() < JavaVersion.JAVA_8) + if (options.getJava().getParsedVersion() < 8) res.addDefault("-XX:PermSize=", options.getMetaspace() + "m"); else res.addDefault("-XX:MetaspaceSize=", options.getMetaspace() + "m"); @@ -186,7 +186,7 @@ private Command generateCommandLine(File nativeFolder) throws IOException { res.addDefault("-Duser.home=", options.getGameDir().getParent()); // Using G1GC with its settings by default - if (options.getJava().getParsedVersion() >= JavaVersion.JAVA_8 + if (options.getJava().getParsedVersion() >= 8 && res.noneMatch(arg -> "-XX:-UseG1GC".equals(arg) || (arg.startsWith("-XX:+Use") && arg.endsWith("GC")))) { res.addUnstableDefault("UnlockExperimentalVMOptions", true); res.addUnstableDefault("UseG1GC", true); @@ -206,7 +206,7 @@ private Command generateCommandLine(File nativeFolder) throws IOException { res.addDefault("-Xss", "1m"); } - if (options.getJava().getParsedVersion() == JavaVersion.JAVA_16) + if (options.getJava().getParsedVersion() == 16) res.addDefault("--illegal-access=", "permit"); res.addDefault("-Dfml.ignoreInvalidMinecraftCertificates=", "true"); @@ -308,7 +308,7 @@ public Map getFeatures() { } private final Map> forbiddens = mapOf( - pair("-Xincgc", () -> options.getJava().getParsedVersion() >= JavaVersion.JAVA_9) + pair("-Xincgc", () -> options.getJava().getParsedVersion() >= 9) ); protected Map> getForbiddens() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/KeyValuePairProperties.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/KeyValuePairProperties.java new file mode 100644 index 0000000000..042527227d --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/KeyValuePairProperties.java @@ -0,0 +1,94 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashMap; + +/** + * @author Glavo + */ +public final class KeyValuePairProperties extends LinkedHashMap { + public static KeyValuePairProperties load(Path file) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(file)) { + return load(reader); + } + } + + public static KeyValuePairProperties load(BufferedReader reader) throws IOException { + KeyValuePairProperties result = new KeyValuePairProperties(); + + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("#")) + continue; + + int idx = line.indexOf('='); + if (idx <= 0) + continue; + + String name = line.substring(0, idx); + String value; + + if (line.length() > idx + 2 && line.charAt(idx + 1) == '"' && line.charAt(line.length() - 1) == '"') { + if (line.indexOf('\\', idx + 1) < 0) { + value = line.substring(idx + 2, line.length() - 1); + } else { + StringBuilder builder = new StringBuilder(); + for (int i = idx + 2, end = line.length() - 1; i < end; i++) { + char ch = line.charAt(i); + if (ch == '\\' && i < end - 1) { + char nextChar = line.charAt(++i); + switch (nextChar) { + case 'n': + builder.append('\n'); + break; + case 'r': + builder.append('\r'); + break; + case 't': + builder.append('\t'); + break; + case 'f': + builder.append('\f'); + break; + case 'b': + builder.append('\b'); + break; + default: + builder.append(nextChar); + break; + } + } else { + builder.append(ch); + } + } + value = builder.toString(); + } + } else { + value = line.substring(idx + 1); + } + + result.put(name, value); + } + return result; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java index fadf40bd21..ccd6bd1733 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/StringUtils.java @@ -377,15 +377,15 @@ public static String repeats(char ch, int repeat) { return result.toString(); } - public static int MAX_SHORT_STRING_LENGTH = 77; + public static String truncate(String str, int limit) { + assert limit > 5; - public static Optional truncate(String str) { - if (str.length() <= MAX_SHORT_STRING_LENGTH) { - return Optional.empty(); + if (str.length() <= limit) { + return str; } - final int halfLength = (MAX_SHORT_STRING_LENGTH - 5) / 2; - return Optional.of(str.substring(0, halfLength) + " ... " + str.substring(str.length() - halfLength)); + final int halfLength = (limit - 5) / 2; + return str.substring(0, halfLength) + " ... " + str.substring(str.length() - halfLength); } public static boolean isASCII(String cs) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index cbd5f88e25..bddf8d3f4a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -21,6 +21,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; import java.io.File; import java.io.IOException; @@ -73,6 +74,13 @@ public static T fromNonNullJson(String json, Type type) throws JsonParseExce return parsed; } + public static T fromNonNullJson(String json, TypeToken type) throws JsonParseException { + T parsed = GSON.fromJson(json, type); + if (parsed == null) + throw new JsonParseException("Json object cannot be null."); + return parsed; + } + public static T fromNonNullJsonFully(InputStream json, Class classOfT) throws IOException, JsonParseException { try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) { T parsed = GSON.fromJson(reader, classOfT); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java index fc9e265949..82830279b1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/IOUtils.java @@ -90,4 +90,21 @@ public static void copyTo(InputStream src, OutputStream dest, byte[] buf) throws public static InputStream wrapFromGZip(InputStream inputStream) throws IOException { return new GZIPInputStream(inputStream); } + + public static void closeQuietly(AutoCloseable closeable) { + try { + if (closeable != null) + closeable.close(); + } catch (Throwable ignored) { + } + } + + public static void closeQuietly(AutoCloseable closeable, Throwable exception) { + try { + if (closeable != null) + closeable.close(); + } catch (Throwable e) { + exception.addSuppressed(e); + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/Architecture.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/Architecture.java index 9f84eca094..bbfd0caf3f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/Architecture.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/Architecture.java @@ -176,6 +176,9 @@ public static Architecture parseArchName(String value) { return LOONGARCH64_OW; return LOONGARCH64; } + case "loongarch64_ow": { + return LOONGARCH64_OW; + } default: if (value.startsWith("armv7")) { return ARM32; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java deleted file mode 100644 index 0e1d82e45e..0000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java +++ /dev/null @@ -1,449 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 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.util.platform; - -import org.jackhuang.hmcl.download.java.JavaRepository; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jackhuang.hmcl.util.versioning.VersionNumber; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -import static java.util.stream.Collectors.toList; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -/** - * Represents a Java installation. - * - * @author huangyuhui - */ -public final class JavaVersion { - - private final Path binary; - private final String longVersion; - private final Platform platform; - private final int version; - private final VersionNumber versionNumber; - - public JavaVersion(Path binary, String longVersion, Platform platform) { - this.binary = binary; - this.longVersion = longVersion; - this.platform = platform; - - if (longVersion != null) { - version = parseVersion(longVersion); - versionNumber = VersionNumber.asVersion(longVersion); - } else { - version = UNKNOWN; - versionNumber = null; - } - } - - public String toString() { - return "JavaVersion {" + binary + ", " + longVersion + "(" + version + ")" + ", " + platform + "}"; - } - - public Path getBinary() { - return binary; - } - - public String getVersion() { - return longVersion; - } - - public Platform getPlatform() { - return platform; - } - - public Architecture getArchitecture() { - return platform.getArchitecture(); - } - - public Bits getBits() { - return platform.getBits(); - } - - public VersionNumber getVersionNumber() { - return versionNumber; - } - - /** - * The major version of Java installation. - * - * @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_9 - * @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_8 - * @see org.jackhuang.hmcl.util.platform.JavaVersion#JAVA_7 - * @see org.jackhuang.hmcl.util.platform.JavaVersion#UNKNOWN - */ - public int getParsedVersion() { - return version; - } - - private static final Pattern REGEX = Pattern.compile("version \"(?(.*?))\""); - private static final Pattern VERSION = Pattern.compile("^(?[0-9]+)"); - - private static final Pattern OS_ARCH = Pattern.compile("os\\.arch = (?.*)"); - private static final Pattern JAVA_VERSION = Pattern.compile("java\\.version = (?.*)"); - - private static final String JAVA_EXECUTABLE = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "java.exe" : "java"; - - public static final int UNKNOWN = -1; - public static final int JAVA_6 = 6; - public static final int JAVA_7 = 7; - public static final int JAVA_8 = 8; - public static final int JAVA_9 = 9; - public static final int JAVA_16 = 16; - public static final int JAVA_17 = 17; - - private static int parseVersion(String version) { - Matcher matcher = VERSION.matcher(version); - if (matcher.find()) { - int head = Lang.parseInt(matcher.group(), -1); - if (head > 1) return head; - } - if (version.contains("1.8")) - return JAVA_8; - else if (version.contains("1.7")) - return JAVA_7; - else if (version.contains("1.6")) - return JAVA_6; - else - return UNKNOWN; - } - - private static final Map fromExecutableCache = new ConcurrentHashMap<>(); - - public static JavaVersion fromExecutable(Path executable) throws IOException { - executable = executable.toRealPath(); - JavaVersion cachedJavaVersion = fromExecutableCache.get(executable); - if (cachedJavaVersion != null) - return cachedJavaVersion; - - String osArch = null; - String version = null; - - Platform platform = null; - - Process process = new ProcessBuilder(executable.toString(), "-XshowSettings:properties", "-version").start(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), OperatingSystem.NATIVE_CHARSET))) { - for (String line; (line = reader.readLine()) != null; ) { - Matcher m; - - m = OS_ARCH.matcher(line); - if (m.find()) { - osArch = m.group("arch"); - if (version != null) { - break; - } else { - continue; - } - } - - m = JAVA_VERSION.matcher(line); - if (m.find()) { - version = m.group("version"); - if (osArch != null) { - break; - } else { - //noinspection UnnecessaryContinue - continue; - } - } - } - } - - if (osArch != null) { - platform = Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.parseArchName(osArch)); - } - - if (version == null) { - boolean is64Bit = false; - process = new ProcessBuilder(executable.toString(), "-version").start(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream(), OperatingSystem.NATIVE_CHARSET))) { - for (String line; (line = reader.readLine()) != null; ) { - Matcher m = REGEX.matcher(line); - if (m.find()) - version = m.group("version"); - if (line.contains("64-Bit")) - is64Bit = true; - } - } - - if (platform == null) { - platform = Platform.getPlatform(OperatingSystem.CURRENT_OS, is64Bit ? Architecture.X86_64 : Architecture.X86); - } - } - - JavaVersion javaVersion = new JavaVersion(executable, version, platform); - if (javaVersion.getParsedVersion() == UNKNOWN) - throw new IOException("Unrecognized Java version " + version + " at " + executable); - fromExecutableCache.put(executable, javaVersion); - return javaVersion; - } - - public static Path getExecutable(Path javaHome) { - return javaHome.resolve("bin").resolve(JAVA_EXECUTABLE); - } - - public static JavaVersion fromCurrentEnvironment() { - return CURRENT_JAVA; - } - - public static final JavaVersion CURRENT_JAVA; - - static { - Path currentExecutable = getExecutable(Paths.get(System.getProperty("java.home")).toAbsolutePath()); - try { - currentExecutable = currentExecutable.toRealPath(); - } catch (IOException e) { - LOG.warning("Failed to resolve current Java path: " + currentExecutable, e); - } - CURRENT_JAVA = new JavaVersion( - currentExecutable, - System.getProperty("java.version"), - Platform.CURRENT_PLATFORM - ); - } - - private static Collection JAVAS; - private static final CountDownLatch LATCH = new CountDownLatch(1); - - public static Collection getJavas() throws InterruptedException { - if (JAVAS != null) - return JAVAS; - LATCH.await(); - return JAVAS; - } - - public static synchronized void initialize() { - if (JAVAS != null) - throw new IllegalStateException("JavaVersions have already been initialized."); - - List javaVersions; - - try (Stream stream = searchPotentialJavaExecutables()) { - javaVersions = lookupJavas(stream); - } catch (IOException e) { - LOG.warning("Failed to search Java homes", e); - javaVersions = new ArrayList<>(); - } - - // insert current java to the list - if (!javaVersions.contains(CURRENT_JAVA)) { - javaVersions.add(CURRENT_JAVA); - } - - JAVAS = Collections.newSetFromMap(new ConcurrentHashMap<>()); - JAVAS.addAll(javaVersions); - - LOG.trace("Finished Java installation lookup, found " + JAVAS.size()); - - LATCH.countDown(); - } - - private static List lookupJavas(Stream javaExecutables) { - return javaExecutables - .filter(Files::isExecutable) - .flatMap(executable -> { // resolve symbolic links - try { - return Stream.of(executable.toRealPath()); - } catch (IOException e) { - LOG.warning("Failed to lookup Java executable at " + executable, e); - return Stream.empty(); - } - }) - .distinct() // remove duplicated javas - .flatMap(executable -> { - if (executable.equals(CURRENT_JAVA.getBinary())) { - return Stream.of(CURRENT_JAVA); - } - try { - LOG.trace("Looking for Java:" + executable); - Future future = Schedulers.io().submit(() -> fromExecutable(executable)); - JavaVersion javaVersion = future.get(5, TimeUnit.SECONDS); - LOG.trace("Found Java (" + javaVersion.getVersion() + ") " + javaVersion.getBinary().toString()); - return Stream.of(javaVersion); - } catch (ExecutionException | InterruptedException | TimeoutException e) { - LOG.warning("Failed to determine Java at " + executable, e); - return Stream.empty(); - } - }) - .collect(toList()); - } - - private static Stream searchPotentialJavaExecutables() throws IOException { - // Add order: - // 1. System-defined locations - // 2. Minecraft-installed locations - // 3. PATH - List> javaExecutables = new ArrayList<>(); - - switch (OperatingSystem.CURRENT_OS) { - case WINDOWS: - javaExecutables.add(queryJavaHomesInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\").stream().map(JavaVersion::getExecutable)); - javaExecutables.add(queryJavaHomesInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\").stream().map(JavaVersion::getExecutable)); - javaExecutables.add(queryJavaHomesInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\").stream().map(JavaVersion::getExecutable)); - javaExecutables.add(queryJavaHomesInRegistryKey("HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\").stream().map(JavaVersion::getExecutable)); - - for (Optional programFiles : Arrays.asList( - FileUtils.tryGetPath(Optional.ofNullable(System.getenv("ProgramFiles")).orElse("C:\\Program Files")), - FileUtils.tryGetPath(Optional.ofNullable(System.getenv("ProgramFiles(x86)")).orElse("C:\\Program Files (x86)")), - FileUtils.tryGetPath(Optional.ofNullable(System.getenv("ProgramFiles(ARM)")).orElse("C:\\Program Files (ARM)")) - )) { - if (!programFiles.isPresent()) - continue; - - for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) { - javaExecutables.add(listDirectory(programFiles.get().resolve(vendor)).map(JavaVersion::getExecutable)); - } - } - break; - - case LINUX: - case FREEBSD: - javaExecutables.add(listDirectory(Paths.get("/usr/java")).map(JavaVersion::getExecutable)); // Oracle RPMs - javaExecutables.add(listDirectory(Paths.get("/usr/lib/jvm")).map(JavaVersion::getExecutable)); // General locations - javaExecutables.add(listDirectory(Paths.get("/usr/lib32/jvm")).map(JavaVersion::getExecutable)); // General locations - javaExecutables.add(listDirectory(Paths.get(System.getProperty("user.home"), ".sdkman/candidates/java")).map(JavaVersion::getExecutable)); // SDKMAN! - break; - - case OSX: - javaExecutables.add(listDirectory(Paths.get("/Library/Java/JavaVirtualMachines")) - .flatMap(dir -> Stream.of(dir.resolve("Contents/Home"), dir.resolve("Contents/Home/jre"))) - .map(JavaVersion::getExecutable)); - javaExecutables.add(listDirectory(Paths.get(System.getProperty("user.home"), "Library/Java/JavaVirtualMachines")) - .flatMap(dir -> Stream.of(dir.resolve("Contents/Home"), dir.resolve("Contents/Home/jre"))) - .map(JavaVersion::getExecutable)); - javaExecutables.add(listDirectory(Paths.get("/System/Library/Java/JavaVirtualMachines")) - .map(dir -> dir.resolve("Contents/Home")) - .map(JavaVersion::getExecutable)); - javaExecutables.add(Stream.of(Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"))); - javaExecutables.add(Stream.of(Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"))); - // Homebrew - javaExecutables.add(Stream.of(Paths.get("/opt/homebrew/opt/java/bin/java"))); - javaExecutables.add(listDirectory(Paths.get("/opt/homebrew/Cellar/openjdk")) - .map(JavaVersion::getExecutable)); - break; - - default: - break; - } - - // Search Minecraft bundled runtimes. - javaExecutables.add(Stream.concat(Stream.of(Optional.of(JavaRepository.getJavaStoragePath())), JavaRepository.findMinecraftRuntimeDirs()) - .flatMap(Lang::toStream) - .flatMap(JavaRepository::findJavaHomeInMinecraftRuntimeDir) - .map(JavaVersion::getExecutable)); - - // Search in PATH. - if (System.getenv("PATH") != null) { - javaExecutables.add(Arrays.stream(System.getenv("PATH").split(OperatingSystem.PATH_SEPARATOR)) - .flatMap(path -> Lang.toStream(FileUtils.tryGetPath(path, JAVA_EXECUTABLE)))); - } - - // Search in HMCL_JRES, convenient environment variable for users to add JRE in global - // May be removed when we implement global Java configuration. - if (System.getenv("HMCL_JRES") != null) { - javaExecutables.add(Arrays.stream(System.getenv("HMCL_JRES").split(OperatingSystem.PATH_SEPARATOR)) - .flatMap(path -> Lang.toStream(FileUtils.tryGetPath(path, "bin", JAVA_EXECUTABLE)))); - } - return javaExecutables.parallelStream().flatMap(stream -> stream); - } - - private static Stream listDirectory(Path directory) throws IOException { - if (Files.isDirectory(directory)) { - try (final DirectoryStream subDirs = Files.newDirectoryStream(directory)) { - final ArrayList paths = new ArrayList<>(); - for (Path subDir : subDirs) { - paths.add(subDir); - } - return paths.stream(); - } - } else { - return Stream.empty(); - } - } - - // ==== Windows Registry Support ==== - private static List queryJavaHomesInRegistryKey(String location) throws IOException { - List homes = new ArrayList<>(); - for (String java : querySubFolders(location)) { - if (!querySubFolders(java).contains(java + "\\MSI")) - continue; - String home = queryRegisterValue(java, "JavaHome"); - if (home != null) { - try { - homes.add(Paths.get(home)); - } catch (InvalidPathException e) { - LOG.warning("Invalid Java path in system registry: " + home); - } - } - } - return homes; - } - - private static List querySubFolders(String location) throws IOException { - List res = new ArrayList<>(); - - Process process = Runtime.getRuntime().exec(new String[] { "cmd", "/c", "reg", "query", location }); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) { - for (String line; (line = reader.readLine()) != null;) { - if (line.startsWith(location) && !line.equals(location)) { - res.add(line); - } - } - } - return res; - } - - private static String queryRegisterValue(String location, String name) throws IOException { - boolean last = false; - Process process = Runtime.getRuntime().exec(new String[] { "cmd", "/c", "reg", "query", location, "/v", name }); - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) { - for (String line; (line = reader.readLine()) != null;) { - if (StringUtils.isNotBlank(line)) { - if (last && line.trim().startsWith(name)) { - int begins = line.indexOf(name); - if (begins > 0) { - String s2 = line.substring(begins + name.length()); - begins = s2.indexOf("REG_SZ"); - if (begins > 0) { - return s2.substring(begins + "REG_SZ".length()).trim(); - } - } - } - if (location.equals(line.trim())) { - last = true; - } - } - } - } - return null; - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/ManagedProcess.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/ManagedProcess.java index 85bba1eb7a..07ca5a5165 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/ManagedProcess.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/ManagedProcess.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.util.platform; +import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.launch.StreamPump; import org.jackhuang.hmcl.util.Lang; @@ -90,7 +91,7 @@ public Process getProcess() { * @return PID */ public long getPID() throws UnsupportedOperationException { - if (JavaVersion.CURRENT_JAVA.getParsedVersion() >= 9) { + if (JavaRuntime.CURRENT_VERSION >= 9) { // Method Process.pid() is provided (Java 9 or later). Invoke it to get the pid. try { return (long) MethodHandles.publicLookup() diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java index 145a934fd1..9d9ca05286 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java @@ -74,6 +74,10 @@ public boolean isLinuxOrBSD() { return this == LINUX || this == FREEBSD; } + public String getJavaExecutable() { + return this == WINDOWS ? "java.exe" : "java"; + } + /** * The current operating system. */ @@ -215,10 +219,10 @@ public static OperatingSystem parseOSName(String name) { name = name.trim().toLowerCase(Locale.ROOT); - if (name.contains("win")) - return WINDOWS; - else if (name.contains("mac")) + if (name.contains("mac") || name.contains("darwin") || name.contains("osx")) return OSX; + else if (name.contains("win")) + return WINDOWS; else if (name.contains("solaris") || name.contains("linux") || name.contains("unix") || name.contains("sunos")) return LINUX; else if (name.equals("freebsd")) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/Platform.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/Platform.java index 56bc7b0eda..7ad533ceec 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/Platform.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/Platform.java @@ -5,6 +5,7 @@ public final class Platform { public static final Platform UNKNOWN = new Platform(OperatingSystem.UNKNOWN, Architecture.UNKNOWN); + public static final Platform WINDOWS_X86 = new Platform(OperatingSystem.WINDOWS, Architecture.X86); public static final Platform WINDOWS_X86_64 = new Platform(OperatingSystem.WINDOWS, Architecture.X86_64); public static final Platform WINDOWS_ARM64 = new Platform(OperatingSystem.WINDOWS, Architecture.ARM64); @@ -78,6 +79,10 @@ public int hashCode() { return Objects.hash(os, arch); } + public boolean equals(OperatingSystem os, Architecture arch) { + return this.os == os && this.arch == arch; + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/SystemUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/SystemUtils.java index 92e74708f2..d5e956ced1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/SystemUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/SystemUtils.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.util.platform; +import org.jackhuang.hmcl.java.JavaRuntime; + import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -42,8 +44,7 @@ public static int callExternalProcess(ProcessBuilder processBuilder) throws IOEx } public static boolean supportJVMAttachment() { - return JavaVersion.CURRENT_JAVA.getParsedVersion() >= 9 - && Thread.currentThread().getContextClassLoader().getResource("com/sun/tools/attach/VirtualMachine.class") != null; + return JavaRuntime.CURRENT_VERSION >= 9 && Thread.currentThread().getContextClassLoader().getResource("com/sun/tools/attach/VirtualMachine.class") != null; } private static void onLogLine(String log) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/UnsupportedPlatformException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/UnsupportedPlatformException.java new file mode 100644 index 0000000000..4e9552d586 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/UnsupportedPlatformException.java @@ -0,0 +1,30 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.util.platform; + +/** + * @author Glavo + */ +public final class UnsupportedPlatformException extends Exception { + public UnsupportedPlatformException() { + } + + public UnsupportedPlatformException(String message) { + super(message); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java new file mode 100644 index 0000000000..ff744e79fd --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ArchiveFileTree.java @@ -0,0 +1,129 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.util.tree; + +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.*; + +/** + * @author Glavo + */ +public abstract class ArchiveFileTree implements Closeable { + + public static ArchiveFileTree open(Path file) throws IOException { + Path namePath = file.getFileName(); + if (namePath == null) { + throw new IOException(file + " is not a valid archive file"); + } + + String name = namePath.toString(); + if (name.endsWith(".jar") || name.endsWith(".zip")) { + return new ZipFileTree(new ZipFile(file)); + } else if (name.endsWith(".tar") || name.endsWith(".tar.gz") || name.endsWith(".tgz")) { + return TarFileTree.open(file); + } else { + throw new IOException(file + " is not a valid archive file"); + } + } + + protected final F file; + protected final Dir root = new Dir<>(); + + public ArchiveFileTree(F file) { + this.file = file; + } + + public F getFile() { + return file; + } + + public Dir getRoot() { + return root; + } + + public void addEntry(E entry) throws IOException { + String[] path = entry.getName().split("/"); + + Dir dir = root; + + for (int i = 0, end = entry.isDirectory() ? path.length : path.length - 1; i < end; i++) { + String item = path[i]; + if (item.equals(".")) + continue; + if (item.equals("..") || item.isEmpty()) + throw new IOException("Invalid entry: " + entry.getName()); + + if (dir.files.containsKey(item)) { + throw new IOException("A file and a directory have the same name: " + entry.getName()); + } + + dir = dir.subDirs.computeIfAbsent(item, name -> new Dir<>()); + } + + if (entry.isDirectory()) { + if (dir.entry != null) { + throw new IOException("Duplicate entry: " + entry.getName()); + } + dir.entry = entry; + } else { + String fileName = path[path.length - 1]; + + if (dir.subDirs.containsKey(fileName)) { + throw new IOException("A file and a directory have the same name: " + entry.getName()); + } + + if (dir.files.containsKey(fileName)) { + throw new IOException("Duplicate entry: " + entry.getName()); + } + + dir.files.put(fileName, entry); + } + } + + public abstract InputStream getInputStream(E entry) throws IOException; + + public abstract boolean isLink(E entry); + + public abstract String getLink(E entry) throws IOException; + + public abstract boolean isExecutable(E entry); + + @Override + public abstract void close() throws IOException; + + public static final class Dir { + E entry; + + final Map> subDirs = new HashMap<>(); + final Map files = new HashMap<>(); + + public Map> getSubDirs() { + return subDirs; + } + + public Map getFiles() { + return files; + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/TarFileTree.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/TarFileTree.java new file mode 100644 index 0000000000..bffa40eefa --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/TarFileTree.java @@ -0,0 +1,133 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.util.tree; + +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import org.apache.commons.compress.archivers.tar.TarFile; +import org.jackhuang.hmcl.util.io.IOUtils; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.zip.GZIPInputStream; + +/** + * @author Glavo + */ +public final class TarFileTree extends ArchiveFileTree { + + public static TarFileTree open(Path file) throws IOException { + String fileName = file.getFileName().toString(); + + if (fileName.endsWith(".tar.gz") || fileName.endsWith(".tgz")) { + Path tempFile = Files.createTempFile("hmcl-", ".tar"); + TarFile tarFile; + try (GZIPInputStream input = new GZIPInputStream(Files.newInputStream(file)); + OutputStream output = Files.newOutputStream(tempFile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE) + ) { + IOUtils.copyTo(input, output); + tarFile = new TarFile(tempFile.toFile()); + } catch (Throwable e) { + try { + Files.deleteIfExists(tempFile); + } catch (Throwable e2) { + e.addSuppressed(e2); + } + throw e; + } + + return new TarFileTree(tarFile, tempFile); + } else { + return new TarFileTree(new TarFile(file), null); + } + } + + private final Path tempFile; + private final Thread shutdownHook; + + public TarFileTree(TarFile file, Path tempFile) throws IOException { + super(file); + this.tempFile = tempFile; + try { + for (TarArchiveEntry entry : file.getEntries()) { + addEntry(entry); + } + } catch (Throwable e) { + try { + file.close(); + } catch (Throwable e2) { + e.addSuppressed(e2); + } + + if (tempFile != null) { + try { + Files.deleteIfExists(tempFile); + } catch (Throwable e2) { + e.addSuppressed(e2); + } + } + + throw e; + } + + if (tempFile != null) { + this.shutdownHook = new Thread(() -> { + try { + Files.deleteIfExists(tempFile); + } catch (Throwable ignored) { + } + }); + Runtime.getRuntime().addShutdownHook(shutdownHook); + } else + this.shutdownHook = null; + } + + @Override + public InputStream getInputStream(TarArchiveEntry entry) throws IOException { + return file.getInputStream(entry); + } + + @Override + public boolean isLink(TarArchiveEntry entry) { + return entry.isSymbolicLink(); + } + + @Override + public String getLink(TarArchiveEntry entry) throws IOException { + return entry.getLinkName(); + } + + @Override + public boolean isExecutable(TarArchiveEntry entry) { + return entry.isFile() && (entry.getMode() & 0b1000000) != 0; + } + + @Override + public void close() throws IOException { + try { + file.close(); + } finally { + if (tempFile != null) { + Runtime.getRuntime().removeShutdownHook(shutdownHook); + Files.deleteIfExists(tempFile); + } + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ZipFileTree.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ZipFileTree.java new file mode 100644 index 0000000000..70e2d3a826 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/tree/ZipFileTree.java @@ -0,0 +1,72 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 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.util.tree; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; + +/** + * @author Glavo + */ +public final class ZipFileTree extends ArchiveFileTree { + public ZipFileTree(ZipFile file) throws IOException { + super(file); + try { + Enumeration entries = file.getEntries(); + while (entries.hasMoreElements()) { + addEntry(entries.nextElement()); + } + } catch (Throwable e) { + try { + file.close(); + } catch (Throwable e2) { + e.addSuppressed(e2); + } + throw e; + } + } + + @Override + public void close() throws IOException { + file.close(); + } + + @Override + public InputStream getInputStream(ZipArchiveEntry entry) throws IOException { + return getFile().getInputStream(entry); + } + + @Override + public boolean isLink(ZipArchiveEntry entry) { + return entry.isUnixSymlink(); + } + + @Override + public String getLink(ZipArchiveEntry entry) throws IOException { + return getFile().getUnixSymlink(entry); + } + + @Override + public boolean isExecutable(ZipArchiveEntry entry) { + return !entry.isDirectory() && !entry.isUnixSymlink() && (entry.getUnixMode() & 0b1000000) != 0; + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/KeyValuePairPropertiesTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/KeyValuePairPropertiesTest.java new file mode 100644 index 0000000000..5a04f7cb9c --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/KeyValuePairPropertiesTest.java @@ -0,0 +1,31 @@ +package org.jackhuang.hmcl.util; + +import org.junit.jupiter.api.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; + +import static org.jackhuang.hmcl.util.Pair.pair; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Glavo + */ +public final class KeyValuePairPropertiesTest { + @Test + public void test() throws IOException { + String content = "#test: key0=value0\n \n" + + "key1=value1\n" + + "key2=\"value2\"\n" + + "key3=\"\\\" \\n\"\n"; + + KeyValuePairProperties properties = KeyValuePairProperties.load(new BufferedReader(new StringReader(content))); + + assertEquals(Lang.mapOf( + pair("key1", "value1"), + pair("key2", "value2"), + pair("key3", "\" \n") + ), properties); + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TaskTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TaskTest.java index eb3482a4c9..91ebcb635f 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TaskTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/TaskTest.java @@ -20,7 +20,6 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; -import org.jackhuang.hmcl.util.platform.JavaVersion; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIf; @@ -79,10 +78,11 @@ public void testWithCompose() { @EnabledIf("org.jackhuang.hmcl.JavaFXLauncher#isStarted") public void testThenAccept() { AtomicBoolean flag = new AtomicBoolean(); - boolean result = Task.supplyAsync(JavaVersion::fromCurrentEnvironment) - .thenAcceptAsync(Schedulers.io(), javaVersion -> { + Object obj = new Object(); + boolean result = Task.supplyAsync(() -> obj) + .thenAcceptAsync(Schedulers.io(), o -> { flag.set(true); - assertEquals(javaVersion, JavaVersion.fromCurrentEnvironment()); + assertSame(obj, o); }) .test(); diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/JavaRuntimeTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/JavaRuntimeTest.java new file mode 100644 index 0000000000..00065986c2 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/platform/JavaRuntimeTest.java @@ -0,0 +1,18 @@ +package org.jackhuang.hmcl.util.platform; + +import org.junit.jupiter.api.Test; + +import static org.jackhuang.hmcl.java.JavaInfo.parseVersion; +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Glavo + */ +public final class JavaRuntimeTest { + @Test + public void testParseVersion() { + assertEquals(8, parseVersion("1.8.0_302")); + assertEquals(11, parseVersion("11")); + assertEquals(11, parseVersion("11.0.12")); + } +} From b624cebc081fb64cc000e953f4e78e3319b4e47a Mon Sep 17 00:00:00 2001 From: ShulkerSakura <50770360+ShulkerSakura@users.noreply.github.com> Date: Sun, 6 Oct 2024 23:21:12 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=8C=89=E9=92=AE=E5=90=8D=E7=A7=B0=20(#3262?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/DownloadPage.java | 4 ++-- HMCL/src/main/resources/assets/lang/I18N.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 ++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index 200bfee6b5..d16124206f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -460,7 +460,7 @@ public ModVersion(RemoteMod.Version version, DownloadPage selfPage) { this.setBody(box); - JFXButton downloadButton = new JFXButton(i18n("download")); + JFXButton downloadButton = new JFXButton(i18n("mods.install")); downloadButton.getStyleClass().add("dialog-accept"); downloadButton.setOnAction(e -> { if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) { @@ -469,7 +469,7 @@ public ModVersion(RemoteMod.Version version, DownloadPage selfPage) { selfPage.download(version); }); - JFXButton saveAsButton = new JFXButton(i18n("button.save_as")); + JFXButton saveAsButton = new JFXButton(i18n("mods.save_as")); saveAsButton.getStyleClass().add("dialog-accept"); saveAsButton.setOnAction(e -> { if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 5515c3da13..195c935006 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -965,6 +965,8 @@ mods.not_modded=You must install a mod loader (Fabric, Forge, Quilt or LiteLoade mods.restore=Rollback mods.url=Official Page mods.update_modpack_mod.warning=Updating mods in a modpack can lead to irreparable results, possibly corrupting the modpack so that it cannot start. Are you sure you want to update? +mods.install=Install +mods.save_as=Save As nbt.entries=%s entries nbt.open.failed=Fail to open file diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 3e99c32a43..a8c474fdd3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -833,6 +833,8 @@ mods.not_modded=你需要先在自動安裝頁面安裝 Fabric、Forge、Quilt mods.restore=回退 mods.url=官方頁面 mods.update_modpack_mod.warning=更新模組包中的 Mod 可能導致綜合包損壞,使模組包無法正常啟動。該操作不可逆,確定要更新嗎? +mods.install=安裝到當前實例 +mods.save_as=下載到本地目錄 nbt.entries=%s 個條目 nbt.open.failed=打開檔案失敗 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 27e2d0c2f1..442513ab26 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -832,6 +832,8 @@ mods.not_modded=你需要先在自动安装页面安装 Fabric、Forge 或 LiteL mods.restore=回退 mods.url=官方页面 mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗? +mods.install=安装到当前版本 +mods.save_as=下载到本地目录 nbt.entries=%s 个条目 nbt.open.failed=打开文件失败 From d3e9511acae1a7f5373306e5ed48693853b9875d Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 6 Oct 2024 23:31:19 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E5=88=A0=E9=99=A4=20FractureiserDetector?= =?UTF-8?q?=20(#3313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/Main.java | 9 --- .../hmcl/util/FractureiserDetector.java | 57 ------------------- 2 files changed, 66 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/util/FractureiserDetector.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java index 6009288c74..ff39601f7b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Main.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Main.java @@ -20,7 +20,6 @@ import javafx.application.Platform; import javafx.scene.control.Alert; import org.jackhuang.hmcl.ui.AwtUtils; -import org.jackhuang.hmcl.util.FractureiserDetector; import org.jackhuang.hmcl.util.SelfDependencyPatcher; import org.jackhuang.hmcl.ui.SwingUtils; import org.jackhuang.hmcl.java.JavaRuntime; @@ -70,7 +69,6 @@ public static void main(String[] args) { checkJavaFX(); verifyJavaFX(); - detectFractureiser(); Launcher.main(args); } @@ -94,13 +92,6 @@ private static void checkDirectoryPath() { } } - private static void detectFractureiser() { - if (FractureiserDetector.detect()) { - LOG.error("Detected that this computer is infected by fractureiser"); - showErrorAndExit(i18n("fatal.fractureiser")); - } - } - private static void checkJavaFX() { try { SelfDependencyPatcher.patch(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/FractureiserDetector.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/FractureiserDetector.java deleted file mode 100644 index 01a4370303..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/FractureiserDetector.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.jackhuang.hmcl.util; - -import org.jackhuang.hmcl.util.platform.OperatingSystem; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -/** - * @see fractureiser-investigation/fractureiser - * @see [MALWARE WARNING] "fractureiser" malware in many popular Minecraft mods and modpacks - */ -public final class FractureiserDetector { - private FractureiserDetector() { - } - - private static final class FractureiserException extends Exception { - } - - public static boolean detect() { - try { - if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { - Path appdata = Paths.get(System.getProperty("user.home"), "AppData"); - if (Files.isDirectory(appdata)) { - check(appdata.resolve("Roaming\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\run.bat")); - - Path falseEdgePath = appdata.resolve("Local\\Microsoft Edge"); - if (Files.exists(falseEdgePath)) { - check(falseEdgePath.resolve(".ref")); - check(falseEdgePath.resolve("client.jar")); - check(falseEdgePath.resolve("lib.dll")); - check(falseEdgePath.resolve("libWebGL64.jar")); - check(falseEdgePath.resolve("run.bat")); - } - } - } else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) { - Path dataDir = Paths.get(System.getProperty("user.home"), ".config", ".data"); - if (Files.exists(dataDir)) { - check(dataDir.resolve(".ref")); - check(dataDir.resolve("client.jar")); - check(dataDir.resolve("lib.jar")); - } - } - } catch (FractureiserException e) { - return true; - } catch (Throwable ignored) { - } - - return false; - } - - private static void check(Path path) throws FractureiserException { - if (Files.isRegularFile(path)) { - throw new FractureiserException(); - } - } -} From 26224ae3665af5eb72f2069bbff8cc86e9849bdd Mon Sep 17 00:00:00 2001 From: Zkitefly <2573874409@qq.com> Date: Sun, 6 Oct 2024 23:38:48 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E5=88=A0=E9=99=A4=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E5=92=8C=E5=88=A0=E9=99=A4=E8=AE=A4=E8=AF=81=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E6=97=B6=E5=BC=B9=E5=87=BA=E6=8F=90=E7=A4=BA=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E5=88=A0=E9=99=A4=20(#3280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/account/AccountListItemSkin.java | 2 +- .../java/org/jackhuang/hmcl/ui/account/AccountListPage.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java index 2e69fbc17e..bb99d8af4d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListItemSkin.java @@ -177,7 +177,7 @@ public void fire() { right.getChildren().add(spinnerCopyUUID); JFXButton btnRemove = new JFXButton(); - btnRemove.setOnMouseClicked(e -> skinnable.remove()); + btnRemove.setOnMouseClicked(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null)); btnRemove.getStyleClass().add("toggle-icon4"); BorderPane.setAlignment(btnRemove, Pos.CENTER); btnRemove.setGraphic(SVG.DELETE.createIcon(Theme.blackFill(), -1, -1)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListPage.java index ddfcfa056a..253ca699b3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/AccountListPage.java @@ -123,7 +123,9 @@ public AccountListPageSkin(AccountListPage skinnable) { JFXButton btnRemove = new JFXButton(); btnRemove.setOnAction(e -> { - skinnable.authServersProperty().remove(server); + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { + skinnable.authServersProperty().remove(server); + }, null); e.consume(); }); btnRemove.getStyleClass().add("toggle-icon4"); From 4c1e607b8e5fb0424e828902dca825ae315ee448 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 7 Oct 2024 00:57:35 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E5=9C=A8=20Linux/FreeBSD=20=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E8=AF=BB=E5=8F=96=20os-release=20(#3314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 简化 OperatingSystem.getPhysicalMemoryStatus * Read os-release * update * update --- .../java/org/jackhuang/hmcl/Launcher.java | 4 +- .../hmcl/game/HMCLGameRepository.java | 2 +- .../jackhuang/hmcl/ui/GameCrashWindow.java | 9 ++-- .../hmcl/ui/versions/VersionSettingsPage.java | 2 +- .../hmcl/util/platform/OperatingSystem.java | 41 ++++++++++++++----- 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index 494dda7b3f..4bd9c28385 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -222,7 +222,9 @@ public static void main(String[] args) { try { LOG.info("*** " + Metadata.TITLE + " ***"); - LOG.info("Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION); + LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null + ? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + : OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')')); LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME); LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME); LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor")); 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 861301bf26..db71cb536d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -396,7 +396,7 @@ public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, F .setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs())) .setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory( vs.getMaxMemory() * 1024L * 1024L, - OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(), + OperatingSystem.getPhysicalMemoryStatus().getAvailable(), vs.isAutoMemory() ) / 1024 / 1024)) .setMinMemory(vs.getMinMemory()) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java index 653dd6d3f9..a93e073e9a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -20,8 +20,6 @@ import com.jfoenix.controls.JFXButton; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; @@ -43,6 +41,7 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Log4jLevel; import org.jackhuang.hmcl.util.logging.Logger; import org.jackhuang.hmcl.util.Pair; @@ -77,8 +76,6 @@ public class GameCrashWindow extends Stage { private final String total_memory; private final String java; private final LibraryAnalyzer analyzer; - private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME); - private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName()); private final TextFlow reasonTextFlow = new TextFlow(new Text(i18n("game.crash.reason.unknown"))); private final BooleanProperty loading = new SimpleBooleanProperty(); private final TextFlow feedbackTextFlow = new TextFlow(); @@ -356,12 +353,12 @@ private final class View extends VBox { TwoLineListItem os = new TwoLineListItem(); os.getStyleClass().setAll("two-line-item-second-large"); os.setTitle(i18n("system.operating_system")); - os.subtitleProperty().bind(GameCrashWindow.this.os); + os.setSubtitle(Lang.requireNonNullElse(OperatingSystem.OS_RELEASE_NAME, OperatingSystem.SYSTEM_NAME)); TwoLineListItem arch = new TwoLineListItem(); arch.getStyleClass().setAll("two-line-item-second-large"); arch.setTitle(i18n("system.architecture")); - arch.subtitleProperty().bind(GameCrashWindow.this.arch); + arch.setSubtitle(Architecture.SYSTEM_ARCH.getDisplayName()); infoPane.getChildren().setAll(launcher, version, total_memory, memory, java, os, arch); } 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 337d24ae01..6fcf3c0de0 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 @@ -457,7 +457,7 @@ public VersionSettingsPage(boolean globalSetting) { } private void initialize() { - memoryStatus.set(OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID)); + memoryStatus.set(OperatingSystem.getPhysicalMemoryStatus()); enableSpecificSettings.addListener((a, b, newValue) -> { if (versionId == null) return; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java index 9d9ca05286..70a32512ee 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/OperatingSystem.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.util.platform; +import org.jackhuang.hmcl.util.KeyValuePairProperties; + import java.io.BufferedReader; import java.io.File; import java.io.IOException; @@ -28,8 +30,9 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; +import java.util.Collections; import java.util.Locale; -import java.util.Optional; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -118,6 +121,9 @@ public String getJavaExecutable() { */ public static final String SYSTEM_VERSION; + public static final String OS_RELEASE_NAME; + public static final String OS_RELEASE_PRETTY_NAME; + public static final Pattern INVALID_RESOURCE_CHARACTERS; private static final String[] INVALID_RESOURCE_BASENAMES; private static final String[] INVALID_RESOURCE_FULLNAMES; @@ -144,7 +150,7 @@ public String getJavaExecutable() { } } } catch (UnsupportedCharsetException e) { - e.printStackTrace(); + e.printStackTrace(System.err); } NATIVE_CHARSET = nativeCharset; @@ -187,9 +193,24 @@ public String getJavaExecutable() { SYSTEM_BUILD_NUMBER = -1; } - TOTAL_MEMORY = getPhysicalMemoryStatus() - .map(physicalMemoryStatus -> (int) (physicalMemoryStatus.getTotal() / 1024 / 1024)) - .orElse(1024); + Map osRelease = Collections.emptyMap(); + if (CURRENT_OS == LINUX || CURRENT_OS == FREEBSD) { + Path osReleaseFile = Paths.get("/etc/os-release"); + if (Files.exists(osReleaseFile)) { + try { + osRelease = KeyValuePairProperties.load(osReleaseFile); + } catch (IOException e) { + e.printStackTrace(System.err); + } + } + } + OS_RELEASE_NAME = osRelease.get("NAME"); + OS_RELEASE_PRETTY_NAME = osRelease.get("PRETTY_NAME"); + + PhysicalMemoryStatus physicalMemoryStatus = getPhysicalMemoryStatus(); + TOTAL_MEMORY = physicalMemoryStatus != PhysicalMemoryStatus.INVALID + ? (int) (physicalMemoryStatus.getTotal() / 1024 / 1024) + : 1024; SUGGESTED_MEMORY = TOTAL_MEMORY >= 32768 ? 8192 : (int) (Math.round(1.0 * TOTAL_MEMORY / 4.0 / 128.0) * 128); @@ -256,7 +277,7 @@ public static boolean isWindows7OrLater() { } @SuppressWarnings("deprecation") - public static Optional getPhysicalMemoryStatus() { + public static PhysicalMemoryStatus getPhysicalMemoryStatus() { if (CURRENT_OS == LINUX) { try { long free = 0, available = 0, total = 0; @@ -277,10 +298,10 @@ public static Optional getPhysicalMemoryStatus() { } } if (total > 0) { - return Optional.of(new PhysicalMemoryStatus(total, available > 0 ? available : free)); + return new PhysicalMemoryStatus(total, available > 0 ? available : free); } } catch (IOException e) { - e.printStackTrace(); + e.printStackTrace(System.err); } } @@ -290,11 +311,11 @@ public static Optional getPhysicalMemoryStatus() { com.sun.management.OperatingSystemMXBean sunBean = (com.sun.management.OperatingSystemMXBean) java.lang.management.ManagementFactory.getOperatingSystemMXBean(); - return Optional.of(new PhysicalMemoryStatus(sunBean.getTotalPhysicalMemorySize(), sunBean.getFreePhysicalMemorySize())); + return new PhysicalMemoryStatus(sunBean.getTotalPhysicalMemorySize(), sunBean.getFreePhysicalMemorySize()); } } catch (NoClassDefFoundError ignored) { } - return Optional.empty(); + return PhysicalMemoryStatus.INVALID; } @SuppressWarnings("removal")