From cc8d130da1cedef86069a73e1238ebe351cc2ae8 Mon Sep 17 00:00:00 2001 From: CHN-Nine9 <2638200842@qq.com> Date: Tue, 11 Aug 2026 17:12:18 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=92=8C=E5=85=A8=E5=B1=80=E6=B8=B8=E6=88=8F=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E7=9A=84=E7=99=BB=E5=BD=95=E6=96=B9=E5=BC=8F=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/setting/AccountSelectionType.java | 34 +++ .../jackhuang/hmcl/setting/GameSettings.java | 27 ++ .../hmcl/ui/construct/RadioChoiceList.java | 7 + .../hmcl/ui/game/GameSettingsPage.java | 266 ++++++++++++++++++ .../hmcl/ui/instances/Instances.java | 97 +++++-- .../resources/assets/lang/I18N.properties | 2 + .../resources/assets/lang/I18N_zh.properties | 2 + .../assets/lang/I18N_zh_CN.properties | 2 + 8 files changed, 406 insertions(+), 31 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/setting/AccountSelectionType.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/AccountSelectionType.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/AccountSelectionType.java new file mode 100644 index 00000000000..e6e4c58d632 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/AccountSelectionType.java @@ -0,0 +1,34 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.setting; + +import org.jetbrains.annotations.NotNullByDefault; + +/// Selects how HMCL resolves the account used to launch a game. +/// +/// Additional payload for [SPECIFIC] is stored in `GameSettings.specificAccountID`. +/// +/// @author Glavo +@NotNullByDefault +public enum AccountSelectionType { + /// Follows the account currently selected in the account list. + GLOBAL, + + /// Uses the account specified by `GameSettings.specificAccountID`. + SPECIFIC +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java index dcee269f706..521b1afd80d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java @@ -26,6 +26,7 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; +import org.jackhuang.hmcl.auth.AccountID; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.java.JavaManager; import org.jackhuang.hmcl.java.JavaRuntime; @@ -704,6 +705,32 @@ public InheritableProperty postExitCommandProperty() { return postExitCommand; } + /// Property name for the account selection mode. + public static final String PROPERTY_ACCOUNT_TYPE = "accountType"; + + /// The account selection mode. + @SerializedName(PROPERTY_ACCOUNT_TYPE) + private final InheritableProperty accountType = + newInheritableProperty(PROPERTY_ACCOUNT_TYPE, AccountSelectionType.GLOBAL); + + /// Returns the account selection mode property. + public InheritableProperty accountTypeProperty() { + return accountType; + } + + /// Property name for the account used by `SPECIFIC` account selection mode. + public static final String PROPERTY_SPECIFIC_ACCOUNT_ID = "specificAccountID"; + + /// Account used by `SPECIFIC` account selection mode. + @SerializedName(PROPERTY_SPECIFIC_ACCOUNT_ID) + private final InheritableProperty<@Nullable AccountID> specificAccountID = + newInheritableProperty(PROPERTY_SPECIFIC_ACCOUNT_ID); + + /// Returns the account used by `SPECIFIC` account selection mode property. + public InheritableProperty<@Nullable AccountID> specificAccountIDProperty() { + return specificAccountID; + } + /// Property name for the quick play target type. public static final String PROPERTY_QUICK_PLAY = "quickPlay"; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/RadioChoiceList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/RadioChoiceList.java index 84d7709a2db..532077158f9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/RadioChoiceList.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/RadioChoiceList.java @@ -214,6 +214,9 @@ public static class Choice { /// The optional subtitle shown on the right side. protected @Nullable String subtitle; + /// The label rendering the current subtitle, kept for dynamic subtitle updates. + private @Nullable javafx.scene.control.Label subtitleLabel; + /// The optional tooltip installed on the radio button. protected @Nullable String tooltip; @@ -251,6 +254,9 @@ public Choice setTitle(String title) { /// Sets the optional subtitle. public Choice setSubtitle(@Nullable String subtitle) { this.subtitle = subtitle; + if (subtitleLabel != null) { + subtitleLabel.setText(subtitle); + } return this; } @@ -332,6 +338,7 @@ private Node createSubtitleLabel() { label.getStyleClass().add("subtitle-label"); label.setStyle("-fx-font-size: 10;"); label.setPadding(new Insets(0, 0, 0, 15)); + subtitleLabel = label; return label; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java index 7685584beaa..1e320e3a0ac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java @@ -28,6 +28,7 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.css.PseudoClass; import javafx.geometry.Insets; @@ -40,6 +41,9 @@ import javafx.scene.layout.*; import javafx.stage.FileChooser; import javafx.stage.Screen; +import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.AccountID; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.java.JavaManager; import org.jackhuang.hmcl.java.JavaRuntime; @@ -129,6 +133,9 @@ public final class GameSettingsPage extends StackPane private final InvalidationListener javaListener = o -> refreshJavaSettings(); private final InvalidationListener weakJavaListener = holder.weak(javaListener); + private final RadioChoiceList<@Nullable Account> accountItem; + private boolean updatingSelectedAccount = false; + public GameSettingsPage(Class settingType) { assert settingType == GameSettings.Preset.class || settingType == GameSettings.Instance.class; @@ -499,6 +506,22 @@ public GameSettingsPage(Class settingType) { GameSettingsPage::getWindowTypeDisplayName); } + // Login Method Setting + var accountSublist = new ComponentSublist(); + gameSettings.getContent().add(accountSublist); + accountSublist.setTitle(i18n("settings.game.login_method")); + accountSublist.setSubtitle(i18n("settings.game.login_method.subtitle")); + accountSublist.setHasSubtitle(true); + { + accountItem = new RadioChoiceList<>(); + accountSublist.getContent().setAll(accountItem); + bindAccountSettings(accountSublist, accountItem); + + Accounts.getAccounts().addListener(holder.weak((ListChangeListener) ignored -> refreshAccountSettings())); + holder.add(FXUtils.onWeakChangeAndOperate(Accounts.selectedAccountProperty(), ignored -> initializeSelectedAccount())); + refreshAccountSettings(); + } + // Show Logs Window Setting var showLogsPane = createInheritableBooleanButton(GameSettings::showLogsProperty); launcherSettings.getContent().add(showLogsPane); @@ -2217,6 +2240,249 @@ private void setWindowSettingsOverridden(GameSettings setting, boolean overridde setPropertyOverridden(setting, setting.windowTypeProperty(), overridden); } + /// Binds the login method radio choice list to the account list or the account selection setting. + /// + /// In global game settings the list mirrors the account list: selecting an account updates the + /// selected account. In instance-specific settings the list switches the instance to its specific + /// account selection when the user selects an account. + private void bindAccountSettings( + ComponentSublist sublist, + RadioChoiceList<@Nullable Account> item) { + ObjectProperty<@Nullable InheritableProperty> activeTypeProperty = new SimpleObjectProperty<>(); + ObjectProperty<@Nullable InheritableProperty> activeParentTypeProperty = new SimpleObjectProperty<>(); + ObjectProperty<@Nullable InheritableProperty<@Nullable AccountID>> activeSpecificProperty = new SimpleObjectProperty<>(); + ObjectProperty<@Nullable InheritableProperty<@Nullable AccountID>> activeParentSpecificProperty = new SimpleObjectProperty<>(); + final Holder refreshHolder = new Holder<>(); + @Nullable JFXButton inheritButton = null; + if (!isPresetSetting) { + inheritButton = createInheritanceButton(); + sublist.setTitleRight(inheritButton); + } + @Nullable JFXButton finalInheritButton = inheritButton; + + InvalidationListener propertyListener = observable -> { + if (isPresetSetting) { + return; + } + GameSettings setting = currentSetting.get(); + updateParentInheritablePropertyListener( + setting, + activeParentTypeProperty, + GameSettings::accountTypeProperty, + refreshHolder.value); + updateParentInheritablePropertyListener( + setting, + activeParentSpecificProperty, + GameSettings::specificAccountIDProperty, + refreshHolder.value); + InheritableProperty property = activeTypeProperty.get(); + if (setting == null || property == null) { + return; + } + + initializeSelectedAccount(); + if (finalInheritButton != null) { + updateInheritanceButton(finalInheritButton, !isPropertyOverridden(setting, property)); + } + }; + InvalidationListener weakPropertyListener = holder.weak(propertyListener); + refreshHolder.value = weakPropertyListener; + activeParentSetting.addListener(weakPropertyListener); + + item.selectedValueProperty().addListener((observable, oldValue, newValue) -> { + if (updatingSelectedAccount) { + return; + } + + if (isPresetSetting) { + if (newValue != null) { + Accounts.setSelectedAccount(newValue); + } + return; + } + + GameSettings setting = currentSetting.get(); + InheritableProperty property = activeTypeProperty.get(); + if (setting == null || property == null || newValue == null) { + return; + } + + updatingSelectedAccount = true; + try { + setPropertyOverridden(setting, property, true); + property.setValue(AccountSelectionType.SPECIFIC); + InheritableProperty<@Nullable AccountID> specificProperty = activeSpecificProperty.get(); + if (specificProperty != null) { + setPropertyOverridden(setting, specificProperty, true); + specificProperty.setValue(newValue.getAccountID()); + } + if (finalInheritButton != null) { + updateInheritanceButton(finalInheritButton, false); + } + } finally { + updatingSelectedAccount = false; + } + }); + + if (finalInheritButton != null) { + finalInheritButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + GameSettings setting = currentSetting.get(); + InheritableProperty property = activeTypeProperty.get(); + if (setting == null || property == null || updatingSelectedAccount) { + return; + } + + updatingSelectedAccount = true; + try { + if (!isPropertyOverridden(setting, property)) { + property.setValue(AccountSelectionType.SPECIFIC); + setPropertyOverridden(setting, property, true); + InheritableProperty<@Nullable AccountID> specificProperty = activeSpecificProperty.get(); + if (specificProperty != null) { + Account selected = Accounts.getSelectedAccount(); + specificProperty.setValue(selected != null ? selected.getAccountID() : null); + setPropertyOverridden(setting, specificProperty, true); + } + } else { + setPropertyOverridden(setting, property, false); + InheritableProperty<@Nullable AccountID> specificProperty = activeSpecificProperty.get(); + if (specificProperty != null) { + setPropertyOverridden(setting, specificProperty, false); + } + } + } finally { + updatingSelectedAccount = false; + } + propertyListener.invalidated(property); + event.consume(); + }); + } + + currentSetting.addListener((observable, oldValue, newValue) -> { + if (isPresetSetting) { + return; + } + if (oldValue != null) { + oldValue.removeListener(weakPropertyListener); + } + + InheritableProperty oldTypeProperty = activeTypeProperty.get(); + if (oldTypeProperty != null) { + oldTypeProperty.removeListener(weakPropertyListener); + } + InheritableProperty<@Nullable AccountID> oldSpecificProperty = activeSpecificProperty.get(); + if (oldSpecificProperty != null) { + oldSpecificProperty.removeListener(weakPropertyListener); + } + + InheritableProperty newTypeProperty = newValue != null ? newValue.accountTypeProperty() : null; + activeTypeProperty.set(newTypeProperty); + InheritableProperty<@Nullable AccountID> newSpecificProperty = newValue != null ? newValue.specificAccountIDProperty() : null; + activeSpecificProperty.set(newSpecificProperty); + if (newValue != null) { + newValue.addListener(weakPropertyListener); + } + if (newTypeProperty != null) { + newTypeProperty.addListener(weakPropertyListener); + } + if (newSpecificProperty != null) { + newSpecificProperty.addListener(weakPropertyListener); + } + propertyListener.invalidated(newValue); + }); + + S setting = currentSetting.get(); + if (setting != null && !isPresetSetting) { + activeTypeProperty.set(setting.accountTypeProperty()); + activeSpecificProperty.set(setting.specificAccountIDProperty()); + setting.addListener(weakPropertyListener); + setting.accountTypeProperty().addListener(weakPropertyListener); + setting.specificAccountIDProperty().addListener(weakPropertyListener); + } + propertyListener.invalidated(setting); + } + + /// Rebuilds the login method account options and refreshes the selected account. + private void refreshAccountSettings() { + refreshAccountOptions(); + initializeSelectedAccount(); + } + + /// Rebuilds the login method radio choices from the current account list. + private void refreshAccountOptions() { + var options = new ArrayList>(); + for (Account account : Accounts.getAccounts()) { + options.add(new RadioChoiceList.Choice<>( + getAccountProfileName(account), + account) + .setSubtitle(getAccountSubtitle(account))); + } + accountItem.setChoices(options); + } + + /// Selects the login method radio choice matching the current account selection setting. + private void initializeSelectedAccount() { + if (isPresetSetting) { + setSelectedAccountValue(Accounts.getSelectedAccount()); + return; + } + + S setting = currentSetting.get(); + if (setting == null) { + return; + } + + AccountSelectionType accountType = getEffectiveValue(setting, GameSettings::accountTypeProperty); + @Nullable Account account = null; + if (accountType == AccountSelectionType.SPECIFIC) { + @Nullable AccountID specificAccountID = getEffectiveValue(setting, GameSettings::specificAccountIDProperty); + account = findAccountById(specificAccountID); + } + if (account == null) { + account = Accounts.getSelectedAccount(); + } + setSelectedAccountValue(account); + } + + /// Selects the given account without treating the change as a user action. + private void setSelectedAccountValue(@Nullable Account account) { + updatingSelectedAccount = true; + try { + accountItem.setSelectedValue(account); + } finally { + updatingSelectedAccount = false; + } + } + + /// Returns the account with the given account ID, if it still exists. + private @Nullable Account findAccountById(@Nullable AccountID accountID) { + if (accountID == null) { + return null; + } + + for (Account account : Accounts.getAccounts()) { + if (accountID.equals(account.getAccountID())) { + return account; + } + } + return null; + } + + /// Returns the display name shown for the given account in the account list. + private static String getAccountProfileName(Account account) { + String profileName = account.getProfileName(); + return StringUtils.isBlank(profileName) ? account.getProfileID().toString() : profileName; + } + + /// Returns the authentication method description shown below the account name in the account list. + private static String getAccountSubtitle(Account account) { + String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account)); + if (account instanceof AuthlibInjectorAccount authlibInjectorAccount) { + return loginTypeName + ", " + i18n("account.injector.server") + ": " + authlibInjectorAccount.getServer().getName(); + } + return loginTypeName; + } + private void bindInheritableSublistDescription(ComponentSublist sublist, Function> propertyGetter, Function converter) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java index 4d62e3263b3..ec7bba182d1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java @@ -22,6 +22,7 @@ import javafx.stage.FileChooser; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.auth.Account; +import org.jackhuang.hmcl.auth.AccountID; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DownloadProvider; @@ -52,6 +53,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.net.URI; @@ -261,34 +263,41 @@ public static void cleanInstance(HMCLGameRepository repository, GameInstanceID i public static void generateLaunchScript(HMCLGameRepository repository, GameInstanceID instanceId, Consumer... injecters) { if (!checkVersionForLaunching(repository, instanceId)) return; - ensureSelectedAccount(account -> { - FileChooser chooser = new FileChooser(); - if (Files.isDirectory(repository.getRunDirectory(instanceId))) - chooser.setInitialDirectory(repository.getRunDirectory(instanceId).toFile()); - chooser.setTitle(i18n("instance.launch_script.save")); - if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) { - chooser.getExtensionFilters().add( - new FileChooser.ExtensionFilter(i18n("extension.command"), "*.command") - ); + Account account = resolveAccountForLaunch(repository, instanceId); + if (account != null) { + generateLaunchScript0(repository, instanceId, account, injecters); + } else { + ensureSelectedAccount(selected -> generateLaunchScript0(repository, instanceId, selected, injecters)); + } + } + + private static void generateLaunchScript0(HMCLGameRepository repository, GameInstanceID instanceId, Account account, Consumer... injecters) { + FileChooser chooser = new FileChooser(); + if (Files.isDirectory(repository.getRunDirectory(instanceId))) + chooser.setInitialDirectory(repository.getRunDirectory(instanceId).toFile()); + chooser.setTitle(i18n("instance.launch_script.save")); + if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) { + chooser.getExtensionFilters().add( + new FileChooser.ExtensionFilter(i18n("extension.command"), "*.command") + ); + } + chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS + ? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat") + : new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh")); + chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1")); + Path file = Controllers.showSaveDialog(chooser); + if (file != null) { + if (!isValidScriptExtension(FileUtils.getExtension(file))) { + String defaultExt = getDefaultScriptExtension(); + file = file.resolveSibling(file.getFileName().toString() + "." + defaultExt); } - chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS - ? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat") - : new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh")); - chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1")); - Path file = Controllers.showSaveDialog(chooser); - if (file != null) { - if (!isValidScriptExtension(FileUtils.getExtension(file))) { - String defaultExt = getDefaultScriptExtension(); - file = file.resolveSibling(file.getFileName().toString() + "." + defaultExt); - } - LauncherHelper launcherHelper = new LauncherHelper(repository, account, instanceId); - for (Consumer injecter : injecters) { - injecter.accept(launcherHelper); - } - launcherHelper.makeLaunchScript(file); + LauncherHelper launcherHelper = new LauncherHelper(repository, account, instanceId); + for (Consumer injecter : injecters) { + injecter.accept(launcherHelper); } - }); + launcherHelper.makeLaunchScript(file); + } } private static boolean isValidScriptExtension(String ext) { @@ -310,13 +319,39 @@ private static String getDefaultScriptExtension() { public static void launch(HMCLGameRepository repository, GameInstanceID instanceId, Consumer... injecters) { if (!checkVersionForLaunching(repository, instanceId)) return; - ensureSelectedAccount(account -> { - LauncherHelper launcherHelper = new LauncherHelper(repository, account, instanceId); - for (Consumer injecter : injecters) { - injecter.accept(launcherHelper); + Account account = resolveAccountForLaunch(repository, instanceId); + if (account != null) { + launch0(repository, instanceId, account, injecters); + } else { + ensureSelectedAccount(selected -> launch0(repository, instanceId, selected, injecters)); + } + } + + private static void launch0(HMCLGameRepository repository, GameInstanceID instanceId, Account account, Consumer... injecters) { + LauncherHelper launcherHelper = new LauncherHelper(repository, account, instanceId); + for (Consumer injecter : injecters) { + injecter.accept(launcherHelper); + } + launcherHelper.launch(); + } + + /// Resolves the account used to launch the instance from its effective game settings. + /// + /// When the instance specifies a specific account, that account is returned if it still exists; + /// otherwise the account currently selected in the account list is returned. + private static @Nullable Account resolveAccountForLaunch(HMCLGameRepository repository, GameInstanceID instanceId) { + GameSettings.Effective effective = repository.getEffectiveGameSettings(instanceId); + if (effective.getInheritable(GameSettings::accountTypeProperty) == AccountSelectionType.SPECIFIC) { + @Nullable AccountID accountID = effective.getInheritable(GameSettings::specificAccountIDProperty); + if (accountID != null) { + for (Account account : Accounts.getAccounts()) { + if (accountID.equals(account.getAccountID())) { + return account; + } + } } - launcherHelper.launch(); - }); + } + return Accounts.getSelectedAccount(); } public static void testGame(HMCLGameRepository repository, GameInstanceID instanceId) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index ca60b1b0a01..b81d79846c5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1575,6 +1575,8 @@ settings.game.java_directory.version=Specify Java Version settings.game.java_directory.template=%1$s (%2$s) settings.game.management=Manage settings.game.override_global=This setting currently uses an instance-specific setting. Click to inherit from the game settings preset. +settings.game.login_method=Login Method +settings.game.login_method.subtitle=Choose the account used to launch the game settings.game.quick_play=Quick Play settings.game.quick_play.multiplayer=Multiplayer settings.game.quick_play.none=None diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 5eeafafcb3b..92f21045f82 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1378,6 +1378,8 @@ settings.game.java_directory.version=指定 Java 版本 settings.game.java_directory.template=%s (%s) settings.game.management=管理 settings.game.override_global=目前使用實例特定設定。點擊後改為繼承遊戲設定預設。 +settings.game.login_method=登入方式 +settings.game.login_method.subtitle=選擇啟動遊戲時使用的帳戶 settings.game.quick_play=快速遊玩 settings.game.quick_play.multiplayer=多人連線 settings.game.quick_play.none=無 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 6fe08134e54..b808acb7f5f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1378,6 +1378,8 @@ settings.game.java_directory.version=指定 Java 版本 settings.game.java_directory.template=%s (%s) settings.game.management=管理 settings.game.override_global=当前使用实例特定设置。点击后改为继承游戏设置预设。 +settings.game.login_method=登录方式 +settings.game.login_method.subtitle=选择启动游戏时使用的账户 settings.game.quick_play=快速游玩 settings.game.quick_play.multiplayer=多人联机 settings.game.quick_play.none=无 From 870423eaea372e6ab2bf9d4a8701786463d4a6f1 Mon Sep 17 00:00:00 2001 From: CHN-Nine9 <2638200842@qq.com> Date: Tue, 11 Aug 2026 17:19:52 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=A4=96=E7=BD=AE=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E8=B4=A6=E6=88=B7=E6=9D=A1=E7=9B=AE=E7=8E=B0=E5=9C=A8=E4=BC=9A?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E7=99=BB=E5=BD=95=E7=94=A8=E6=88=B7=E5=90=8D?= =?UTF-8?q?(=E4=B8=8E=E8=B4=A6=E6=88=B7=E5=88=97=E8=A1=A8=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E4=B8=80=E8=87=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/game/GameSettingsPage.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java index 1e320e3a0ac..bb9c0d8fb49 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java @@ -43,6 +43,7 @@ import javafx.stage.Screen; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AccountID; +import org.jackhuang.hmcl.auth.ClassicAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.java.JavaManager; @@ -2261,7 +2262,7 @@ private void bindAccountSettings( @Nullable JFXButton finalInheritButton = inheritButton; InvalidationListener propertyListener = observable -> { - if (isPresetSetting) { + if (isPresetSetting || updatingSelectedAccount) { return; } GameSettings setting = currentSetting.get(); @@ -2322,6 +2323,7 @@ private void bindAccountSettings( } finally { updatingSelectedAccount = false; } + propertyListener.invalidated(property); }); if (finalInheritButton != null) { @@ -2471,16 +2473,22 @@ private void setSelectedAccountValue(@Nullable Account account) { /// Returns the display name shown for the given account in the account list. private static String getAccountProfileName(Account account) { String profileName = account.getProfileName(); - return StringUtils.isBlank(profileName) ? account.getProfileID().toString() : profileName; + if (StringUtils.isBlank(profileName)) { + profileName = account.getProfileID().toString(); + } + if (account instanceof ClassicAccount classicAccount) { + return profileName + " - " + classicAccount.getLoginName(); + } + return profileName; } /// Returns the authentication method description shown below the account name in the account list. private static String getAccountSubtitle(Account account) { String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account)); if (account instanceof AuthlibInjectorAccount authlibInjectorAccount) { - return loginTypeName + ", " + i18n("account.injector.server") + ": " + authlibInjectorAccount.getServer().getName(); + loginTypeName = loginTypeName + ", " + i18n("account.injector.server") + ": " + authlibInjectorAccount.getServer().getName(); } - return loginTypeName; + return account.isPortable() ? loginTypeName + ", " + i18n("account.portable") : loginTypeName; } private void bindInheritableSublistDescription(ComponentSublist sublist, From 97cde35d0363512495181eea374ba044fa0afc41 Mon Sep 17 00:00:00 2001 From: CHN-Nine9 <2638200842@qq.com> Date: Tue, 11 Aug 2026 18:07:07 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=97=A0=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/game/GameSettingsPage.java | 24 ++++++++++++++++++- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java index bb9c0d8fb49..add51e089d4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java @@ -52,6 +52,7 @@ import org.jackhuang.hmcl.setting.property.InheritableProperty; import org.jackhuang.hmcl.setting.property.SettingProperty; import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.account.CreateAccountPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.instances.GameInstanceIconDialog; @@ -135,6 +136,7 @@ public final class GameSettingsPage extends StackPane private final InvalidationListener weakJavaListener = holder.weak(javaListener); private final RadioChoiceList<@Nullable Account> accountItem; + private final Hyperlink accountAddPrompt; private boolean updatingSelectedAccount = false; public GameSettingsPage(Class settingType) { @@ -515,7 +517,12 @@ public GameSettingsPage(Class settingType) { accountSublist.setHasSubtitle(true); { accountItem = new RadioChoiceList<>(); - accountSublist.getContent().setAll(accountItem); + accountAddPrompt = new Hyperlink(i18n("settings.game.login_method.no_account")); + FXUtils.setLimitHeight(accountAddPrompt, 30); + accountAddPrompt.setVisible(false); + accountAddPrompt.setManaged(false); + accountAddPrompt.setOnAction(e -> Controllers.dialog(new CreateAccountPane())); + accountSublist.getContent().setAll(accountItem, accountAddPrompt); bindAccountSettings(accountSublist, accountItem); Accounts.getAccounts().addListener(holder.weak((ListChangeListener) ignored -> refreshAccountSettings())); @@ -2411,7 +2418,22 @@ private void refreshAccountSettings() { } /// Rebuilds the login method radio choices from the current account list. + /// + /// When the account list is empty, the account choices are hidden and the add-account prompt is shown instead. private void refreshAccountOptions() { + if (Accounts.getAccounts().isEmpty()) { + accountItem.setVisible(false); + accountItem.setManaged(false); + accountAddPrompt.setVisible(true); + accountAddPrompt.setManaged(true); + return; + } + + accountItem.setVisible(true); + accountItem.setManaged(true); + accountAddPrompt.setVisible(false); + accountAddPrompt.setManaged(false); + var options = new ArrayList>(); for (Account account : Accounts.getAccounts()) { options.add(new RadioChoiceList.Choice<>( diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b81d79846c5..27f18b4a4cb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1576,6 +1576,7 @@ settings.game.java_directory.template=%1$s (%2$s) settings.game.management=Manage settings.game.override_global=This setting currently uses an instance-specific setting. Click to inherit from the game settings preset. settings.game.login_method=Login Method +settings.game.login_method.no_account=No account added yet, click to add one settings.game.login_method.subtitle=Choose the account used to launch the game settings.game.quick_play=Quick Play settings.game.quick_play.multiplayer=Multiplayer diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 92f21045f82..055aebfe725 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1379,6 +1379,7 @@ settings.game.java_directory.template=%s (%s) settings.game.management=管理 settings.game.override_global=目前使用實例特定設定。點擊後改為繼承遊戲設定預設。 settings.game.login_method=登入方式 +settings.game.login_method.no_account=尚未新增帳戶,點擊新增 settings.game.login_method.subtitle=選擇啟動遊戲時使用的帳戶 settings.game.quick_play=快速遊玩 settings.game.quick_play.multiplayer=多人連線 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 b808acb7f5f..c7b4c70cb42 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1379,6 +1379,7 @@ settings.game.java_directory.template=%s (%s) settings.game.management=管理 settings.game.override_global=当前使用实例特定设置。点击后改为继承游戏设置预设。 settings.game.login_method=登录方式 +settings.game.login_method.no_account=尚未添加账户,点击添加 settings.game.login_method.subtitle=选择启动游戏时使用的账户 settings.game.quick_play=快速游玩 settings.game.quick_play.multiplayer=多人联机