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..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 @@ -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,10 @@ 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.ClassicAccount; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.java.JavaManager; import org.jackhuang.hmcl.java.JavaRuntime; @@ -47,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; @@ -129,6 +135,10 @@ 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 final Hyperlink accountAddPrompt; + private boolean updatingSelectedAccount = false; + public GameSettingsPage(Class settingType) { assert settingType == GameSettings.Preset.class || settingType == GameSettings.Instance.class; @@ -499,6 +509,27 @@ 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<>(); + 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())); + holder.add(FXUtils.onWeakChangeAndOperate(Accounts.selectedAccountProperty(), ignored -> initializeSelectedAccount())); + refreshAccountSettings(); + } + // Show Logs Window Setting var showLogsPane = createInheritableBooleanButton(GameSettings::showLogsProperty); launcherSettings.getContent().add(showLogsPane); @@ -2217,6 +2248,271 @@ 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 || updatingSelectedAccount) { + 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; + } + propertyListener.invalidated(property); + }); + + 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. + /// + /// 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<>( + 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(); + 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) { + loginTypeName = loginTypeName + ", " + i18n("account.injector.server") + ": " + authlibInjectorAccount.getServer().getName(); + } + return account.isPortable() ? loginTypeName + ", " + i18n("account.portable") : 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..27f18b4a4cb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1575,6 +1575,9 @@ 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.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 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..055aebfe725 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1378,6 +1378,9 @@ 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.no_account=尚未新增帳戶,點擊新增 +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..c7b4c70cb42 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,9 @@ 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.no_account=尚未添加账户,点击添加 +settings.game.login_method.subtitle=选择启动游戏时使用的账户 settings.game.quick_play=快速游玩 settings.game.quick_play.multiplayer=多人联机 settings.game.quick_play.none=无