From e9a1ad649f94b0dcfe4945fef85da4ec24399ab6 Mon Sep 17 00:00:00 2001 From: OctopusCat101 Date: Mon, 13 Jul 2026 21:24:13 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89java=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E4=B8=8B=E8=BD=BD=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/java/JavaManager.java | 72 +++++++++++++++---- .../hmcl/setting/LauncherSettings.java | 33 +++++++++ .../org/jackhuang/hmcl/ui/Controllers.java | 6 ++ .../hmcl/ui/main/DownloadSettingsPage.java | 26 ++++++- .../resources/assets/lang/I18N.properties | 4 ++ .../resources/assets/lang/I18N_zh.properties | 4 ++ .../assets/lang/I18N_zh_CN.properties | 4 ++ 7 files changed, 134 insertions(+), 15 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java index 3d1c11b8bb4..654caec7a16 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java @@ -46,7 +46,9 @@ import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException; import org.jackhuang.hmcl.util.platform.windows.WinReg; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import java.io.BufferedReader; import java.io.File; @@ -63,15 +65,18 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -/** - * @author Glavo - */ +/// Detects, selects, installs, and removes Java runtimes used by HMCL. +/// +/// @author Glavo +@NotNullByDefault public final class JavaManager { + /// Prevents construction of this utility class. private JavaManager() { } - private static final String[] KNOWN_VENDOR_DIRECTORIES = { + /// Vendor directory names searched in conventional Java installation locations. + private static final String @Unmodifiable [] KNOWN_VENDOR_DIRECTORIES = { "Java", "BellSoft", "AdoptOpenJDK", @@ -81,9 +86,18 @@ private JavaManager() { "Semeru" }; + /// Default repository for Java runtimes managed under the HMCL user data directory. public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_USER_HOME.resolve("java")); + + /// Repository for Java runtimes bundled under the current HMCL workspace. public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_LOCAL_HOME.resolve("java")); + /// Returns the repository selected by the current Java auto-download directory setting. + private static HMCLJavaRepository getActiveRepository() { + Path root = Path.of(SettingsManager.settings().getResolvedJavaDirectory()).toAbsolutePath().normalize(); + return new HMCLJavaRepository(root); + } + public static String getMojangJavaPlatform(Platform platform) { if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) { if (Architecture.SYSTEM_ARCH == Architecture.X86) { @@ -231,7 +245,7 @@ public static Task getAddJavaTask(Path binary) { } public static Task getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) { - return REPOSITORY.getDownloadJavaTask(downloadProvider, platform, gameJavaVersion) + return getActiveRepository().getDownloadJavaTask(downloadProvider, platform, gameJavaVersion) .thenApplyAsync(Schedulers.javafx(), java -> { addJava(java); return java; @@ -239,7 +253,7 @@ public static Task getDownloadJavaTask(DownloadProvider downloadPro } public static Task getInstallJavaTask(Platform platform, String name, Map update, Path archiveFile) { - return REPOSITORY.getInstallJavaTask(platform, name, update, archiveFile) + return getActiveRepository().getInstallJavaTask(platform, name, update, archiveFile) .thenApplyAsync(Schedulers.javafx(), java -> { addJava(java); return java; @@ -249,15 +263,35 @@ public static Task getInstallJavaTask(Platform platform, String nam public static Task getUninstallJavaTask(JavaRuntime java) { assert java.isManaged(); + HMCLJavaRepository activeRepository = getActiveRepository(); + @Nullable Task uninstallTask = getUninstallJavaTask(activeRepository, java); + if (uninstallTask != null) { + return uninstallTask; + } + + if (!activeRepository.getPlatformRoot(java.getPlatform()).equals(REPOSITORY.getPlatformRoot(java.getPlatform()))) { + uninstallTask = getUninstallJavaTask(REPOSITORY, java); + if (uninstallTask != null) { + return uninstallTask; + } + } + + return Task.completed(null); + } + + /// Creates an uninstall task when the runtime belongs to the supplied managed repository. + /// + /// @return the uninstall task, or `null` when the runtime is outside the repository + private static @Nullable Task getUninstallJavaTask(HMCLJavaRepository repository, JavaRuntime java) { Path platformRoot; try { - platformRoot = REPOSITORY.getPlatformRoot(java.getPlatform()).toRealPath(); + platformRoot = repository.getPlatformRoot(java.getPlatform()).toRealPath(); } catch (Throwable ignored) { - return Task.completed(null); + return null; } if (!java.getBinary().startsWith(platformRoot)) - return Task.completed(null); + return null; Path relativized = platformRoot.relativize(java.getBinary()); if (relativized.getNameCount() > 1) { @@ -270,9 +304,9 @@ public static Task getUninstallJavaTask(JavaRuntime java) { }); String name = relativized.getName(0).toString(); - return REPOSITORY.getUninstallJavaTask(java.getPlatform(), name); + return repository.getUninstallJavaTask(java.getPlatform(), name); } else { - return Task.completed(null); + return null; } } @@ -773,17 +807,27 @@ void tryAddJavaInComponentDir(String platform, Path component, boolean verify) { } void searchAllJavaInRepository(Platform platform) { - for (Path java : REPOSITORY.getAllJava(platform)) { - tryAddJavaExecutable(java, true); + HMCLJavaRepository activeRepository = getActiveRepository(); + searchAllJavaInRepository(activeRepository, platform); + + if (!activeRepository.getPlatformRoot(platform).equals(REPOSITORY.getPlatformRoot(platform))) { + searchAllJavaInRepository(REPOSITORY, platform); } for (Path java : LOCAL_REPOSITORY.getAllJava(platform)) { tryAddJavaExecutable(java, true); } + } + + /// Adds runtimes from one HMCL-managed repository and its legacy macOS platform directory. + void searchAllJavaInRepository(HMCLJavaRepository repository, Platform platform) { + for (Path java : repository.getAllJava(platform)) { + tryAddJavaExecutable(java, true); + } if (platform.os() == OperatingSystem.MACOS) { // In the past, we used 'osx' as the checked name for macOS - Path platformRoot = REPOSITORY.getPlatformRoot(platform).resolveSibling("osx-" + platform.getArchitecture().getCheckedName()); + Path platformRoot = repository.getPlatformRoot(platform).resolveSibling("osx-" + platform.getArchitecture().getCheckedName()); searchAllJavaInDirectory(platformRoot); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherSettings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherSettings.java index 4538f1b01d9..7655b0995c2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherSettings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherSettings.java @@ -253,6 +253,39 @@ public String getResolvedCommonDirectory() { : getDefaultCommonDirectory(); } + /// The HMCL-managed Java directory selection mode. + @SerializedName("javaDirectoryType") + private final ObjectProperty javaDirectoryType = new RawPreservingObjectProperty<>(EnumCommonDirectory.DEFAULT); + + /// Returns the HMCL-managed Java directory selection mode property. + public ObjectProperty javaDirectoryTypeProperty() { + return javaDirectoryType; + } + + /// The custom directory used to install HMCL-managed Java runtimes. + @SerializedName("javaDirectory") + private final StringProperty javaDirectory = new SimpleStringProperty(); + + /// Returns the custom HMCL-managed Java directory property. + public StringProperty javaDirectoryProperty() { + return javaDirectory; + } + + /// Returns the default directory used to install HMCL-managed Java runtimes. + public static String getDefaultJavaDirectory() { + return Metadata.HMCL_USER_HOME.resolve("java").toString(); + } + + /// Resolves the effective directory used to install HMCL-managed Java runtimes. + public String getResolvedJavaDirectory() { + EnumCommonDirectory type = javaDirectoryType.get(); + String customPath = javaDirectory.get(); + + return type == EnumCommonDirectory.CUSTOM && StringUtils.isNotBlank(customPath) + ? customPath + : getDefaultJavaDirectory(); + } + /// The maximum number of log lines kept in log views. @SerializedName("logLines") private final ObjectProperty<@Nullable Integer> logLines = new SimpleObjectProperty<>(); 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 0c70fed237d..d191d202806 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -394,6 +394,12 @@ public static void initialize(Stage stage) { dialog(i18n("launcher.cache_directory.invalid")); } + if (settings().javaDirectoryTypeProperty().get() == EnumCommonDirectory.CUSTOM && + !FileUtils.canCreateDirectory(settings().getResolvedJavaDirectory())) { + settings().javaDirectoryTypeProperty().set(EnumCommonDirectory.DEFAULT); + dialog(i18n("launcher.java_directory.invalid")); + } + Lang.thread(JavaManager::initialize, "Search Java", true); scene = new Scene(decorator.getDecorator()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/DownloadSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/DownloadSettingsPage.java index ee4ad559c9a..d8596902ff0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/DownloadSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/DownloadSettingsPage.java @@ -38,6 +38,7 @@ import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.javafx.SafeStringConverter; +import org.jetbrains.annotations.NotNullByDefault; import java.nio.file.Path; import java.util.Arrays; @@ -48,10 +49,14 @@ import static org.jackhuang.hmcl.setting.SettingsManager.settings; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +/// Displays launcher download, cache, Java directory, thread, and proxy settings. +@NotNullByDefault public class DownloadSettingsPage extends StackPane { + /// Keeps weak listeners alive for the lifetime of this page. private final WeakListenerHolder holder = new WeakListenerHolder(); + /// Creates the download settings page and binds its controls to launcher settings. public DownloadSettingsPage() { VBox content = new VBox(10); content.setPadding(new Insets(10)); @@ -131,6 +136,24 @@ public DownloadSettingsPage() { cleanButton.setOnAction(e -> clearCacheDirectory()); fileCommonLocationSublist.setHeaderRight(cleanButton); + ComponentSublist javaDirectorySublist = new ComponentSublist(() -> { + MultiFileItem javaDirectory = new MultiFileItem<>(); + javaDirectory.loadChildren(Arrays.asList( + new MultiFileItem.Option<>(i18n("launcher.java_directory.default"), EnumCommonDirectory.DEFAULT), + new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM) + .setChooserTitle(i18n("launcher.java_directory.choose")) + .setSelectionMode(FileSelector.SelectionMode.DIRECTORY) + .bindBidirectional(settings().javaDirectoryProperty()) + )); + javaDirectory.selectedDataProperty().bindBidirectional(settings().javaDirectoryTypeProperty()); + return List.of(javaDirectory); + }); + javaDirectorySublist.setTitle(i18n("launcher.java_directory")); + javaDirectorySublist.setHasSubtitle(true); + javaDirectorySublist.descriptionProperty().bind( + Bindings.createStringBinding(settings()::getResolvedJavaDirectory, + settings().javaDirectoryProperty(), settings().javaDirectoryTypeProperty())); + ComponentSublist downloadThreadsSublist = new ComponentSublist(() -> { var downloadThreadsList = new RadioChoiceList(); downloadThreadsList.setChoices( @@ -184,7 +207,7 @@ protected Node createRightNode() { } }, settings().autoDownloadThreadsProperty(), settings().downloadThreadsProperty())); - downloadList.getContent().addAll(fileCommonLocationSublist, downloadThreadsSublist); + downloadList.getContent().addAll(fileCommonLocationSublist, javaDirectorySublist, downloadThreadsSublist); content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadList); } @@ -348,6 +371,7 @@ protected Node createRightNode() { } + /// Deletes cached download files from the resolved common directory. private void clearCacheDirectory() { String commonDirectory = settings().getResolvedCommonDirectory(); if (commonDirectory != null) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 711f6d5bcc1..3e28e7159d4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -931,6 +931,10 @@ launcher.cache_directory.choose=Choose cache directory launcher.cache_directory.default=Default ("%APPDATA%/.minecraft" or "~/.minecraft") launcher.cache_directory.disabled=Disabled launcher.cache_directory.invalid=Failed to create a cache directory, falling back to default. +launcher.java_directory=Java Auto-download Directory +launcher.java_directory.choose=Choose Java auto-download directory +launcher.java_directory.default=Default (HMCL data directory/java) +launcher.java_directory.invalid=Failed to create the custom Java auto-download directory, falling back to default. launcher.contact=Contact Us launcher.crash=Hello Minecraft! Launcher has encountered a fatal error! Please copy the following log and ask for help on our Discord, QQ group, GitHub, or other Minecraft forum. launcher.crash.java_internal_error=Hello Minecraft! Launcher has encountered a fatal error because your Java is corrupted. Please uninstall your Java and download a suitable Java here. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 3e2c4d3d092..7e8c1d1c723 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -738,6 +738,10 @@ launcher.cache_directory.choose=選取檔案下載快取目錄 launcher.cache_directory.default=預設 ("%APPDATA%/.minecraft" 或 "~/.minecraft") launcher.cache_directory.disabled=停用 launcher.cache_directory.invalid=無法建立自訂的快取目錄。已還原至預設設定。 +launcher.java_directory=Java 自動下載目錄 +launcher.java_directory.choose=選取 Java 自動下載目錄 +launcher.java_directory.default=預設(HMCL 資料目錄/java) +launcher.java_directory.invalid=無法建立自訂的 Java 自動下載目錄。已還原至預設設定。 launcher.contact=聯絡我們 launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤。請複製下列內容並透過 GitHub、Discord 或 HMCL QQ 群回報問題。 launcher.crash.java_internal_error=Hello Minecraft! Launcher 由於目前 Java 損壞而無法繼續執行。請移除目前 Java,點擊 此處 安裝合適的 Java 版本。 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index e1d798ad008..6f27ed0d3e0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -743,6 +743,10 @@ launcher.cache_directory.choose=选择文件下载缓存文件夹 launcher.cache_directory.default=默认 ("%APPDATA%/.minecraft" 或 "~/.minecraft") launcher.cache_directory.disabled=禁用 (总是使用游戏文件夹路径) launcher.cache_directory.invalid=无法创建自定义的缓存文件夹。已经恢复到默认设置。 +launcher.java_directory=Java 自动下载文件夹 +launcher.java_directory.choose=选择 Java 自动下载文件夹 +launcher.java_directory.default=默认(HMCL 数据文件夹/java) +launcher.java_directory.invalid=无法创建自定义的 Java 自动下载文件夹。已经恢复到默认设置。 launcher.contact=联系我们 launcher.crash=Hello Minecraft! Launcher 遇到了无法处理的错误。请复制下列内容并点击右下角的按钮反馈问题。 launcher.crash.java_internal_error=Hello Minecraft! Launcher 由于当前 Java 损坏而无法继续运行。请卸载当前 Java,点击 此处 安装合适的 Java 版本。 From e0ad67aa8e9aa6d4aa80a6187eff264118faa4d3 Mon Sep 17 00:00:00 2001 From: OctopusCat101 Date: Mon, 13 Jul 2026 21:32:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8B=AC=E5=8F=B7?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 7e8c1d1c723..af9cb63089d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -740,7 +740,7 @@ launcher.cache_directory.disabled=停用 launcher.cache_directory.invalid=無法建立自訂的快取目錄。已還原至預設設定。 launcher.java_directory=Java 自動下載目錄 launcher.java_directory.choose=選取 Java 自動下載目錄 -launcher.java_directory.default=預設(HMCL 資料目錄/java) +launcher.java_directory.default=預設 (HMCL 資料目錄/java) launcher.java_directory.invalid=無法建立自訂的 Java 自動下載目錄。已還原至預設設定。 launcher.contact=聯絡我們 launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤。請複製下列內容並透過 GitHub、Discord 或 HMCL QQ 群回報問題。 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 6f27ed0d3e0..4fad0f03080 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -745,7 +745,7 @@ launcher.cache_directory.disabled=禁用 (总是使用游戏文件夹路径) launcher.cache_directory.invalid=无法创建自定义的缓存文件夹。已经恢复到默认设置。 launcher.java_directory=Java 自动下载文件夹 launcher.java_directory.choose=选择 Java 自动下载文件夹 -launcher.java_directory.default=默认(HMCL 数据文件夹/java) +launcher.java_directory.default=默认 (HMCL 数据文件夹/java) launcher.java_directory.invalid=无法创建自定义的 Java 自动下载文件夹。已经恢复到默认设置。 launcher.contact=联系我们 launcher.crash=Hello Minecraft! Launcher 遇到了无法处理的错误。请复制下列内容并点击右下角的按钮反馈问题。