diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java index f38d46d22..f7265458a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java @@ -21,16 +21,18 @@ import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.launch.DefaultLauncher; import org.jackhuang.hmcl.launch.ProcessListener; -import org.jackhuang.hmcl.util.i18n.I18n; +import org.jackhuang.hmcl.util.i18n.Locales; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.ManagedProcess; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.io.File; import java.io.IOException; -import java.util.Locale; -import java.util.Map; +import java.nio.file.*; +import java.util.*; +import java.util.stream.Stream; +import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.util.logging.Logger.LOG; /** @@ -59,63 +61,83 @@ public final class HMCLGameLauncher extends DefaultLauncher { } private void generateOptionsTxt() { - File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt"); - File configFolder = new File(repository.getRunDirectory(version.getId()), "config"); - - if (optionsFile.exists()) { + if (config().isDisableAutoGameOptions()) return; - } - if (configFolder.isDirectory()) { - if (findFiles(configFolder, "options.txt")) { - return; + Path runDir = repository.getRunDirectory(version.getId()).toPath(); + Path optionsFile = runDir.resolve("options.txt"); + Path configFolder = runDir.resolve("config"); + + if (Files.exists(optionsFile)) + return; + + if (Files.isDirectory(configFolder)) { + try (Stream stream = Files.walk(configFolder, 2, FileVisitOption.FOLLOW_LINKS)) { + if (stream.anyMatch(file -> "options.txt".equals(FileUtils.getName(file)))) + return; + } catch (IOException e) { + LOG.warning("Failed to visit config folder", e); } } + Locale locale = Locale.getDefault(); + if (Locales.isEnglish(locale)) + return; + /* - 1.0- :没有语言选项,遇到这些版本时不设置 - 1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃) - 1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文 - 1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文 - 1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文 + * 1.0 : No language option, do not set for these versions + * 1.1 ~ 1.5 : zh_CN works fine, zh_cn will crash (the last two letters must be uppercase, otherwise it will cause an NPE crash) + * 1.6 ~ 1.10 : zh_CN works fine, zh_cn will automatically switch to English + * 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected + * 1.13+ : zh_cn works fine, zh_CN will automatically switch to English */ GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version)); if (gameVersion.compareTo("1.1") < 0) return; - String lang; - - if (I18n.isUseChinese()) { - lang = "zh_CN"; - } else if (Locale.getDefault().getLanguage().equals("lzh")) { - lang = "lzh"; - } else { + String lang = normalizedLanguageTag(locale, gameVersion); + if (lang.isEmpty()) return; - } - if (gameVersion.compareTo("1.11") >= 0) { + if (gameVersion.compareTo("1.11") >= 0) lang = lang.toLowerCase(Locale.ROOT); - } try { - FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang)); + Files.createDirectories(optionsFile.getParent()); + Files.writeString(optionsFile, String.format("lang:%s\n", lang)); } catch (IOException e) { LOG.warning("Unable to generate options.txt", e); } } - private boolean findFiles(File folder, String fileName) { - File[] fs = folder.listFiles(); - if (fs != null) { - for (File f : fs) { - if (f.isDirectory()) - if (f.listFiles((dir, name) -> name.equals(fileName)) != null) - return true; - if (f.getName().equals(fileName)) - return true; - } + private static String normalizedLanguageTag(Locale locale, GameVersionNumber gameVersion) { + String language = locale.getLanguage(); + String region = locale.getCountry(); + + switch (language) { + case "zh": + case "cmn": + if (Locales.isSimplifiedChinese(locale)) + return "zh_CN"; + if (gameVersion.compareTo("1.16") >= 0 + && (region.equals("HK") || region.equals("MO"))) + return "zh_HK"; + return "zh_TW"; + case "ru": + return "ru_RU"; + case "uk": + return "uk_UA"; + case "es": + return "es_ES"; + case "ja": + return "ja_JP"; + case "lzh": + return gameVersion.compareTo("1.16") >= 0 + ? "lzh" + : ""; + default: + return ""; } - return false; } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index 60afbbee1..bd7a5f1ad 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -186,6 +186,9 @@ public final class Config implements Observable { @SerializedName("addedLittleSkin") private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false); + @SerializedName("disableAutoGameOptions") + private BooleanProperty disableAutoGameOptions = new SimpleBooleanProperty(false); + @SerializedName("promptedVersion") private StringProperty promptedVersion = new SimpleStringProperty(); @@ -631,6 +634,18 @@ public final class Config implements Observable { this.addedLittleSkin.set(addedLittleSkin); } + public BooleanProperty disableAutoGameOptionsProperty() { + return disableAutoGameOptions; + } + + public boolean isDisableAutoGameOptions() { + return disableAutoGameOptions.get(); + } + + public void setDisableAutoGameOptions(boolean disableAutoGameOptions) { + this.disableAutoGameOptions.set(disableAutoGameOptions); + } + public int getConfigVersion() { return configVersion.get(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java index 99d4d9355..60aa5dd57 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java @@ -70,6 +70,8 @@ public final class SettingsPage extends SettingsView { // ==== Languages ==== cboLanguage.getItems().setAll(Locales.LOCALES); selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty()); + + disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty()); // ==== fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java index c6ff64abd..2ed61a480 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java @@ -37,6 +37,7 @@ import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.ComponentSublist; import org.jackhuang.hmcl.ui.construct.MultiFileItem; +import org.jackhuang.hmcl.ui.construct.OptionToggleButton; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.i18n.Locales; import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale; @@ -50,6 +51,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; public abstract class SettingsView extends StackPane { protected final JFXComboBox cboLanguage; + protected final OptionToggleButton disableAutoGameOptionsPane; protected final MultiFileItem fileCommonLocation; protected final ComponentSublist fileCommonLocationSublist; protected final Label lblUpdate; @@ -193,6 +195,13 @@ public abstract class SettingsView extends StackPane { settingsPane.getContent().add(languagePane); } + { + disableAutoGameOptionsPane = new OptionToggleButton(); + disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options")); + + settingsPane.getContent().add(disableAutoGameOptionsPane); + } + { BorderPane debugPane = new BorderPane(); 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 bd06996b6..b9fa2fb00 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 @@ -76,15 +76,15 @@ public final class Locales { public static final SupportedLocale JA = new SupportedLocale("ja", Locale.JAPANESE); /** - * Traditional Chinese - */ - public static final SupportedLocale ZH_HANT = new SupportedLocale("zh", Locale.forLanguageTag("zh-Hant")); - - /** - * Simplified Chinese + * Chinese (Simplified) */ public static final SupportedLocale ZH_HANS = new SupportedLocale("zh_CN", Locale.forLanguageTag("zh-Hans")); + /** + * Chinese (Traditional) + */ + public static final SupportedLocale ZH_HANT = new SupportedLocale("zh", Locale.forLanguageTag("zh-Hant")); + /** * Wenyan (Classical Chinese) */ @@ -133,7 +133,27 @@ public final class Locales { } public static boolean isChinese(Locale locale) { - return locale.getLanguage().equals("zh") || locale.getLanguage().equals("lzh"); + switch (locale.getLanguage()) { + case "zh": + case "lzh": + case "cmn": + return true; + default: + return false; + } + } + + public static boolean isSimplifiedChinese(Locale locale) { + if (locale.getLanguage().equals("zh") || locale.getLanguage().equals("cmn")) { + String script = locale.getScript(); + if (script.isEmpty()) { + String region = locale.getCountry(); + return region.isEmpty() || region.equals("CN") || region.equals("SG") || region.equals("MY"); + } else + return script.equals("Hans"); + } else { + return false; + } } @JsonAdapter(SupportedLocale.TypeAdapter.class) @@ -166,26 +186,15 @@ public final class Locales { ResourceBundle bundle = resourceBundle; if (resourceBundle == null) { - bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, new ResourceBundle.Control() { + resourceBundle = bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, new ResourceBundle.Control() { @Override public List getCandidateLocales(String baseName, Locale locale) { - if (locale.getLanguage().equals("zh")) { - boolean simplified; - - String script = locale.getScript(); - String region = locale.getCountry(); - if (script.isEmpty()) - simplified = region.equals("CN") || region.equals("SG"); - else - simplified = script.equals("Hans"); - - if (simplified) { - return List.of( - Locale.SIMPLIFIED_CHINESE, - Locale.CHINESE, - Locale.ROOT - ); - } + if (isSimplifiedChinese(locale)) { + return List.of( + Locale.SIMPLIFIED_CHINESE, + Locale.CHINESE, + Locale.ROOT + ); } if (locale.getLanguage().equals("lzh")) { @@ -203,7 +212,6 @@ public final class Locales { return super.getCandidateLocales(baseName, locale); } }); - resourceBundle = bundle; } return bundle; diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index bd26eaf60..cbec63502 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1338,6 +1338,7 @@ settings.launcher=Launcher Settings settings.launcher.appearance=Appearance settings.launcher.common_path.tooltip=HMCL will put all game assets and dependencies here. If there are existing libraries in the game directory, then HMCL will prefer to use them first. settings.launcher.debug=Debug +settings.launcher.disable_auto_game_options=Do not switch game language settings.launcher.download=Download settings.launcher.download.threads=Threads settings.launcher.download.threads.auto=Automatically Determine diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index d53016adf..4a4e55fb9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -1057,6 +1057,7 @@ settings.launcher=啟設 settings.launcher.appearance=驛式 settings.launcher.common_path.tooltip=啟者將諸遊戲資源及相依庫檔聚於此。若遊戲目錄自有,則不用公庫。 settings.launcher.debug=勘 +settings.launcher.disable_auto_game_options=不易戲文 settings.launcher.download=載 settings.launcher.download.threads=執緒數 settings.launcher.download.threads.auto=從宜 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 61d7c33c1..64259de43 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1128,6 +1128,7 @@ settings.launcher=啟動器設定 settings.launcher.appearance=外觀 settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及相依元件庫檔案放於此集中管理。如果遊戲目錄內有現成的將不會使用公共庫檔案。 settings.launcher.debug=除錯 +settings.launcher.disable_auto_game_options=不自動切換遊戲語言 settings.launcher.download=下載 settings.launcher.download.threads=執行緒數 settings.launcher.download.threads.auto=自動選取執行緒數 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 496d768cf..67c04812c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1138,6 +1138,7 @@ settings.launcher=启动器设置 settings.launcher.appearance=外观 settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库文件存放于此集中管理。如果游戏文件夹内有现成的将不会使用公共库文件。 settings.launcher.debug=调试 +settings.launcher.disable_auto_game_options=不自动切换游戏语言 settings.launcher.download=下载 settings.launcher.download.threads=线程数 settings.launcher.download.threads.auto=自动选择线程数