Compare commits

..
Author SHA1 Message Date
8d876fbff5 [release/3.6] 为不同类型的 Dialog 指定相应的默认标题 (#5671)
https://github.com/HMCL-dev/HMCL/pull/5079
https://github.com/HMCL-dev/HMCL/pull/5152
https://github.com/HMCL-dev/HMCL/pull/5633

Co-authored-by: neveler <55753029+neveler@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2026-03-23 22:07:21 +08:00
辞庐andGlavo 4c1f15c98c [release/3.6] 修复 CurseForge 源无法搜索到内容的问题 (#5733)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-08 22:13:54 +08:00
3gf8jv4dv e8b9d98c99 [release/3.6] chore: update copyright year and attribution (#5670)
https://github.com/HMCL-dev/HMCL/pull/5622
2026-03-07 21:50:58 +08:00
3gf8jv4dv 61b7eb34f6 [release/3.6] fix(l10n): replace mcedit with minecraft wiki's tutorial page (#5672)
https://github.com/HMCL-dev/HMCL/pull/5524
2026-03-07 21:39:05 +08:00
3gf8jv4dv a2df345b3c [release/3.6] style(l10n): simplify update button text in mod management header (#5673)
https://github.com/HMCL-dev/HMCL/pull/4712
2026-03-07 21:37:56 +08:00
3gf8jv4dv 70030a9558 [release/3.6] fix(i18n): update i18n for open dir button on instance mods tab (#5674)
https://github.com/HMCL-dev/HMCL/pull/4734
2026-03-03 21:45:00 +08:00
Glavoand3gf8jv4dv f03a4ff807 [release/3.6] 添加“提前预览 HMCL 版本”选项 (#4668)
https://github.com/HMCL-dev/HMCL/pull/4223
https://github.com/HMCL-dev/HMCL/pull/4598
https://github.com/HMCL-dev/HMCL/pull/4599

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-10-13 15:38:29 +08:00
Glavo de9a105f2f [release/3.6] Update versions.txt to 1.21.10 (#4667)
https://github.com/HMCL-dev/HMCL/pull/4626
2025-10-13 15:27:48 +08:00
Glavo b5d3653c9a [release/3.6] 移除 Pack200 支持 (#4666)
https://github.com/HMCL-dev/HMCL/pull/4267
2025-10-13 15:23:53 +08:00
Glavo 554503fff0 [release/3.6] 在 CacheRepository.ETagItem 中添加 expires 字段 (#4662) 2025-10-13 15:12:51 +08:00
Glavo 785f334241 [release/3.6] 更新 Jenkins CI 服务器的发布模型 (#4582)
https://github.com/HMCL-dev/HMCL/pull/4559
2025-10-02 20:38:58 +08:00
GlavoandShulkerSakura 0b87959716 [release/3.6] Added 16 characters length detection of username (#4580)
https://github.com/HMCL-dev/HMCL/pull/4088

Co-authored-by: ShulkerSakura <50770360+ShulkerSakura@users.noreply.github.com>
2025-10-01 20:44:11 +08:00
Glavoand8Mi_Yile ae18ca7567 [release/3.6] 更新 unlisted-versions-of-minecraft 镜像源地址 (#4579)
https://github.com/HMCL-dev/HMCL/pull/4120

Co-authored-by: 8Mi_Yile <admin@8mi.tech>
2025-10-01 20:34:56 +08:00
Glavoand3gf8jv4dv 7ebdc783c6 [release/3.6] chore: update versions.txt to 1.21.9 (#4578)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-10-01 20:22:18 +08:00
Glavo 39abdf1ecf [release/3.6] 修复启用代理身份认证且未设置密码时启动器崩溃的问题 (#4536)
https://github.com/HMCL-dev/HMCL/pull/4465
2025-09-23 11:11:19 +08:00
Glavo 8df6563d56 [release/3.6] 修复滚动切换游戏实例/账户功能 (#4535)
https://github.com/HMCL-dev/HMCL/pull/4466
2025-09-22 22:39:38 +08:00
Glavo f62c12fd3c [release/3.6] 优化 Java 自动选择规则 (#4534)
https://github.com/HMCL-dev/HMCL/pull/4515
2025-09-22 22:32:11 +08:00
Glavo 1205212e52 [release/3.6] 修复使用 JavaFX 25 时游戏下载界面布局错误的问题 (#4533)
https://github.com/HMCL-dev/HMCL/pull/4508
2025-09-22 22:25:32 +08:00
Glavo 0d9a275fb3 [release/3.6] 检测到配置文件由高版本启动器创建时禁止保存配置文件 (#4449)
https://github.com/HMCL-dev/HMCL/pull/4414
2025-09-11 01:19:56 +08:00
Glavoand3gf8jv4dv ab72ac3d10 [release/3.6] chore(l10n): change translation from 版本 to 实例 in zh-cn for term INSTANCE (#4448)
https://github.com/HMCL-dev/HMCL/pull/4376

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-09-11 00:24:53 +08:00
Glavoand辞庐 9d5f8a9f0e [release/3.6] 修复 LWJGL 路径显示错误的问题 (#4447)
https://github.com/HMCL-dev/HMCL/pull/4192

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-11 00:15:13 +08:00
GlavoandWulian233 fe2e96345b [release/3.6] 修复世界管理页面不支持显示极限模式的问题 (#4446)
https://github.com/HMCL-dev/HMCL/pull/4321

Co-authored-by: Wulian233 <1055917385@qq.com>
2025-09-11 00:05:23 +08:00
Glavoand辞庐 881fa2858f [release/3.6] 修复整合包格式不匹配提示未被正确格式化的问题 (#4445)
https://github.com/HMCL-dev/HMCL/pull/4176

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-11 00:01:11 +08:00
Glavo a0b95866be [release/3.6] Update microsoft_auth.html (#4444)
https://github.com/HMCL-dev/HMCL/pull/4388
2025-09-10 23:53:35 +08:00
Glavo 93bbef385d [release/3.6] 修复 RemoteMod::getIntegrityCheck 未选择正确的算法的问题 (#4443)
https://github.com/HMCL-dev/HMCL/pull/4238
2025-09-10 23:44:09 +08:00
Glavoand神奇的赵二 9dcc2727aa [release/3.6] 新增内存显示条禁用样式 (#4442)
https://github.com/HMCL-dev/HMCL/pull/4185

Co-authored-by: 神奇的赵二 (OrzMiku) <miku@ecy.pink>
2025-09-10 23:39:12 +08:00
Glavoand辞庐 865c8588d8 [release/3.6] 修复“下载线程数”的指示器被提示覆盖的问题 (#4441)
https://github.com/HMCL-dev/HMCL/pull/4153

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-10 23:34:52 +08:00
3gf8jv4dvand辞庐 419612bd3e [release/3.6] backport language file changes to the 3.6 branch (#4319)
https://github.com/HMCL-dev/HMCL/pull/3690
https://github.com/HMCL-dev/HMCL/pull/4149
https://github.com/HMCL-dev/HMCL/pull/4188
https://github.com/HMCL-dev/HMCL/pull/4189
https://github.com/HMCL-dev/HMCL/pull/4199
https://github.com/HMCL-dev/HMCL/pull/4200
https://github.com/HMCL-dev/HMCL/pull/4303

---------

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-10 23:34:13 +08:00
Glavo d16eded1ad [release/3.6] 弃用 Java 11 支持 (#4422) 2025-09-10 23:24:51 +08:00
Glavo c3a47655f0 [release/3.6] 更新模组和整合包数据 (#4440)
https://github.com/HMCL-dev/HMCL/pull/4439
2025-09-10 23:21:30 +08:00
Glavo 34842a5f26 [release/3.6] 修复无法加载 WebP 图标的问题 (#4436)
https://github.com/HMCL-dev/HMCL/pull/4171
2025-09-10 23:20:27 +08:00
Glavo 6dd7d53c20 [release/3.6] 修复 backgroundType 为 null 时启动器崩溃的问题 (#4438)
https://github.com/HMCL-dev/HMCL/pull/4437
2025-09-10 22:58:18 +08:00
Glavo 1654c652e0 [release/3.6] 在 ./.hmcl/java 中搜索 Java (#4434)
https://github.com/HMCL-dev/HMCL/pull/4179
2025-09-10 21:37:45 +08:00
Glavoand辞庐 881f5e7874 [release/3.6] 模组下载列表翻页后自动回到顶部 (#4433)
https://github.com/HMCL-dev/HMCL/pull/4170

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-09-10 21:28:03 +08:00
Glavo 73f0eb472a [release/3.6] 修复首次打开子页面时切换动画丢失的问题 (#4431)
https://github.com/HMCL-dev/HMCL/pull/4365
2025-09-10 21:17:00 +08:00
Glavo e13b8385e0 [release/3.6] 在 Windows 7/8/8.1 上使用 llvmpipe-loader 加载 mesa (#4430)
https://github.com/HMCL-dev/HMCL/pull/4224
2025-09-10 21:01:36 +08:00
Glavo a6e4651307 [release/3.6] 修复非 Windows 平台上游戏进程的 user.home 可能被设置为 null 的问题 (#4427)
https://github.com/HMCL-dev/HMCL/pull/4241
2025-09-10 20:55:33 +08:00
Glavo 554e573d7f [release/3.6] 支持通过环境变量强制使用 GPU 渲染和控制动画帧率 (#4426)
https://github.com/HMCL-dev/HMCL/pull/4164
2025-09-10 20:46:51 +08:00
Glavo 3096f933af [release/3.6] 修复导入异常格式的数据包时启动器崩溃的问题 (#4425)
https://github.com/HMCL-dev/HMCL/pull/4152
2025-09-10 20:41:29 +08:00
GlavoandWulian233 ff3b486f58 [release/3.6] 修复游戏版本列表错误地过滤掉部分版本的问题 (#4424)
https://github.com/HMCL-dev/HMCL/pull/4066
https://github.com/HMCL-dev/HMCL/pull/4244

---------

Co-authored-by: Wulian233 <1055917385@qq.com>
2025-09-10 20:36:01 +08:00
GlavoandRoj234 708e0326a7 [release/3.6] 修复多次启动游戏时内存泄露的问题 (#4423)
https://github.com/HMCL-dev/HMCL/pull/4121

Co-authored-by: Roj234 <82699138+roj234@users.noreply.github.com>
2025-09-10 20:27:12 +08:00
GlavoandWulian233 f7a5c7a310 [release/3.6] 修复 1.21.5 以上的地图跳转种子地图错误 (#4317)
https://github.com/HMCL-dev/HMCL/pull/4274

Co-authored-by: Wulian233 <1055917385@qq.com>
2025-08-22 20:48:56 +08:00
Glavo 633e8f8fe2 [release/3.6] Update amdgpu.ids (#4316) 2025-08-22 20:44:36 +08:00
Glavoand辞庐 440ea1b21d [release/3.6] fix: 无法从 BMCLAPI 下载 NeoForge 所有所需文件 (#4313)
https://github.com/HMCL-dev/HMCL/pull/4174

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-08-22 20:36:18 +08:00
227 changed files with 4706 additions and 2920 deletions

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
name: Check Style
on:
push:
paths:
- '**.java'
pull_request:
paths:
- '**.java'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Check style main
run: ./gradlew checkstyleMain --no-daemon
- name: Check style test
run: ./gradlew checkstyleTest --no-daemon
@@ -1,13 +1,11 @@
name: Check Codes
name: Check Translations
on:
push:
paths:
- '**.java'
- '**.properties'
pull_request:
paths:
- '**.java'
- '**.properties'
jobs:
@@ -15,11 +13,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
java-version: '11'
java-package: 'jdk+fx'
- name: Check Codes
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel
- name: Check style test
run: ./gradlew checkTranslations --no-daemon
+2 -2
View File
@@ -13,11 +13,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
java-version: '11'
java-package: 'jdk+fx'
- name: Build with Gradle
run: ./gradlew build --no-daemon
-1
View File
@@ -18,7 +18,6 @@ minecraft-exported-crash-info*
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLBoot/build/
/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/buildSrc/build/
+41 -25
View File
@@ -1,4 +1,7 @@
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
@@ -12,39 +15,44 @@ plugins {
alias(libs.plugins.shadow)
}
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
.isNullOrEmpty())
val projectConfig = PropertiesUtils.load(rootProject.file("config/project.properties").toPath())
val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
if (number != null) {
(number - offset).toString()
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
val prefix = if (isOfficial) "dev" else "unofficial"
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
version = "$versionRoot.$buildNumber"
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt()
if (buildNumber != null) {
version = if (JenkinsUtils.IS_ON_CI && versionType == "dev") {
"$versionRoot.0.$buildNumber"
} else {
"$versionRoot.$buildNumber"
}
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
version = if (shortCommit.isNullOrBlank()) {
"$versionRoot.SNAPSHOT"
} else if (isOfficial) {
"$versionRoot.dev-$shortCommit"
} else {
"$versionRoot.unofficial-$shortCommit"
}
}
dependencies {
implementation(project(":HMCLCore"))
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
if (launcherExe == null) {
if (launcherExe.isBlank()) {
implementation(libs.hmclauncher)
}
}
@@ -90,13 +98,16 @@ fun attachSignature(jar: File) {
}
}
tasks.withType<JavaCompile> {
sourceCompatibility = "11"
targetCompatibility = "11"
val java11 = sourceSets.create("java11") {
java {
srcDir("src/main/java11")
}
}
tasks.compileJava {
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
sourceCompatibility = "11"
targetCompatibility = "11"
}
tasks.jar {
@@ -124,7 +135,6 @@ tasks.shadowJar {
exclude(dependency("com.google.code.gson:.*:.*"))
exclude(dependency("net.java.dev.jna:jna:.*"))
exclude(dependency("libs:JFoenix:.*"))
exclude(project(":HMCLBoot"))
}
manifest {
@@ -137,7 +147,6 @@ tasks.shadowJar {
"Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"Build-Channel" to versionType,
"Class-Path" to "pack200.jar",
"Add-Opens" to listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
@@ -161,7 +170,7 @@ tasks.shadowJar {
}
}
if (launcherExe != null) {
if (launcherExe.isNotBlank()) {
into("assets") {
from(file(launcherExe))
}
@@ -173,6 +182,13 @@ tasks.shadowJar {
}
}
tasks.processResources {
into("META-INF/versions/11") {
from(sourceSets["java11"].output)
}
dependsOn(tasks["java11Classes"])
}
val makeExecutables by tasks.registering {
val extensions = listOf("exe", "sh")
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
@@ -28,7 +27,6 @@ import javafx.scene.input.DataFormat;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
@@ -56,8 +54,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
@@ -82,20 +78,16 @@ public final class Launcher extends Application {
LOG.warning("Failed to get prism pipeline", e);
}
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
try {
try {
ConfigHolder.init();
} catch (SambaException e) {
showAlert(AlertType.WARNING, i18n("fatal.samba"));
} catch (SambaException ignored) {
Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (IOException e) {
LOG.error("Failed to load config", e);
checkConfigInTempDir();
checkConfigOwner();
showAlert(AlertType.ERROR, i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
EntryPoint.exit(1);
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
}
// https://lapcatsoftware.com/articles/app-translocation.html
@@ -113,8 +105,12 @@ public final class Launcher extends Application {
return;
}
if (ConfigHolder.isUnsupportedVersion()) {
showAlert(AlertType.WARNING, i18n("fatal.config_unsupported_version"));
}
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
showAlert(AlertType.WARNING, i18n("fatal.illegal_char"));
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
}
// runLater to ensure ConfigHolder.init() finished initialization
@@ -177,7 +173,7 @@ public final class Launcher extends Application {
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
EntryPoint.exit(0);
Main.exit(0);
}
}
@@ -217,7 +213,7 @@ public final class Launcher extends Application {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
EntryPoint.exit(1);
Main.exit(1);
}
@Override
@@ -17,26 +17,41 @@
*/
package org.jackhuang.hmcl;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.ModuleHelper;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.concurrent.CancellationException;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class EntryPoint {
public final class Main {
private EntryPoint() {
private Main() {
}
public static void main(String[] args) {
@@ -64,6 +79,10 @@ public final class EntryPoint {
checkDirectoryPath();
if (JavaRuntime.CURRENT_VERSION < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
initIcon();
@@ -93,8 +112,6 @@ public final class EntryPoint {
}
}
} catch (IOException e) {
// Logger has not been started yet, so print directly to System.err
System.err.println("Failed to create HMCL directory: " + Metadata.HMCL_CURRENT_DIRECTORY);
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
}
@@ -110,17 +127,16 @@ public final class EntryPoint {
}
private static void initIcon() {
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(EntryPoint.class.getResource("/assets/img/icon-mac.png"));
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon-mac.png"));
AwtUtils.setAppleIcon(image);
}
private static void checkDirectoryPath() {
String currentDir = System.getProperty("user.dir", "");
if (currentDir.contains("!")) {
LOG.error("The current working path contains an exclamation mark: " + currentDir);
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
// No Chinese translation because both Swing and JavaFX cannot render Chinese character properly when exclamation mark exists in the path.
showErrorAndExit("Exclamation mark(!) is not allowed in the path where HMCL is in.\n"
+ "The path is " + currentDir);
+ "The path is " + currentDirectory);
}
}
@@ -128,10 +144,10 @@ public final class EntryPoint {
try {
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.error("Unable to patch JVM", e);
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.error("Unable to patch JVM", e);
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.error("User cancels downloading JavaFX", e);
@@ -148,7 +164,7 @@ public final class EntryPoint {
Class.forName("javafx.stage.Stage"); // javafx.graphics
Class.forName("javafx.scene.control.Skin"); // javafx.controls
} catch (Exception e) {
LOG.warning("JavaFX is incomplete or not found", e);
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
@@ -156,20 +172,9 @@ public final class EntryPoint {
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
// javafx.graphics
Module module = Class.forName("javafx.stage.Stage").getModule();
if (module.isNamed()) {
try {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
MethodHandle implAddEnableNativeAccess = lookup.findVirtual(Module.class,
"implAddEnableNativeAccess", MethodType.methodType(Module.class));
Module ignored = (Module) implAddEnableNativeAccess.invokeExact(module);
} catch (Throwable e) {
e.printStackTrace(System.err);
}
}
ModuleHelper.addEnableNativeAccess(Class.forName("javafx.stage.Stage")); // javafx.graphics
} catch (ClassNotFoundException e) {
LOG.error("Failed to add enable native access for JavaFX", e);
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
@@ -180,11 +185,11 @@ public final class EntryPoint {
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
try {
Class<?> clazz = Class.forName("sun.misc.Unsafe");
boolean ignored = (boolean) MethodHandles.privateLookupIn(clazz, MethodHandles.lookup())
.findStatic(clazz, "trySetMemoryAccessWarned", MethodType.methodType(boolean.class))
.invokeExact();
Method trySetMemoryAccessWarned = clazz.getDeclaredMethod("trySetMemoryAccessWarned");
trySetMemoryAccessWarned.setAccessible(true);
trySetMemoryAccessWarned.invoke(null);
} catch (Throwable e) {
LOG.warning("Failed to enable unsafe memory access", e);
e.printStackTrace(System.err);
}
}
}
@@ -192,8 +197,70 @@ public final class EntryPoint {
/**
* Indicates that a fatal error has occurred, and that the application cannot start.
*/
private static void showErrorAndExit(String message) {
static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.ERROR, message).showAndWait();
exit(1);
}
} catch (Throwable ignored) {
}
SwingUtils.showErrorDialog(message);
exit(1);
}
/**
* Indicates that potential issues have been detected, and that the application may not function properly (but it can still run).
*/
static void showWarningAndContinue(String message) {
System.err.println(message);
System.err.println("Potential issues have been detected.");
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.WARNING, message).showAndWait();
return;
}
} catch (Throwable ignored) {
}
SwingUtils.showWarningDialog(message);
}
private static void fixLetsEncrypt() {
try {
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
try (InputStream ksStream = Files.newInputStream(ksPath)) {
defaultKeyStore.load(ksStream, "changeit".toCharArray());
}
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
}
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
merged.load(null, new char[0]);
for (String alias : Collections.list(letsEncryptKeyStore.aliases()))
merged.setCertificateEntry(alias, letsEncryptKeyStore.getCertificate(alias));
for (String alias : Collections.list(defaultKeyStore.aliases()))
merged.setCertificateEntry(alias, defaultKeyStore.getCertificate(alias));
TrustManagerFactory instance = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
instance.init(merged);
SSLContext tls = SSLContext.getInstance("TLS");
tls.init(null, instance.getTrustManagers(), null);
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
LOG.info("Added Lets Encrypt root certificates as additional trust");
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
KeyManagementException e) {
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
}
}
}
@@ -19,10 +19,13 @@ package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
/**
* Stores metadata about this application.
@@ -35,6 +38,9 @@ public final class Metadata {
public static final String FULL_NAME = "Hello Minecraft! Launcher";
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 8;
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
@@ -94,4 +100,31 @@ public final class Metadata {
return !isStable() && !isDev();
}
public static @Nullable String getSuggestedJavaDownloadLink() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
return "https://www.loongnix.cn/zh/api/java/downloads-jdk21/index.html";
else {
EnumSet<Architecture> supportedArchitectures;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.X86, Architecture.ARM64);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
supportedArchitectures = EnumSet.of(
Architecture.X86_64, Architecture.X86,
Architecture.ARM64, Architecture.ARM32,
Architecture.RISCV64, Architecture.LOONGARCH64
);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.ARM64);
else
supportedArchitectures = EnumSet.noneOf(Architecture.class);
if (supportedArchitectures.contains(Architecture.SYSTEM_ARCH))
return String.format("https://docs.hmcl.net/downloads/%s/%s.html",
OperatingSystem.CURRENT_OS.getCheckedName(),
Architecture.SYSTEM_ARCH.getCheckedName()
);
else
return null;
}
}
}
@@ -158,8 +158,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
blackList.add("saves");
if (Files.exists(dstDir)) throw new IOException("Version exists");
Files.createDirectories(dstDir);
FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null));
Path fromJson = srcDir.resolve(srcId + ".json");
@@ -172,7 +170,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
Files.copy(fromJson, toJson);
JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId));
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
@@ -196,7 +194,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File file = getLocalVersionSettingFile(id);
if (file.exists())
try {
VersionSetting versionSetting = GSON.fromJson(Files.readString(file.toPath()), VersionSetting.class);
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
initLocalVersionSetting(id, versionSetting);
} catch (Exception ex) {
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
@@ -429,7 +427,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
File json = getModpackConfiguration(version);
if (json.exists()) {
try {
String jsonText = Files.readString(json.toPath());
String jsonText = FileUtils.readText(json);
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
@@ -65,7 +66,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.fromJsonFile(json.toPath(), ModpackConfiguration.typeOf(Modpack.class));
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(Modpack.class));
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
@@ -53,7 +53,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.*;
@@ -160,8 +160,10 @@ public final class LauncherHelper {
}),
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getMesaLoader(java);
if (renderer != Renderer.DEFAULT
&& OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& OperatingSystem.WINDOWS_VERSION != null) {
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.WINDOWS_VERSION);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
@@ -267,27 +269,27 @@ public final class LauncherHelper {
if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex.getCause();
int responseCode = rce.getResponseCode();
URI uri = rce.getUri();
URL url = rce.getUrl();
if (responseCode == 404)
message += i18n("download.code.404", uri);
message += i18n("download.code.404", url);
else
message += i18n("download.failed", uri, responseCode);
message += i18n("download.failed", url, responseCode);
} else {
message += StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof DownloadException) {
URI uri = ((DownloadException) ex).getUri();
URL url = ((DownloadException) ex).getUrl();
if (ex.getCause() instanceof SocketTimeoutException) {
message = i18n("install.failed.downloading.timeout", uri);
message = i18n("install.failed.downloading.timeout", url);
} else if (ex.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) ex.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
} else {
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else {
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
message = i18n("assets.index.malformed");
@@ -298,11 +300,11 @@ public final class LauncherHelper {
} else if (ex instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) ex;
int responseCode = rce.getResponseCode();
URI uri = rce.getUri();
URL url = rce.getUrl();
if (responseCode == 404)
message = i18n("download.code.404", uri);
message = i18n("download.code.404", url);
else
message = i18n("download.failed", uri, responseCode);
message = i18n("download.failed", url, responseCode);
} else if (ex instanceof CommandTooLongException) {
message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) {
@@ -140,7 +140,7 @@ public final class ModpackHelper {
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.fromJsonFile(file.toPath(), ModpackConfiguration.class);
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -103,6 +104,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
LOG.error("Failed to load html", e);
@@ -45,7 +45,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@@ -110,7 +110,7 @@ public final class TexturesLoader {
if (!Files.isRegularFile(file)) {
// download it
try {
new FileDownloadTask(URI.create(texture.getUrl()), file).run();
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
LOG.info("Texture downloaded: " + texture.getUrl());
} catch (Exception e) {
if (Files.isRegularFile(file)) {
@@ -30,6 +30,7 @@ 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.*;
@@ -115,7 +116,11 @@ public final class HMCLJavaRepository implements JavaRepository {
}
if (Files.isDirectory(javaDir)) {
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
JavaManifest manifest;
try (InputStream input = Files.newInputStream(file)) {
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
}
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
}
}
@@ -186,7 +191,7 @@ public final class HMCLJavaRepository implements JavaRepository {
});
JavaManifest manifest = new JavaManifest(info, update, files);
JsonUtils.writeToJsonFile(getManifestFile(platform, gameJavaVersion), manifest);
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
return JavaRuntime.of(executable, info, true);
});
}
@@ -198,7 +203,7 @@ public final class HMCLJavaRepository implements JavaRepository {
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
return JavaRuntime.of(executable, result.getInfo(), true);
});
}
@@ -56,6 +56,7 @@ public final class JavaManager {
}
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
@@ -254,16 +255,15 @@ public final class JavaManager {
}
}
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();
private static JavaRuntime chooseJava(@Nullable JavaRuntime java1, JavaRuntime java2) {
if (java1 == null)
return java2;
if (b1 != b2)
return b1 ? 1 : -1;
}
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
if (java1.getParsedVersion() != java2.getParsedVersion())
// Prefer the Java version that is closer to the game's recommended Java version
return java1.getParsedVersion() < java2.getParsedVersion() ? java1 : java2;
else
return java1.getVersionNumber().compareTo(java2.getVersionNumber()) >= 0 ? java1 : java2;
}
@Nullable
@@ -279,9 +279,6 @@ public final class JavaManager {
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
&& (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) {
@@ -309,15 +306,10 @@ public final class JavaManager {
}
if (!violationMandatory) {
if (mandatory == null) mandatory = java;
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
mandatory = java;
mandatory = chooseJava(mandatory, java);
if (!violationSuggested) {
if (suggested == null) suggested = java;
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
suggested = java;
}
if (!violationSuggested)
suggested = chooseJava(suggested, java);
}
}
@@ -411,15 +403,8 @@ public final class JavaManager {
if (System.getenv("PATH") != null) {
String[] paths = System.getenv("PATH").split(File.pathSeparator);
for (String path : paths) {
// https://github.com/HMCL-dev/HMCL/issues/4079
// https://github.com/Meloong-Git/PCL/issues/4261
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && path.toLowerCase(Locale.ROOT)
.contains("\\common files\\oracle\\java\\")) {
continue;
}
try {
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
@@ -568,6 +553,10 @@ public final class JavaManager {
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import java.io.IOException;
@@ -72,7 +73,8 @@ public final class AuthlibInjectorServers implements Validation {
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
AuthlibInjectorServers configInstance;
try {
configInstance = JsonUtils.fromJsonFile(configLocation, AuthlibInjectorServers.class);
String content = FileUtils.readText(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.warning("Malformed authlib-injectors.json", e);
return;
@@ -50,6 +50,7 @@ import java.util.TreeMap;
public final class Config implements Observable {
public static final int CURRENT_VERSION = 2;
public static final int CURRENT_UI_VERSION = 0;
public static final Gson CONFIG_GSON = new GsonBuilder()
@@ -180,6 +181,9 @@ public final class Config implements Observable {
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@SerializedName("acceptPreviewUpdate")
private BooleanProperty acceptPreviewUpdate = new SimpleBooleanProperty(false);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -670,6 +674,18 @@ public final class Config implements Observable {
this.promptedVersion.set(promptedVersion);
}
public boolean isAcceptPreviewUpdate() {
return acceptPreviewUpdate.get();
}
public BooleanProperty acceptPreviewUpdateProperty() {
return acceptPreviewUpdate;
}
public void setAcceptPreviewUpdate(boolean acceptPreviewUpdate) {
this.acceptPreviewUpdate.set(acceptPreviewUpdate);
}
public ObservableMap<String, Object> getShownTips() {
return shownTips;
}
@@ -45,6 +45,7 @@ public final class ConfigHolder {
private static GlobalConfig globalConfigInstance;
private static boolean newlyCreated;
private static boolean ownerChanged = false;
private static boolean unsupportedVersion = false;
public static Config config() {
if (configInstance == null) {
@@ -72,6 +73,10 @@ public final class ConfigHolder {
return ownerChanged;
}
public static boolean isUnsupportedVersion() {
return unsupportedVersion;
}
public static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
@@ -82,7 +87,8 @@ public final class ConfigHolder {
LOG.info("Config location: " + configLocation);
configInstance = loadConfig();
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
if (!unsupportedVersion)
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
globalConfigInstance = loadGlobalConfig();
globalConfigInstance.addListener(source -> FileSaver.save(GLOBAL_CONFIG_PATH, globalConfigInstance.toJson()));
@@ -101,7 +107,6 @@ public final class ConfigHolder {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& configLocation.getFileSystem() == FileSystems.getDefault()
&& configLocation.toFile().canWrite()) {
LOG.warning("Config at " + configLocation + " is not writable, but it seems to be a Samba share or OpenJDK bug");
// There are some serious problems with the implementation of Samba or OpenJDK
throw new SambaException();
} else {
@@ -158,12 +163,19 @@ public final class ConfigHolder {
LOG.warning("Failed to get owner");
}
try {
String content = Files.readString(configLocation);
String content = FileUtils.readText(configLocation);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
ConfigUpgrader.upgradeConfig(deserialized, content);
int configVersion = deserialized.getConfigVersion();
if (configVersion < Config.CURRENT_VERSION) {
ConfigUpgrader.upgradeConfig(deserialized, content);
} else if (configVersion > Config.CURRENT_VERSION) {
unsupportedVersion = true;
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", Config.CURRENT_VERSION, configVersion));
}
return deserialized;
}
} catch (JsonParseException e) {
@@ -180,7 +192,7 @@ public final class ConfigHolder {
private static GlobalConfig loadGlobalConfig() throws IOException {
if (Files.exists(GLOBAL_CONFIG_PATH)) {
try {
String content = Files.readString(GLOBAL_CONFIG_PATH);
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
GlobalConfig deserialized = GlobalConfig.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
@@ -31,8 +31,6 @@ final class ConfigUpgrader {
private ConfigUpgrader() {
}
private static final int CURRENT_VERSION = 2;
/**
* This method is for the compatibility with old HMCL versions.
*
@@ -42,17 +40,10 @@ final class ConfigUpgrader {
static void upgradeConfig(Config deserialized, String rawContent) {
int configVersion = deserialized.getConfigVersion();
if (configVersion == CURRENT_VERSION) {
if (configVersion >= Config.CURRENT_VERSION)
return;
}
if (configVersion > CURRENT_VERSION) {
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
deserialized.setConfigVersion(CURRENT_VERSION);
return;
}
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, Config.CURRENT_VERSION));
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
if (configVersion < 1) {
@@ -100,6 +91,6 @@ final class ConfigUpgrader {
}
}
deserialized.setConfigVersion(CURRENT_VERSION);
deserialized.setConfigVersion(Config.CURRENT_VERSION);
}
}
@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.util.io.ResponseCodeException;
import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.Arrays;
import java.util.Map;
@@ -137,26 +137,26 @@ public final class DownloadProviders {
public static String localizeErrorMessage(Throwable exception) {
if (exception instanceof DownloadException) {
URI uri = ((DownloadException) exception).getUri();
URL url = ((DownloadException) exception).getUrl();
if (exception.getCause() instanceof SocketTimeoutException) {
return i18n("install.failed.downloading.timeout", uri);
return i18n("install.failed.downloading.timeout", url);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
return i18n("download.code." + responseCodeException.getResponseCode(), uri);
return i18n("download.code." + responseCodeException.getResponseCode(), url);
} else {
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception.getCause() instanceof FileNotFoundException) {
return i18n("download.code.404", uri);
return i18n("download.code.404", url);
} else if (exception.getCause() instanceof AccessDeniedException) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
} else {
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception instanceof ArtifactMalformedException) {
return i18n("exception.artifact_malformed");
@@ -21,6 +21,7 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -28,6 +29,9 @@ import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -148,33 +152,46 @@ public final class FontManager {
return null;
}
Font[] fonts = Font.loadFonts(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (fonts == null) {
LOG.warning("Failed to load font from " + path);
return null;
} else if (fonts.length == 0) {
LOG.warning("No fonts loaded from " + path);
return null;
}
if (JavaRuntime.CURRENT_VERSION >= 9) {
try {
MethodHandle methodHandle = MethodHandles.publicLookup().findStatic(Font.class, "loadFonts",
MethodType.methodType(Font[].class, String.class, double.class));
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(family)) {
return font;
}
}
Font[] fonts = (Font[]) methodHandle.invokeExact(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (fonts == null) {
LOG.warning("Failed to load font from " + path);
return null;
} else if (fonts.length == 0) {
LOG.warning("No fonts loaded from " + path);
return null;
}
if (family.indexOf(',') >= 0) {
for (String candidateFamily : family.split(",")) {
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
if (font.getFamily().equalsIgnoreCase(family)) {
return font;
}
}
if (family.indexOf(',') >= 0) {
for (String candidateFamily : family.split(",")) {
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
return font;
}
}
}
}
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
return fonts[0];
} catch (NoSuchMethodException | IllegalAccessException ignored) {
}
}
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
return fonts[0];
Font font = Font.loadFont(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (font == null)
LOG.warning("Failed to load font from " + path);
return font;
} catch (Throwable e) {
LOG.warning("Failed to get default font with fc-match", e);
return null;
@@ -21,7 +21,10 @@ import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
@@ -54,8 +57,6 @@ public final class GlobalConfig implements Observable {
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
private final StringProperty fontAntiAliasing = new SimpleStringProperty();
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
@@ -130,18 +131,6 @@ public final class GlobalConfig implements Observable {
enableOfflineAccount.set(value);
}
public StringProperty fontAntiAliasingProperty() {
return fontAntiAliasing;
}
public String getFontAntiAliasing() {
return fontAntiAliasing.get();
}
public void setFontAntiAliasing(String value) {
this.fontAntiAliasing.set(value);
}
public ObservableSet<String> getUserJava() {
return userJava;
}
@@ -157,8 +146,7 @@ public final class GlobalConfig implements Observable {
"logRetention",
"userJava",
"disabledJava",
"enableOfflineAccount",
"fontAntiAliasing"
"enableOfflineAccount"
));
@Override
@@ -171,7 +159,6 @@ public final class GlobalConfig implements Observable {
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
jsonObject.add("fontAntiAliasing", context.serialize(src.getFontAntiAliasing()));
if (src.enableOfflineAccount.get())
jsonObject.addProperty("enableOfflineAccount", true);
@@ -199,7 +186,6 @@ public final class GlobalConfig implements Observable {
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
config.setFontAntiAliasing(Optional.ofNullable(obj.get("fontAntiAliasing")).map(JsonElement::getAsString).orElse(null));
JsonElement userJava = obj.get("userJava");
if (userJava != null && userJava.isJsonArray()) {
@@ -18,15 +18,13 @@
package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.*;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -34,10 +32,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ProxyManager {
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
private static final ProxySelector SYSTEM_DEFAULT = Objects.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
private static volatile @NotNull ProxySelector defaultProxySelector = SYSTEM_DEFAULT;
private static volatile @Nullable SimpleAuthenticator defaultAuthenticator = null;
private static final ProxySelector SYSTEM_DEFAULT = Lang.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
private static ProxySelector getProxySelector() {
if (config().hasProxy()) {
@@ -58,13 +53,16 @@ public final class ProxyManager {
}
}
private static SimpleAuthenticator getAuthenticator() {
private static Authenticator getAuthenticator() {
if (config().hasProxy() && config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
if (username != null || password != null)
return new SimpleAuthenticator(username, password.toCharArray());
return new SimpleAuthenticator(
Lang.requireNonNullElse(username, ""),
Lang.requireNonNullElse(password, "").toCharArray()
);
else
return null;
} else
@@ -72,34 +70,15 @@ public final class ProxyManager {
}
static void init() {
ProxySelector.setDefault(new ProxySelector() {
@Override
public List<Proxy> select(URI uri) {
return defaultProxySelector.select(uri);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
defaultProxySelector.connectFailed(uri, sa, ioe);
}
});
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
var defaultAuthenticator = ProxyManager.defaultAuthenticator;
return defaultAuthenticator != null ? defaultAuthenticator.getPasswordAuthentication() : null;
}
});
defaultProxySelector = getProxySelector();
InvalidationListener updateProxySelector = observable -> defaultProxySelector = getProxySelector();
ProxySelector.setDefault(getProxySelector());
InvalidationListener updateProxySelector = observable -> ProxySelector.setDefault(getProxySelector());
config().proxyTypeProperty().addListener(updateProxySelector);
config().proxyHostProperty().addListener(updateProxySelector);
config().proxyPortProperty().addListener(updateProxySelector);
config().hasProxyProperty().addListener(updateProxySelector);
defaultAuthenticator = getAuthenticator();
InvalidationListener updateAuthenticator = observable -> defaultAuthenticator = getAuthenticator();
Authenticator.setDefault(getAuthenticator());
InvalidationListener updateAuthenticator = observable -> Authenticator.setDefault(getAuthenticator());
config().hasProxyProperty().addListener(updateAuthenticator);
config().hasProxyAuthProperty().addListener(updateAuthenticator);
config().proxyUserProperty().addListener(updateAuthenticator);
@@ -148,7 +127,7 @@ public final class ProxyManager {
}
@Override
public PasswordAuthentication getPasswordAuthentication() {
protected PasswordAuthentication getPasswordAuthentication() {
return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null;
}
}
@@ -23,6 +23,7 @@ import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.charset.Charset;
@@ -67,7 +68,7 @@ public final class StyleSheets {
Path temp = Files.createTempFile("hmcl", ".css");
// For JavaFX 17 or earlier, CssParser uses the default charset
// https://bugs.openjdk.org/browse/JDK-8279328
Files.writeString(temp, styleSheet, Charset.defaultCharset());
FileUtils.writeText(temp, styleSheet, Charset.defaultCharset());
temp.toFile().deleteOnExit();
return temp.toUri().toString();
} catch (IOException | NullPointerException e) {
@@ -42,6 +42,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.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -175,18 +176,6 @@ public final class Controllers {
public static void initialize(Stage stage) {
LOG.info("Start initializing application");
if (System.getProperty("prism.lcdtext") == null) {
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
LOG.info("Enable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "true");
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)
|| OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && SCREEN.getOutputScaleX() > 1) {
LOG.info("Disable sub-pixel antialiasing");
System.getProperties().put("prism.lcdtext", "false");
}
}
Controllers.stage = stage;
stageSizeChangeListener = o -> {
@@ -321,6 +310,26 @@ public final class Controllers {
}
}
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
Number shownTipVersion = null;
try {
shownTipVersion = (Number) config().getShownTips().get(JAVA_VERSION_TIP);
} catch (ClassCastException e) {
LOG.warning("Invalid type for shown tips key: " + JAVA_VERSION_TIP, e);
}
if (shownTipVersion == null || shownTipVersion.intValue() < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version"), null, MessageType.WARNING);
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
if (downloadLink != null)
builder.addHyperLink(i18n("fatal.deprecated_java_version.download_link", 21), downloadLink);
Controllers.dialog(builder
.ok(() -> config().getShownTips().put(JAVA_VERSION_TIP, Metadata.MINIMUM_SUPPORTED_JAVA_VERSION))
.build());
}
}
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
@@ -25,13 +25,10 @@ import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.WeakListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.*;
import javafx.collections.ObservableMap;
import javafx.event.Event;
import javafx.event.EventDispatcher;
import javafx.event.EventType;
@@ -45,7 +42,10 @@ import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
@@ -55,7 +55,7 @@ import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.jackhuang.hmcl.task.CacheFileTask;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
@@ -66,7 +66,6 @@ import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -81,8 +80,6 @@ import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.ref.WeakReference;
import java.net.*;
import java.nio.ByteBuffer;
@@ -113,11 +110,6 @@ public final class FXUtils {
public static final int JAVAFX_MAJOR_VERSION;
/// @see Platform.Preferences
public static final @Nullable ObservableMap<String, Object> PREFERENCES;
public static final @Nullable ObservableBooleanValue DARK_MODE;
public static final @Nullable Boolean REDUCED_MOTION;
static {
String jfxVersion = System.getProperty("javafx.version");
int majorVersion = -1;
@@ -128,40 +120,6 @@ public final class FXUtils {
}
}
JAVAFX_MAJOR_VERSION = majorVersion;
ObservableMap<String, Object> preferences = null;
ObservableBooleanValue darkMode = null;
Boolean reducedMotion = null;
if (JAVAFX_MAJOR_VERSION >= 22) {
try {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
Class<?> preferencesClass = Class.forName("javafx.application.Platform$Preferences");
@SuppressWarnings("unchecked")
var preferences0 = (ObservableMap<String, Object>) lookup.findStatic(Platform.class, "getPreferences", MethodType.methodType(preferencesClass))
.invoke();
preferences = preferences0;
@SuppressWarnings("unchecked")
var colorSchemeProperty =
(ReadOnlyObjectProperty<? extends Enum<?>>)
lookup.findVirtual(preferencesClass, "colorSchemeProperty", MethodType.methodType(ReadOnlyObjectProperty.class))
.invoke(preferences);
darkMode = Bindings.createBooleanBinding(() ->
"DARK".equals(colorSchemeProperty.get().name()), colorSchemeProperty);
if (JAVAFX_MAJOR_VERSION >= 24) {
reducedMotion = (boolean)
lookup.findVirtual(preferencesClass, "isReducedMotion", MethodType.methodType(boolean.class))
.invoke(preferences);
}
} catch (Throwable e) {
LOG.warning("Failed to get preferences", e);
}
}
PREFERENCES = preferences;
DARK_MODE = darkMode;
REDUCED_MOTION = reducedMotion;
}
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
@@ -171,8 +129,18 @@ public final class FXUtils {
);
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
private static final Map<String, Path> remoteImageCache = new ConcurrentHashMap<>();
public static void shutdown() {
for (Map.Entry<String, Path> entry : remoteImageCache.entrySet()) {
try {
Files.deleteIfExists(entry.getValue());
} catch (IOException e) {
LOG.warning(String.format("Failed to delete cache file %s.", entry.getValue()), e);
}
remoteImageCache.remove(entry.getKey());
}
builtinImageCache.clear();
}
@@ -365,15 +333,8 @@ public final class FXUtils {
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);
public static void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
tooltip.setShowDelay(showDelay);
tooltip.setShowDuration(showDuration);
tooltip.setHideDelay(hideDelay);
Tooltip.install(node, tooltip);
}
public static void installFastTooltip(Node node, Tooltip tooltip) {
runInFX(() -> installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
}
public static void installFastTooltip(Node node, String tooltip) {
@@ -381,7 +342,7 @@ public final class FXUtils {
}
public static void installSlowTooltip(Node node, Tooltip tooltip) {
runInFX(() -> installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
}
public static void installSlowTooltip(Node node, String tooltip) {
@@ -823,6 +784,21 @@ public final class FXUtils {
}
}
public static Image loadWebPImage(InputStream input,
int requestedWidth, int requestedHeight,
boolean preserveRatio, boolean smooth) throws IOException {
WebPImageReaderSpi spi = new WebPImageReaderSpi();
ImageReader reader = spi.createReaderInstance(null);
BufferedImage bufferedImage;
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
reader.setInput(imageInput, true, true);
bufferedImage = reader.read(0, reader.getDefaultReadParam());
} finally {
reader.dispose();
}
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
}
public static Image loadImage(Path path) throws Exception {
try (InputStream input = Files.newInputStream(path)) {
if ("webp".equalsIgnoreCase(FileUtils.getExtension(path)))
@@ -836,14 +812,50 @@ public final class FXUtils {
}
}
public static Image loadImage(URI uri) throws Exception {
URLConnection connection = NetworkUtils.createConnection(uri);
public static Image loadImage(Path path,
int requestedWidth, int requestedHeight,
boolean preserveRatio, boolean smooth) throws Exception {
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
String ext = FileUtils.getExtension(path).toLowerCase(Locale.ROOT);
if ("webp".equalsIgnoreCase(ext))
return loadWebPImage(Channels.newInputStream(channel),
requestedWidth, requestedHeight, preserveRatio, smooth);
if (!IMAGE_EXTENSIONS.contains(ext)) {
byte[] header = new byte[12];
ByteBuffer buffer = ByteBuffer.wrap(header);
//noinspection StatementWithEmptyBody
while (buffer.hasRemaining() && channel.read(buffer) > 0) {
}
channel.position(0L);
if (!buffer.hasRemaining()) {
// WebP File
if (header[0] == 'R' && header[1] == 'I' && header[2] == 'F' && header[3] == 'F' &&
header[8] == 'W' && header[9] == 'E' && header[10] == 'B' && header[11] == 'P') {
return loadWebPImage(Channels.newInputStream(channel),
requestedWidth, requestedHeight, preserveRatio, smooth);
}
}
}
Image image = new Image(Channels.newInputStream(channel),
requestedWidth, requestedHeight,
preserveRatio, smooth);
if (image.isError())
throw image.getException();
return image;
}
}
public static Image loadImage(URL url) throws Exception {
URLConnection connection = NetworkUtils.createConnection(url);
if (connection instanceof HttpURLConnection) {
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
}
try (InputStream input = connection.getInputStream()) {
String path = uri.getPath();
String path = url.getPath();
if (path != null && "webp".equalsIgnoreCase(StringUtils.substringAfterLast(path, '.')))
return loadWebPImage(input);
else {
@@ -895,46 +907,56 @@ public final class FXUtils {
}
}
public static Task<Image> getRemoteImageTask(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
return new CacheFileTask(URI.create(url))
.thenApplyAsync(file -> {
try (var channel = FileChannel.open(file, StandardOpenOption.READ)) {
var header = new byte[12];
var buffer = ByteBuffer.wrap(header);
public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
return Task.composeAsync(() -> {
Path currentPath = remoteImageCache.get(url);
if (currentPath != null) {
if (Files.isReadable(currentPath))
return Task.completed(currentPath);
//noinspection StatementWithEmptyBody
while (channel.read(buffer) > 0) {
// The file is unavailable or unreadable.
remoteImageCache.remove(url);
try {
Files.deleteIfExists(currentPath);
} catch (IOException e) {
LOG.warning("An exception encountered while deleting broken cached image file.", e);
}
}
Path newPath = Files.createTempFile("hmcl-net-resource-cache-", ".cache");
return new FileDownloadTask(NetworkUtils.toURL(url), newPath.toFile())
.thenSupplyAsync(() -> {
Path otherPath = remoteImageCache.putIfAbsent(url, newPath);
if (otherPath == null)
return newPath;
else {
// The image has been loaded in another task. Delete the image here in order not to pollute the tmp folder.
Files.delete(newPath);
return otherPath;
}
channel.position(0L);
if (!buffer.hasRemaining()) {
// WebP File
if (header[0] == 'R' && header[1] == 'I' && header[2] == 'F' && header[3] == 'F' &&
header[8] == 'W' && header[9] == 'E' && header[10] == 'B' && header[11] == 'P') {
WebPImageReaderSpi spi = new WebPImageReaderSpi();
ImageReader reader = spi.createReaderInstance(null);
BufferedImage bufferedImage;
try (ImageInputStream imageInput = ImageIO.createImageInputStream(Channels.newInputStream(channel))) {
reader.setInput(imageInput, true, true);
bufferedImage = reader.read(0, reader.getDefaultReadParam());
} finally {
reader.dispose();
}
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
}
}
Image image = new Image(Channels.newInputStream(channel), requestedWidth, requestedHeight, preserveRatio, smooth);
if (image.isError())
throw image.getException();
return image;
}
});
});
}).thenApplyAsync(path -> loadImage(path, requestedWidth, requestedHeight, preserveRatio, smooth));
}
public static ObservableValue<Image> newRemoteImage(String url, double requestedWidth, double requestedHeight, boolean preserveRatio, boolean smooth) {
var image = new SimpleObjectProperty<Image>();
/**
* Load image from the internet. It will cache the data of images for the further usage.
* The cached data will be deleted when HMCL is closed or hidden.
*
* @param url the url of image. The image resource should be a file on the internet.
* @param requestedWidth the image's bounding box width
* @param requestedHeight the image's bounding box height
* @param preserveRatio indicates whether to preserve the aspect ratio of
* the original image when scaling to fit the image within the
* specified bounding box
* @param smooth indicates whether to use a better quality filtering
* algorithm or a faster one when scaling this image to fit within
* the specified bounding box
* @return the image resource within the jar.
*/
public static ObservableValue<Image> newRemoteImage(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
SimpleObjectProperty<Image> image = new SimpleObjectProperty<>();
getRemoteImageTask(url, requestedWidth, requestedHeight, preserveRatio, smooth)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
@@ -22,6 +22,7 @@ import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.util.StringUtils;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
@@ -159,28 +160,19 @@ public final class HTMLRenderer {
private void appendImage(Node node) {
String src = node.absUrl("src");
URI imageUri = null;
try {
if (!src.isEmpty())
imageUri = URI.create(src);
} catch (Exception ignored) {
}
String alt = node.attr("alt");
if (imageUri != null) {
URI uri = URI.create(src);
if (StringUtils.isNotBlank(src)) {
String widthAttr = node.attr("width");
String heightAttr = node.attr("height");
double width = 0;
double height = 0;
int width = 0;
int height = 0;
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
try {
width = Double.parseDouble(widthAttr);
height = Double.parseDouble(heightAttr);
width = (int) Double.parseDouble(widthAttr);
height = (int) Double.parseDouble(heightAttr);
} catch (NumberFormatException ignored) {
}
@@ -191,7 +183,7 @@ public final class HTMLRenderer {
}
try {
Image image = FXUtils.getRemoteImageTask(uri.toString(), width, height, true, true)
Image image = FXUtils.getRemoteImageTask(src, width, height, true, true)
.run();
if (image == null)
throw new AssertionError("Image loading task returned null");
@@ -207,7 +199,7 @@ public final class HTMLRenderer {
children.add(imageView);
return;
} catch (Throwable e) {
LOG.warning("Failed to load image: " + uri, e);
LOG.warning("Failed to load image: " + src, e);
}
}
@@ -22,6 +22,7 @@ import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -312,7 +313,9 @@ public class InstallerItem extends Control {
private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
private static final WeakListenerHolder holder = new WeakListenerHolder();
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final ChangeListener<Number> holder;
InstallerItemSkin(InstallerItem control) {
super(control);
@@ -320,9 +323,14 @@ public class InstallerItem extends Control {
Pane pane;
if (control.style == Style.CARD) {
pane = new VBox();
holder.add(FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7)));
if (FXUtils.JAVAFX_MAJOR_VERSION == 8)
holder = FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
else
holder = FXUtils.onWeakChangeAndOperate(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
} else {
pane = new HBox();
holder = null;
}
pane.getStyleClass().add("installer-item");
RipplerContainer container = new RipplerContainer(pane);
@@ -38,6 +38,7 @@ import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
@@ -185,7 +186,7 @@ public final class LogWindow extends Stage {
}
private void onTerminateGame() {
LogWindow.this.gameProcess.stop();
LauncherHelper.stopManagedProcesses();
}
private void onClear() {
@@ -223,7 +224,7 @@ public final class LogWindow extends Stage {
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getProcess().pid(), dumpFile);
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
} catch (Throwable e) {
@@ -1,27 +1,7 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
package org.jackhuang.hmcl.ui;
import javax.swing.*;
/**
* @author Glavo
*/
public final class SwingUtils {
static {
if (System.getProperty("swing.defaultlaf") == null) {
@@ -0,0 +1,123 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public class TooltipInstaller {
public static final TooltipInstaller INSTALLER;
static {
TooltipInstaller installer = null;
try {
installer = new NewInstaller();
} catch (Throwable e) {
try {
installer = new OldInstaller();
} catch (Throwable e2) {
e2.addSuppressed(e);
LOG.warning("Failed to initialize TooltipInstaller", e2);
}
}
INSTALLER = installer != null ? installer : new TooltipInstaller();
}
TooltipInstaller() {
}
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
Tooltip.install(node, tooltip);
}
// For Java 8
private static final class OldInstaller extends TooltipInstaller {
private static final Constructor<?> createTooltipBehavior;
private static final Method installTooltipBehavior;
static {
try {
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
createTooltipBehavior.setAccessible(true);
installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installTooltipBehavior.setAccessible(true);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false);
installTooltipBehavior.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
LOG.warning("Failed to set tooltip show delay", e);
Tooltip.install(node, tooltip);
}
}
}
// For Java 9+
private static final class NewInstaller extends TooltipInstaller {
private static final MethodHandle setTooltipShowDelay;
private static final MethodHandle setTooltipShowDuration;
private static final MethodHandle setTooltipHideDelay;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodType methodType = MethodType.methodType(void.class, Duration.class);
setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType);
setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType);
setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
setTooltipShowDelay.invokeExact(tooltip, showDelay);
setTooltipShowDuration.invokeExact(tooltip, showDuration);
setTooltipHideDelay.invokeExact(tooltip, hideDelay);
} catch (Throwable e) {
LOG.warning("Failed to set tooltip show delay", e);
}
Tooltip.install(node, tooltip);
}
}
}
@@ -48,11 +48,22 @@ public final class UpgradeDialog extends JFXDialogLayout {
setBody(new ProgressIndicator());
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
boolean isPreview = remoteVersion.isPreview();
Task.supplyAsync(Schedulers.io(), () -> {
Document document = Jsoup.parse(new URL(url), 30 * 1000);
Node node = document.selectFirst("#nowchange");
String id = null;
Node node = null;
if (isPreview) {
id = "nowpreview";
node = document.selectFirst("#" + id);
}
if (node == null) {
id = "nowchange";
node = document.selectFirst("#" + id);
}
if (node == null || !"h1".equals(node.nodeName()))
throw new IOException("Cannot find #nowchange in document");
throw new IOException("Cannot find current changelog in document");
HTMLRenderer renderer = new HTMLRenderer(uri -> {
LOG.info("Open link: " + uri);
@@ -60,7 +71,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
});
do {
if ("h1".equals(node.nodeName()) && !"nowchange".equals(node.attr("id"))) {
if ("h1".equals(node.nodeName()) && !id.equals(node.attr("id"))) {
break;
}
renderer.appendNode(node);
@@ -78,15 +78,22 @@ public class AccountAdvancedListItem extends AdvancedListItem {
setActionButtonVisible(false);
setOnScroll(event -> {
double deltaY = event.getDeltaY();
if (deltaY == 0)
return;
Account current = account.get();
if (current == null) return;
ObservableList<Account> accounts = Accounts.getAccounts();
int currentIndex = accounts.indexOf(account.get());
if (event.getDeltaY() > 0) { // up
int currentIndex = accounts.indexOf(current);
if (currentIndex < 0) return;
if (deltaY > 0) // up
currentIndex--;
} else { // down
else // down
currentIndex++;
}
Accounts.setSelectedAccount(accounts.get((currentIndex + accounts.size()) % accounts.size()));
});
}
@@ -33,7 +33,6 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
@@ -129,10 +128,6 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
JFXButton btnRefresh = new JFXButton();
SpinnerPane spinnerRefresh = new SpinnerPane();
spinnerRefresh.getStyleClass().setAll("small-spinner-pane");
if (skinnable.getAccount() instanceof MicrosoftAccount && Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
btnRefresh.setDisable(true);
FXUtils.installFastTooltip(spinnerRefresh, i18n("account.methods.microsoft.snapshot"));
}
btnRefresh.setOnAction(e -> {
spinnerRefresh.showSpinner();
skinnable.refreshAsync()
@@ -31,10 +31,10 @@ import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.net.URI;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
@@ -196,7 +196,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
lblServerName.setText(serverBeingAdded.getName());
lblServerUrl.setText(serverBeingAdded.getUrl());
lblServerWarning.setVisible("http".equals(URI.create(serverBeingAdded.getUrl()).getScheme()));
lblServerWarning.setVisible("http".equals(NetworkUtils.toURL(serverBeingAdded.getUrl()).getProtocol()));
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT);
} else {
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.animation;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
/**
@@ -37,7 +36,7 @@ public final class AnimationUtils {
public static void init() {
}
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled() && FXUtils.REDUCED_MOTION != Boolean.TRUE;
private static final boolean ENABLED = !ConfigHolder.config().isAnimationDisabled();
private static final boolean PLAY_WINDOW_ANIMATION = ENABLED && !OperatingSystem.CURRENT_OS.isLinuxOrBSD();
public static boolean isAnimationEnabled() {
@@ -30,8 +30,7 @@ public class TransitionPane extends StackPane implements AnimationHandler {
private Duration duration;
private Node previousNode, currentNode;
{
currentNode = getChildren().stream().findFirst().orElse(null);
public TransitionPane() {
FXUtils.setOverflowHidden(this);
}
@@ -91,8 +90,12 @@ public class TransitionPane extends StackPane implements AnimationHandler {
private void updateContent(Node newView) {
if (getWidth() > 0 && getHeight() > 0) {
previousNode = currentNode;
if (previousNode == null)
previousNode = EMPTY_PANE;
if (previousNode == null) {
if (getChildren().isEmpty())
previousNode = EMPTY_PANE;
else
previousNode = getChildren().get(0);
}
} else
previousNode = EMPTY_PANE;
@@ -98,7 +98,7 @@ public final class MessageDialogPane extends HBox {
{
StackPane titlePane = new StackPane();
titlePane.getStyleClass().addAll("jfx-layout-heading", "title");
titlePane.getChildren().setAll(new Label(title != null ? title : i18n("message.info")));
titlePane.getChildren().setAll(new Label(title != null ? title : type.getDisplayName()));
StackPane content = new StackPane();
content.getStyleClass().add("jfx-layout-body");
@@ -26,7 +26,9 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jetbrains.annotations.NotNull;
@@ -41,7 +43,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class TaskExecutorDialogPane extends BorderPane {
private TaskExecutor executor;
private TaskCancellationAction onCancel;
private final Consumer<FetchTask.SpeedEvent> speedEventHandler;
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
private final Label lblTitle;
private final Label lblProgress;
@@ -106,7 +108,7 @@ public class TaskExecutorDialogPane extends BorderPane {
String finalUnit = unit;
Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit)));
};
FileDownloadTask.speedEvent.channel(FetchTask.SpeedEvent.class).registerWeak(speedEventHandler);
FileDownloadTask.speedEvent.channel(FileDownloadTask.SpeedEvent.class).registerWeak(speedEventHandler);
onEscPressed(this, btnCancel::fire);
}
@@ -261,6 +261,7 @@ public final class TaskListPane extends StackPane {
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
case "hmcl.install.quilt-api": message = i18n("install.installer.install", i18n("install.installer.quilt-api") + " " + stageValue); break;
default: message = i18n(stageKey); break;
}
// @formatter:on
@@ -58,7 +58,7 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -198,6 +198,8 @@ public class DecoratorController {
private Background getBackground() {
EnumBackgroundImage imageType = config().getBackgroundImageType();
if (imageType == null)
imageType = EnumBackgroundImage.DEFAULT;
Image image = null;
switch (imageType) {
@@ -214,7 +216,7 @@ public class DecoratorController {
String backgroundImageUrl = config().getBackgroundImageUrl();
if (backgroundImageUrl != null) {
try {
image = FXUtils.loadImage(URI.create(backgroundImageUrl));
image = FXUtils.loadImage(new URL(backgroundImageUrl));
} catch (Exception e) {
LOG.warning("Couldn't load background image", e);
}
@@ -124,7 +124,7 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
libraryPane.setVgap(16);
libraryPane.setHgap(16);
if (libraries.length <= 8) {
if (FXUtils.JAVAFX_MAJOR_VERSION < 25 && libraries.length <= 8) {
BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0));
root.setCenter(libraryPane);
} else {
@@ -133,6 +133,9 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa
scrollPane.setFitToHeight(true);
BorderPane.setMargin(scrollPane, new Insets(16, 0, 16, 0));
root.setCenter(scrollPane);
if (libraries.length <= 8)
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
}
}
@@ -52,10 +52,10 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Locale;
@@ -156,7 +156,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
Controllers.taskDialog(Task.composeAsync(() -> {
var task = new FileDownloadTask(URI.create(file.getFile().getUrl()), dest);
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
}).whenComplete(Schedulers.javafx(), exception -> {
@@ -18,11 +18,11 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
@@ -102,7 +102,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
} catch (MismatchedModpackTypeException e) {
Controllers.dialog(i18n("modpack.mismatched_type"), i18n("message.error"), MessageType.ERROR);
Controllers.dialog(i18n("modpack.mismatched_type", e.getRequired(), e.getFound()), i18n("message.error"), MessageType.ERROR);
} catch (IOException e) {
Controllers.dialog(i18n("modpack.invalid"), i18n("message.error"), MessageType.ERROR);
}
@@ -29,7 +29,9 @@ import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -41,7 +43,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
@@ -126,7 +128,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
private void onChooseRemoteFile() {
Controllers.prompt(i18n("modpack.choose.remote.tooltip"), (urlString, resolve, reject) -> {
try {
URI url = URI.create(urlString);
URL url = new URL(urlString);
if (urlString.endsWith("server-manifest.json")) {
// if urlString ends with .json, we assume that the url is server-manifest.json
Controllers.taskDialog(new GetTask(url).whenComplete(Schedulers.javafx(), (result, e) -> {
@@ -148,7 +150,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
resolve.run();
Controllers.taskDialog(
new FileDownloadTask(url, modpack, null)
new FileDownloadTask(url, modpack.toFile(), null)
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
resolve.run();
@@ -37,7 +37,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -133,28 +133,28 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException rce = (ResponseCodeException) exception.getCause();
int responseCode = rce.getResponseCode();
URI uri = rce.getUri();
URL url = rce.getUrl();
if (responseCode == 404)
message += i18n("download.code.404", uri);
message += i18n("download.code.404", url);
else
message += i18n("download.failed", uri, responseCode);
message += i18n("download.failed", url, responseCode);
} else {
message += StringUtils.getStackTrace(exception.getCause());
}
Controllers.dialog(message, i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception instanceof DownloadException) {
URI uri = ((DownloadException) exception).getUri();
URL url = ((DownloadException) exception).getUrl();
if (exception.getCause() instanceof SocketTimeoutException) {
Controllers.dialog(i18n("install.failed.downloading.timeout", uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.timeout", url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), uri), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("download.code." + responseCodeException.getResponseCode(), url), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
}
} else {
Controllers.dialog(i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
Controllers.dialog(i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR, next);
}
} else if (exception instanceof UnsupportedInstallationException) {
switch (((UnsupportedInstallationException) exception).getReason()) {
@@ -49,8 +49,6 @@ import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -69,6 +67,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -97,7 +96,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
private final StackPane center;
private final VersionList<?> versionList;
private Task<?> executor;
private CompletableFuture<?> executor;
private final HBox searchBar;
private final StringProperty queryString = new SimpleStringProperty();
@@ -309,7 +308,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
public void refresh() {
VersionList<?> currentVersionList = versionList;
root.setContent(spinner, ContainerAnimations.FADE);
executor = currentVersionList.refreshAsync(gameVersion).whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
executor = currentVersionList.refreshAsync(gameVersion).whenComplete((result, exception) -> {
if (exception == null) {
List<RemoteVersion> items = loadVersions();
@@ -339,7 +338,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
// https://github.com/HMCL-dev/HMCL/issues/938
System.gc();
});
executor.start();
}
@Override
@@ -350,9 +348,8 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@Override
public void cleanup(Map<String, Object> settings) {
settings.remove(libraryId);
// fixme
// if (executor != null)
// executor.cancel(true);
if (executor != null)
executor.cancel(true);
}
private void onRefresh() {
@@ -34,6 +34,9 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -114,8 +117,8 @@ public final class AboutPage extends StackPane {
return componentList;
}
try {
JsonArray array = JsonUtils.fromJsonFully(input, JsonArray.class);
try (Reader reader = new InputStreamReader(input, StandardCharsets.UTF_8)) {
JsonArray array = JsonUtils.GSON.fromJson(reader, JsonArray.class);
for (JsonElement element : array) {
JsonObject obj = element.getAsJsonObject();
@@ -363,7 +363,7 @@ public final class JavaDownloadDialog extends StackPane {
return getIntegrityCheck
.thenComposeAsync(integrityCheck ->
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
targetFile.toPath(), integrityCheck).setName(fileInfo.getFileName()))
targetFile, integrityCheck).setName(fileInfo.getFileName()))
.thenSupplyAsync(targetFile::toPath);
})
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
@@ -29,6 +29,7 @@ import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
@@ -61,12 +62,12 @@ import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.SINE;
@@ -124,16 +125,18 @@ public final class MainPage extends StackPane implements DecoratorPage {
titleBar.getStyleClass().add("title");
titleBar.setLeft(new Label(title));
JFXButton btnHide = new JFXButton();
btnHide.setOnAction(e -> {
announcementPane.setContent(new StackPane(), ContainerAnimations.FADE);
if (Metadata.isDev()) {
config().getShownTips().put(ANNOUNCEMENT, Metadata.VERSION);
Node hideNode = SVG.CLOSE.createIcon(Theme.blackFill(), 20);
hideNode.setCursor(Cursor.HAND);
titleBar.setRight(hideNode);
FXUtils.onClicked(hideNode, () -> {
if (announcementPane != null) {
if (Metadata.isDev()) {
config().getShownTips().put(ANNOUNCEMENT, Metadata.VERSION);
}
announcementPane.setContent(new StackPane(), ContainerAnimations.FADE);
}
});
btnHide.getStyleClass().add("announcement-close-button");
btnHide.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), 20));
titleBar.setRight(btnHide);
TextFlow body = FXUtils.segmentToTextFlow(content, Controllers::onHyperlinkAction);
body.setLineSpacing(4);
@@ -194,15 +197,17 @@ public final class MainPage extends StackPane implements DecoratorPage {
launchPane.setMaxWidth(230);
launchPane.setMaxHeight(55);
launchPane.setOnScroll(event -> {
int index = IntStream.range(0, versions.size())
.filter(i -> versions.get(i).getId().equals(getCurrentGame()))
.findFirst().orElse(-1);
double deltaY = event.getDeltaY();
if (deltaY == 0)
return;
String currentId = getCurrentGame();
int index = Lang.indexWhere(versions, instance -> instance.getId().equals(currentId));
if (index < 0) return;
if (event.getDeltaY() > 0) {
if (deltaY > 0) // up
index--;
} else {
else // down
index++;
}
profile.setSelectedVersion(versions.get((index + versions.size()) % versions.size()).getId());
});
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
@@ -276,10 +281,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
FXUtils.onClicked(menu, popup::hide);
versionNodes = MappedObservableList.create(versions, version -> {
Node node = PopupMenu.wrapPopupMenuItem(new GameItem(profile, version.getId()));
FXUtils.onClicked(node, () -> {
profile.setSelectedVersion(version.getId());
popup.hide();
});
FXUtils.onClicked(node, () -> profile.setSelectedVersion(version.getId()));
return node;
});
Bindings.bindContent(menu.getContent(), versionNodes);
@@ -18,13 +18,11 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.When;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ColorPicker;
@@ -36,7 +34,6 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
import org.jackhuang.hmcl.setting.EnumBackgroundImage;
import org.jackhuang.hmcl.setting.FontManager;
import org.jackhuang.hmcl.setting.Theme;
@@ -47,11 +44,8 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class PersonalizationPage extends StackPane {
@@ -222,46 +216,6 @@ public class PersonalizationPage extends StackPane {
fontPane.getContent().add(vbox);
}
{
BorderPane fontAntiAliasingPane = new BorderPane();
{
Label left = new Label(i18n("settings.launcher.font.anti_aliasing"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
fontAntiAliasingPane.setLeft(left);
}
{
@SuppressWarnings("unchecked")
JFXComboBox<Optional<FontSmoothingType>> cboAntiAliasing = new JFXComboBox<>(FXCollections.observableArrayList(
Optional.empty(),
Optional.of(FontSmoothingType.LCD),
Optional.of(FontSmoothingType.GRAY)
));
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
cboAntiAliasing.setValue(Optional.of(FontSmoothingType.LCD));
} else if ("gray".equalsIgnoreCase(fontAntiAliasing)) {
cboAntiAliasing.setValue(Optional.of(FontSmoothingType.GRAY));
} else {
cboAntiAliasing.setValue(Optional.empty());
}
cboAntiAliasing.setConverter(FXUtils.stringConverter(value -> {
if (value.isPresent()) {
return i18n("settings.launcher.font.anti_aliasing." + value.get().name().toLowerCase(Locale.ROOT));
} else {
return i18n("settings.launcher.font.anti_aliasing.auto");
}
}));
FXUtils.onChange(cboAntiAliasing.valueProperty(), value ->
globalConfig().setFontAntiAliasing(value.map(it -> it.name().toLowerCase(Locale.ROOT))
.orElse(null)));
fontAntiAliasingPane.setRight(cboAntiAliasing);
}
fontPane.getContent().add(fontAntiAliasingPane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.font")), fontPane);
}
}
@@ -106,9 +106,12 @@ public final class SettingsPage extends SettingsView {
chkUpdateStable.setUserData(UpdateChannel.STABLE);
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
updateChannel.set(UpdateChannel.getChannel());
updateChannel.addListener((a, b, newValue) -> {
UpdateChecker.requestCheckUpdate(newValue);
});
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
updateChannel.addListener(checkUpdateListener);
previewPane.selectedProperty().addListener(checkUpdateListener);
// ====
}
@@ -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.SupportedLocale;
@@ -56,6 +57,7 @@ public abstract class SettingsView extends StackPane {
protected final JFXRadioButton chkUpdateStable;
protected final JFXRadioButton chkUpdateDev;
protected final JFXButton btnUpdate;
protected final OptionToggleButton previewPane;
protected final ScrollPane scroll;
public SettingsView() {
@@ -145,6 +147,15 @@ public abstract class SettingsView extends StackPane {
settingsPane.getContent().add(updatePane);
}
{
previewPane = new OptionToggleButton();
previewPane.setTitle(i18n("update.preview"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
FXUtils.installFastTooltip(previewPane, i18n("update.preview.tooltip"));
settingsPane.getContent().add(previewPane);
}
{
fileCommonLocation = new MultiFileItem<>();
fileCommonLocationSublist = new ComponentSublist();
@@ -18,8 +18,9 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Locale;
@@ -52,7 +53,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
private final JFXComboBox<Renderer> cboRenderer;
public AdvancedVersionSettingPage(Profile profile, String versionId, VersionSetting versionSetting) {
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
this.profile = profile;
this.versionId = versionId;
this.versionSetting = versionSetting;
@@ -239,8 +240,27 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
useNativeOpenALPane.selectedProperty().bindBidirectional(versionSetting.useNativeOpenALProperty());
nativesDirItem.selectedDataProperty().bindBidirectional(versionSetting.nativesDirTypeProperty());
nativesDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
nativesDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> {
if (versionSetting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
String nativesDirName = "natives-" + Platform.SYSTEM_PLATFORM;
if (versionId == null) {
return String.format("%s/%s/%s",
profile.getRepository().getBaseDirectory().toPath().resolve("versions").toAbsolutePath().normalize(),
i18n("settings.advanced.natives_directory.default.version_id"),
nativesDirName
);
} else {
return profile.getRepository().getVersionRoot(versionId).toPath()
.toAbsolutePath().normalize()
.resolve(nativesDirName)
.toString();
}
} else if (versionSetting.getNativesDirType() == NativesDirectoryType.CUSTOM) {
return versionSetting.getNativesDir();
} else {
return null;
}
}, versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
}
void unbindProperties() {
@@ -68,7 +68,6 @@ import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
@@ -543,7 +542,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
.collect(Collectors.toList()));
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
LOG.debug("Icon: " + dataItem.getIconUrl());
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 40, 40, true, true));
}
}
@@ -49,12 +49,12 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -180,7 +180,7 @@ public class DownloadPage extends Control implements DecoratorPage {
Controllers.taskDialog(
Task.composeAsync(() -> {
var task = new FileDownloadTask(URI.create(file.getFile().getUrl()), dest.toPath(), file.getFile().getIntegrityCheck());
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest, file.getFile().getIntegrityCheck());
task.setName(file.getName());
return task;
}),
@@ -142,7 +142,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
toolbarNormal.getChildren().setAll(
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
createToolbarButton2(i18n("mods.add"), SVG.ADD, skinnable::add),
createToolbarButton2(i18n("folder.mod"), SVG.FOLDER_OPEN, skinnable::openModFolder),
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::openModFolder),
createToolbarButton2(i18n("mods.check_updates"), SVG.UPDATE, skinnable::checkUpdates),
createToolbarButton2(i18n("download"), SVG.DOWNLOAD, skinnable::download),
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar))
@@ -45,7 +45,7 @@ import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CSVTable;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
@@ -293,9 +293,9 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
if (isDisabled)
fileName += ModManager.DISABLED_EXTENSION;
var task = new FileDownloadTask(
URI.create(remote.getFile().getUrl()),
modManager.getModsDirectory().resolve(fileName));
FileDownloadTask task = new FileDownloadTask(
new URL(remote.getFile().getUrl()),
modManager.getModsDirectory().resolve(fileName).toFile());
task.setName(remote.getName());
return task;
@@ -28,7 +28,10 @@ import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.CreateAccountPane;
@@ -40,12 +43,12 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CancellationException;
@@ -73,18 +76,18 @@ public final class Versions {
public static void downloadModpackImpl(Profile profile, String version, RemoteMod.Version file) {
Path modpack;
URI downloadURL;
URL downloadURL;
try {
modpack = Files.createTempFile("modpack", ".zip");
downloadURL = new URI(file.getFile().getUrl());
} catch (IOException | URISyntaxException e) {
downloadURL = new URL(file.getFile().getUrl());
} catch (IOException e) {
Controllers.dialog(
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed.no_code"), MessageDialogPane.MessageType.ERROR);
return;
}
Controllers.taskDialog(
new FileDownloadTask(downloadURL, modpack)
new FileDownloadTask(downloadURL, modpack.toFile())
.whenComplete(Schedulers.javafx(), e -> {
if (e == null) {
if (version != null) {
@@ -107,8 +110,10 @@ public final class Versions {
public static void deleteVersion(Profile profile, String version) {
boolean isIndependent = profile.getVersionSetting(version).getGameDirType() == GameDirectoryType.VERSION_FOLDER;
boolean isMovingToTrashSupported = FileUtils.isMovingToTrashSupported();
String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) :
i18n("version.manage.remove.confirm.trash", version, version + "_removed");
isMovingToTrashSupported ? i18n("version.manage.remove.confirm.trash", version, version + "_removed") :
i18n("version.manage.remove.confirm", version);
JFXButton deleteButton = new JFXButton(i18n("button.delete"));
deleteButton.getStyleClass().add("dialog-error");
@@ -361,14 +361,27 @@ public final class WorldInfoPage extends SpinnerPane {
playerGameTypePane.setRight(gameTypeBox);
Tag tag = player.get("playerGameType");
Tag hardcoreTag = dataTag.get("hardcore");
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
if (tag instanceof IntTag) {
IntTag intTag = (IntTag) tag;
GameType gameType = GameType.of(intTag.getValue());
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
if (gameType != null) {
gameTypeBox.setValue(gameType);
gameTypeBox.valueProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
intTag.setValue(newValue.ordinal());
if (newValue == GameType.HARDCORE) {
intTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
if (hardcoreTag instanceof ByteTag) {
((ByteTag) hardcoreTag).setValue((byte) 1);
}
} else {
intTag.setValue(newValue.ordinal());
if (hardcoreTag instanceof ByteTag) {
((ByteTag) hardcoreTag).setValue((byte) 0);
}
}
saveLevelDat();
}
});
@@ -606,12 +619,13 @@ public final class WorldInfoPage extends SpinnerPane {
}
private enum GameType {
SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR;
SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR, HARDCORE;
static final ObservableList<GameType> items = FXCollections.observableList(Arrays.asList(values()));
static GameType of(int d) {
return d >= 0 && d <= items.size() ? items.get(d) : null;
static GameType of(int d, boolean hardcore) {
if (hardcore && d == 0) return HARDCORE; // hardcore + survival
return d >= 0 && d < 4 ? items.get(d) : null;
}
@Override
@@ -64,7 +64,7 @@ final class ExecutableHeaderHelper {
ZipEntry entry = zip.getEntry(location);
if (entry != null && !entry.isDirectory()) {
try (InputStream in = zip.getInputStream(entry)) {
return Optional.of(IOUtils.readFully(in));
return Optional.of(IOUtils.readFullyAsByteArray(in));
}
}
}
@@ -18,22 +18,16 @@
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.tukaani.xz.XZInputStream;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarOutputStream;
final class HMCLDownloadTask extends FileDownloadTask {
class HMCLDownloadTask extends FileDownloadTask {
private final RemoteVersion.Type archiveFormat;
private RemoteVersion.Type archiveFormat;
public HMCLDownloadTask(RemoteVersion version, Path target) {
super(URI.create(version.getUrl()), target, version.getIntegrityCheck());
super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck());
archiveFormat = version.getType();
}
@@ -42,28 +36,16 @@ final class HMCLDownloadTask extends FileDownloadTask {
super.execute();
try {
Path target = getPath();
Path target = getFile().toPath();
switch (archiveFormat) {
case JAR:
break;
case PACK_XZ:
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200Utils.unpack(in, out);
}
break;
default:
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
}
} catch (Throwable e) {
try {
Files.deleteIfExists(getPath());
} catch (Throwable e2) {
e.addSuppressed(e2);
}
getFile().delete();
throw e;
}
}
@@ -55,7 +55,7 @@ public final class IntegrityChecker {
if (in == null) {
throw new IOException("Public key not found");
}
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(IOUtils.readFully(in)));
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(IOUtils.readFullyAsByteArray(in)));
} catch (GeneralSecurityException e) {
throw new IOException("Failed to load public key", e);
}
@@ -76,7 +76,7 @@ public final class IntegrityChecker {
}
if (SIGNATURE_FILE.equals(filename)) {
signature = IOUtils.readFully(in);
signature = IOUtils.readFullyAsByteArray(in);
} else {
md.reset();
fileFingerprints.put(filename, DigestUtils.digest(md, in));
@@ -21,29 +21,23 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;
public class RemoteVersion {
public final class RemoteVersion {
public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException {
public static RemoteVersion fetch(UpdateChannel channel, boolean preview, String url) throws IOException {
try {
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(URI.create(url)), JsonObject.class);
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class);
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null);
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null);
String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null);
boolean force = Optional.ofNullable(response.get("force")).map(JsonElement::getAsBoolean).orElse(false);
if (Pack200Utils.isSupported() && packXZUrl != null && packXZHash != null) {
return new RemoteVersion(channel, version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash), force);
} else if (jarUrl != null && jarHash != null) {
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), force);
if (jarUrl != null && jarHash != null) {
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), preview, force);
} else {
throw new IOException("No download url is available");
}
@@ -57,14 +51,16 @@ public class RemoteVersion {
private final String url;
private final Type type;
private final IntegrityCheck integrityCheck;
private final boolean preview;
private final boolean force;
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean force) {
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean preview, boolean force) {
this.channel = channel;
this.version = version;
this.url = url;
this.type = type;
this.integrityCheck = integrityCheck;
this.preview = preview;
this.force = force;
}
@@ -88,6 +84,10 @@ public class RemoteVersion {
return integrityCheck;
}
public boolean isPreview() {
return preview;
}
public boolean isForce() {
return force;
}
@@ -98,7 +98,6 @@ public class RemoteVersion {
}
public enum Type {
PACK_XZ,
JAR
}
}
@@ -27,14 +27,15 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.IOException;
import java.util.LinkedHashMap;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class UpdateChecker {
private UpdateChecker() {}
private UpdateChecker() {
}
private static final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>();
private static final BooleanBinding outdated = Bindings.createBooleanBinding(
@@ -55,7 +56,7 @@ public final class UpdateChecker {
private static final ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
public static void init() {
requestCheckUpdate(UpdateChannel.getChannel());
requestCheckUpdate(UpdateChannel.getChannel(), config().isAcceptPreviewUpdate());
}
public static RemoteVersion getLatestVersion() {
@@ -82,16 +83,17 @@ public final class UpdateChecker {
return checkingUpdate.getReadOnlyProperty();
}
private static RemoteVersion checkUpdate(UpdateChannel channel) throws IOException {
private static RemoteVersion checkUpdate(UpdateChannel channel, boolean preview) throws IOException {
if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) {
throw new IOException("Self verification failed");
}
String url = NetworkUtils.withQuery(Metadata.HMCL_UPDATE_URL, mapOf(
pair("version", Metadata.VERSION),
pair("channel", channel.channelName)));
LinkedHashMap<String, String> query = new LinkedHashMap<>();
query.put("version", Metadata.VERSION);
query.put("channel", preview ? channel.channelName + "-preview" : channel.channelName);
return RemoteVersion.fetch(channel, url);
String url = NetworkUtils.withQuery(Metadata.HMCL_UPDATE_URL, query);
return RemoteVersion.fetch(channel, preview, url);
}
private static boolean isDevelopmentVersion(String version) {
@@ -99,7 +101,7 @@ public final class UpdateChecker {
version.contains("SNAPSHOT"); // eg. 3.5.SNAPSHOT
}
public static void requestCheckUpdate(UpdateChannel channel) {
public static void requestCheckUpdate(UpdateChannel channel, boolean preview) {
Platform.runLater(() -> {
if (isCheckingUpdate())
return;
@@ -108,8 +110,8 @@ public final class UpdateChecker {
thread(() -> {
RemoteVersion result = null;
try {
result = checkUpdate(channel);
LOG.info("Latest version (" + channel + ") is " + result);
result = checkUpdate(channel, preview);
LOG.info("Latest version (" + channel + ", preview=" + preview + ") is " + result);
} catch (IOException e) {
LOG.warning("Failed to check for update", e);
}
@@ -21,17 +21,18 @@ import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import javafx.application.Platform;
import org.jackhuang.hmcl.EntryPoint;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.UpgradeDialog;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -102,6 +103,16 @@ public final class UpdateHandler {
return;
}
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version.update"), i18n("message.error"), MessageType.ERROR);
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
if (downloadLink != null)
builder.addHyperLink(i18n("fatal.deprecated_java_version.download_link", 21), downloadLink);
builder.ok(null);
Controllers.dialog(builder.build());
return;
}
Controllers.dialog(new UpgradeDialog(version, () -> {
Path downloaded;
try {
@@ -125,7 +136,7 @@ public final class UpdateHandler {
}
requestUpdate(downloaded, getCurrentLocation());
EntryPoint.exit(0);
Main.exit(0);
} catch (IOException e) {
LOG.warning("Failed to update to " + version, e);
Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR));
@@ -264,7 +275,7 @@ public final class UpdateHandler {
Path hmclVersionJson = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("hmclver.json");
if (Files.isRegularFile(hmclVersionJson)) {
try {
Map<?, ?> content = new Gson().fromJson(Files.readString(hmclVersionJson), Map.class);
Map<?, ?> content = new Gson().fromJson(FileUtils.readText(hmclVersionJson), Map.class);
Object ver = content.get("ver");
if (ver instanceof String && ((String) ver).startsWith("3.")) {
Files.delete(hmclVersionJson);
@@ -30,9 +30,9 @@ public final class ChunkBaseApp {
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
private static final String[] SEED_MAP_GAME_VERSIONS = {
"1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3", "1.19",
"1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12", "1.11",
"1.10", "1.9", "1.8", "1.7"
"1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", "1.19.3",
"1.19", "1.18", "1.17", "1.16", "1.15", "1.14", "1.13", "1.12",
"1.11", "1.10", "1.9", "1.8", "1.7"
};
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.net.URI;
import java.util.HashMap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -123,7 +122,7 @@ public final class CrashReporter implements Thread.UncaughtExceptionHandler {
map.put("version", Metadata.VERSION);
map.put("log", LOG.getLogs());
try {
String response = NetworkUtils.doPost(URI.create(Metadata.PUBLISH_URL + "/hmcl/crash.php"), map);
String response = NetworkUtils.doPost(NetworkUtils.toURL(Metadata.PUBLISH_URL + "/hmcl/crash.php"), map);
if (StringUtils.isNotBlank(response))
LOG.error("Crash server response: " + response);
} catch (IOException ex) {
@@ -17,16 +17,11 @@
*/
package org.jackhuang.hmcl.util;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* Utility for Adding JavaFX to module path.
*
@@ -36,43 +31,7 @@ public final class JavaFXPatcher {
private JavaFXPatcher() {
}
/**
* Add JavaFX to module path at runtime.
*
* @param modules All module names
* @param jarPaths All jar paths
* @throws ReflectiveOperationException When the call to add these jars to the system module path failed.
*/
public static void patch(Set<String> modules, Path[] jarPaths, String[] addOpens) throws ReflectiveOperationException {
// Find all modules
ModuleFinder finder = ModuleFinder.of(jarPaths);
// Load all modules as unnamed module
for (ModuleReference mref : finder.findAll()) {
((jdk.internal.loader.BuiltinClassLoader) ClassLoader.getSystemClassLoader()).loadModule(mref);
}
// Define all modules
Configuration config = Configuration.resolveAndBind(finder, List.of(ModuleLayer.boot().configuration()), finder, modules);
ModuleLayer layer = ModuleLayer.defineModules(config, List.of(ModuleLayer.boot()), name -> ClassLoader.getSystemClassLoader()).layer();
// Add-Exports and Add-Opens
try {
// Some hacks
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
MethodHandle handle = lookup.findVirtual(Module.class, "implAddOpensToAllUnnamed", MethodType.methodType(void.class, String.class));
for (String target : addOpens) {
String[] name = target.split("/", 2); // <module>/<package>
layer.findModule(name[0]).ifPresent(m -> {
try {
handle.invokeWithArguments(m, name[1]);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
} catch (Throwable t) {
throw new ReflectiveOperationException(t);
}
public static void patch(Set<String> modules, Path[] jarPaths, String[] addOpens) {
LOG.info("No need to patch JavaFX with Java 8");
}
}
@@ -15,16 +15,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl;
package org.jackhuang.hmcl.util;
/**
* This is a dummy class and will be overwritten in the shadow jar.
* @author Glavo
*/
public final class EntryPoint {
private EntryPoint() {
public final class ModuleHelper {
public static void addEnableNativeAccess(Class<?> clazzInModule) {
// do nothing
}
public static void main(String[] args) {
throw new AssertionError("This method should not be called, please verify that the HMCL is complete.");
private ModuleHelper() {
}
}
@@ -26,7 +26,10 @@ import org.jackhuang.hmcl.util.platform.Architecture;
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.platform.windows.WindowsVersion;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -178,7 +181,19 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
public static Library getMesaLoader(JavaRuntime javaVersion) {
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull WindowsVersion windowsVersion) {
if (renderer == Renderer.DEFAULT)
return null;
if (windowsVersion.compareTo(WindowsVersion.WINDOWS_10) >= 0) {
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
} else if (windowsVersion.compareTo(WindowsVersion.WINDOWS_7) >= 0) {
if (renderer == Renderer.LLVMPIPE)
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
else
return null;
} else {
return null;
}
}
}
@@ -41,8 +41,9 @@
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.EntryPoint;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.IOUtils;
@@ -180,6 +181,12 @@ public final class SelfDependencyPatcher {
// We are probably on 8 and its on 11
throw new 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 (JavaRuntime.CURRENT_VERSION < 11) {
throw new IncompatibleVersionException();
}
SelfDependencyPatcher patcher = new SelfDependencyPatcher();
@@ -239,7 +246,7 @@ public final class SelfDependencyPatcher {
}
} else {
LOG.info("User choose not to download JavaFX");
EntryPoint.exit(0);
Main.exit(0);
}
throw new AssertionError();
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.util.i18n;
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 java.io.IOException;
@@ -115,7 +116,10 @@ public final class Locales {
if (resourceBundle == null) {
if (this != DEFAULT && this.locale == DEFAULT.locale) {
bundle = DEFAULT.getResourceBundle();
} 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
bundle = ResourceBundle.getBundle("assets.lang.I18N", locale);
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
package org.jackhuang.hmcl.util.i18n;
import java.io.IOException;
import java.io.InputStream;
@@ -28,9 +28,11 @@ import java.util.ResourceBundle;
import static java.nio.charset.StandardCharsets.UTF_8;
public final class UTF8Control extends ResourceBundle.Control {
final class UTF8Control extends ResourceBundle.Control {
public UTF8Control() {}
public static final UTF8Control INSTANCE = new UTF8Control();
private UTF8Control() {}
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IOException {
@@ -0,0 +1,78 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
/**
* Utility for Adding JavaFX to module path.
*
* @author ZekerZhayard
*/
public final class JavaFXPatcher {
private JavaFXPatcher() {
}
/**
* Add JavaFX to module path at runtime.
*
* @param modules All module names
* @param jarPaths All jar paths
* @throws ReflectiveOperationException When the call to add these jars to the system module path failed.
*/
public static void patch(Set<String> modules, Path[] jarPaths, String[] addOpens) throws ReflectiveOperationException {
// Find all modules
ModuleFinder finder = ModuleFinder.of(jarPaths);
// Load all modules as unnamed module
for (ModuleReference mref : finder.findAll()) {
((jdk.internal.loader.BuiltinClassLoader) ClassLoader.getSystemClassLoader()).loadModule(mref);
}
// Define all modules
Configuration config = Configuration.resolveAndBind(finder, List.of(ModuleLayer.boot().configuration()), finder, modules);
ModuleLayer layer = ModuleLayer.defineModules(config, List.of(ModuleLayer.boot()), name -> ClassLoader.getSystemClassLoader()).layer();
// Add-Exports and Add-Opens
try {
// Some hacks
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
MethodHandle handle = lookup.findVirtual(Module.class, "implAddOpensToAllUnnamed", MethodType.methodType(void.class, String.class));
for (String target : addOpens) {
String[] name = target.split("/", 2); // <module>/<package>
layer.findModule(name[0]).ifPresent(m -> {
try {
handle.invokeWithArguments(m, name[1]);
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
});
}
} catch (Throwable t) {
throw new ReflectiveOperationException(t);
}
}
}
@@ -0,0 +1,46 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
/**
* @author Glavo
*/
public final class ModuleHelper {
public static void addEnableNativeAccess(Class<?> clazzInModule) {
Module module = clazzInModule.getModule();
if (module.isNamed()) {
try {
MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(Module.class, MethodHandles.lookup());
MethodHandle implAddEnableNativeAccess = lookup.findVirtual(Module.class, "implAddEnableNativeAccess", MethodType.methodType(Module.class));
Module ignored = (Module) implAddEnableNativeAccess.invokeExact(module);
} catch (Throwable e) {
e.printStackTrace(System.err);
}
} else {
System.err.println("TODO: Add enable native access for anonymous modules is not yet supported");
}
}
private ModuleHelper() {
}
}
@@ -0,0 +1,23 @@
package org.jackhuang.hmcl.util.logging;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
/**
* @author Glavo
*/
final class CallerFinder {
private static final String PACKAGE_PREFIX = CallerFinder.class.getPackageName() + ".";
private static final Predicate<StackWalker.StackFrame> PREDICATE = stackFrame -> !stackFrame.getClassName().startsWith(PACKAGE_PREFIX);
private static final Function<Stream<StackWalker.StackFrame>, Optional<StackWalker.StackFrame>> FUNCTION = stream -> stream.filter(PREDICATE).findFirst();
private static final Function<StackWalker.StackFrame, String> FRAME_MAPPING = frame -> frame.getClassName() + "." + frame.getMethodName();
static String getCaller() {
return StackWalker.getInstance().walk(FUNCTION).map(FRAME_MAPPING).orElse(null);
}
private CallerFinder() {
}
}
+12 -17
View File
@@ -106,14 +106,26 @@
-fx-background-color: -fx-base-darker-color;
}
.memory-used:disabled {
-fx-opacity: 0.4;
}
.memory-allocate {
-fx-background-color: derive(-fx-base-color, 100%);
}
.memory-allocate:disabled {
-fx-opacity: 0.4;
}
.memory-total {
-fx-background-color: -fx-base-rippler-color;
}
.memory-total:disabled {
-fx-opacity: 0.4;
}
.update-label {
-fx-text-fill: red;
}
@@ -1080,23 +1092,6 @@
-jfx-mask-type: CIRCLE;
}
.announcement-close-button {
-fx-announcement-close-button-size: 20px;
-fx-pref-width: -fx-announcement-close-button-size;
-fx-max-width: -fx-announcement-close-button-size;
-fx-min-width: -fx-announcement-close-button-size;
-fx-pref-height: -fx-announcement-close-button-size;
-fx-max-height: -fx-announcement-close-button-size;
-fx-min-height: -fx-announcement-close-button-size;
-fx-background-radius: 50px;
-fx-background-color: transparent;
}
.announcement-close-button .jfx-rippler {
-jfx-rippler-fill: -fx-base-check-color;
-jfx-mask-type: CIRCLE;
}
.transparent {
-fx-background-color: null;
}
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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
@@ -21,7 +21,7 @@
about=About
about.copyright=Copyright
about.copyright.statement=Copyright © 2025 huangyuhui
about.copyright.statement=Copyright © 2013-2026 huangyuhui and contributors.
about.author=Author
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -183,6 +183,7 @@ button.export=Export
button.no=No
button.ok=OK
button.ok.countdown=OK (%d)
button.reveal_dir=Reveal
button.refresh=Refresh
button.remove=Remove
button.remove.confirm=Are you sure you want to permanently remove it? This action cannot be undone!
@@ -387,6 +388,9 @@ fatal.config_loading_failure=Cannot load configuration files.\n\
For macOS, try putting HMCL somewhere with permissions other than "Desktop", "Downloads", and "Documents" and try again.
fatal.config_loading_failure.unix=Hello Minecraft! Launcher could not load the configuration file because it was created by user "%1$s".\n\
Please open HMCL as root user (not recommended), or execute the following command in the terminal to change the ownership of the configuration file to the current user:\n%2$s
fatal.config_unsupported_version=The current configuration file was created by a newer version of Hello Minecraft! Launcher, and this version of HMCL cannot load it properly.\n\
Please update and restart HMCL.\n\
Before updating the launcher, any settings you modify will not be saved.
fatal.mac_app_translocation=Hello Minecraft! Launcher is isolated to a temporary directory by the OS due to macOS security mechanisms.\n\
Please move HMCL to a different directory before attempting to open. Otherwise, your settings and game data may be lost after restarting.\n\
Do you still want to continue?
@@ -398,6 +402,16 @@ fatal.apply_update_failure=We are sorry, but Hello Minecraft! Launcher is unable
fatal.apply_update_need_win7=Hello Minecraft! Launcher cannot automatically update on Windows XP/Vista.\n\
\n\
You can update manually by downloading a newer launcher version from %s.
fatal.deprecated_java_version=HMCL will require Java 17 or later to run in the future, but will still support launching games with Java 6~16.\n\
\n\
It is recommended to install the latest version of Java to ensure that HMCL works properly.\n\
\n\
You can continue to keep the old version of Java. HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
fatal.deprecated_java_version.update=HMCL will require Java 17 or later to run in the future. Please install the latest version of Java to ensure that HMCL can complete the upgrade.\n\
\n\
You can continue to keep the old version of Java.\
HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
fatal.deprecated_java_version.download_link=Download Java %d
fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive, some features might not be working. Please try updating your Java or moving the launcher to another directory.
fatal.illegal_char=Your user path contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
@@ -442,7 +456,7 @@ game.crash.reason.analyzing=Analyzing...
game.crash.reason.multiple=Several reasons detected:\n\n
game.crash.reason.block=The game crashed because of a block in the world.\n\
\n\
You can try removing this block using MCEdit or deleting the mod that added it.\n\
You can try removing this block using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
\n\
Block Type: %1$s\n\
Location: %2$s
@@ -458,7 +472,7 @@ game.crash.reason.duplicated_mod=The game cannot continue to run because of dupl
Each mod can only be installed once. Please delete the duplicate mod and try again.
game.crash.reason.entity=The game crashed because of an entity in the world.\n\
\n\
You can try removing the entity using MCEdit or deleting the mod that added it.\n\
You can try removing the entity using the <a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">map editors</a> or deleting the mod that added it.\n\
\n\
Entity Type: %1$s\n\
Location: %2$s
@@ -851,6 +865,7 @@ message.info=Information
message.success=Operation successfully completed
message.unknown=Unknown
message.warning=Warning
message.question=Question
modpack=Modpacks
modpack.choose=Choose Modpack
@@ -859,7 +874,7 @@ modpack.choose.local.detail=You can drag the modpack file here.
modpack.choose.remote=Download from URL
modpack.choose.remote.detail=A direct download link to the remote modpack file is required.
modpack.choose.repository=Download Modpack from CurseForge or Modrinth
modpack.choose.repository.detail=Remember to go back to this page and drop the modpack file here after the modpack is downloaded.
modpack.choose.repository.detail=You can choose the desired modpack on the next page.
modpack.choose.remote.tooltip=Please enter your modpack URL
modpack.completion=Downloading dependencies
modpack.desc=Describe your modpack, including an introduction and probably some changelog. Markdown and images from URL are currently supported.
@@ -1032,7 +1047,7 @@ mods.category=Category
mods.channel.alpha=Alpha
mods.channel.beta=Beta
mods.channel.release=Release
mods.check_updates=Check for Updates
mods.check_updates=Update
mods.check_updates.current_version=Current Version
mods.check_updates.empty=All mods are up-to-date
mods.check_updates.failed_check=Failed to check for updates.
@@ -1132,6 +1147,7 @@ world.info.player.food_level=Hunger Level
world.info.player.game_type=Game Mode
world.info.player.game_type.adventure=Adventure
world.info.player.game_type.creative=Creative
world.info.player.game_type.hardcore=Hardcore
world.info.player.game_type.spectator=Spectator
world.info.player.game_type.survival=Survival
world.info.player.health=Health
@@ -1258,6 +1274,7 @@ settings.advanced.natives_directory=Native Library Path
settings.advanced.natives_directory.choose=Choose the location of the desired native library
settings.advanced.natives_directory.custom=Custom
settings.advanced.natives_directory.default=Default
settings.advanced.natives_directory.default.version_id=<Version ID>
settings.advanced.natives_directory.hint=This option is intended only for users of Apple silicon or other not officially supported platforms. Please do not edit this option unless you know what you are doing.\n\
\n\
Before proceeding, please make sure all libraries (e.g. lwjgl.dll, libopenal.so) are provided in your desired directory.\n\
@@ -1327,10 +1344,6 @@ settings.launcher.download_source=Download Source
settings.launcher.download_source.auto=Automatically Choose Download Sources
settings.launcher.enable_game_list=Show instance list in homepage
settings.launcher.font=Font
settings.launcher.font.anti_aliasing=Anti-aliasing
settings.launcher.font.anti_aliasing.auto=Auto
settings.launcher.font.anti_aliasing.gray=Grayscale
settings.launcher.font.anti_aliasing.lcd=Sub-pixel
settings.launcher.general=General
settings.launcher.language=Language (Applies After Restart)
settings.launcher.launcher_log.export=Export Launcher Logs
@@ -1412,6 +1425,8 @@ update.note=Beta and Nightly channels may have more features or fixes, but they
update.latest=This is the latest version
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
update.tooltip=Update
update.preview=Preview HMCL releases early
update.preview.tooltip=Enable this option to receive new versions of HMCL early for testing before their official release.
version=Games
version.name=Instance Name
@@ -1443,6 +1458,7 @@ version.manage.manage=Edit Instance
version.manage.manage.title=Edit Instance - %1s
version.manage.redownload_assets_index=Update Game Assets
version.manage.remove=Delete Instance
version.manage.remove.confirm=Are you sure you want to permanently remove the instance "%s"? This operation cannot be undone!!!
version.manage.remove.confirm.trash=Are you sure you want to remove the instance "%s"? You can still find its files in your recycle bin by the name of "%s".
version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its saves and other data. Do you still want to delete the instance "%s"?
version.manage.remove_assets=Delete All Assets
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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
@@ -21,7 +21,6 @@
about=Acerca de
about.copyright=Copyright
about.copyright.statement=Copyright © 2025 huangyuhui.
about.author=Autor
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -184,6 +183,7 @@ button.export=Exportar
button.no=No
button.ok=Aceptar
button.ok.countdown=Aceptar (%d)
button.reveal_dir=Carpeta
button.refresh=Refrescar
button.remove=Eliminar
button.remove.confirm=¿Estás seguro de que deseas eliminarlo de forma permanente? ¡Esta acción no se puede deshacer!
@@ -400,6 +400,17 @@ fatal.apply_update_failure=Lo sentimos, pero Hello Minecraft! Launcher no puede
fatal.apply_update_need_win7=Hello Minecraft! Launcher no puede actualizarse automáticamente en Windows XP/Vista.\n\
\n\
Puedes actualizar manualmente descargando una versión más reciente del launcher desde %s.
fatal.deprecated_java_version=HMCL requerirá Java 17 o posterior para funcionar en el futuro, pero seguirá siendo compatible con el lanzamiento de juegos con Java 6~16.\n\
\n\
Se recomienda instalar la última versión de Java para garantizar el correcto funcionamiento de HMCL.\n\
\n\
Puedes conservar la versión antigua de Java. HMCL puede reconocer y gestionar varias versiones de Java y elegirá automáticamente la adecuada para usted en función de la versión del juego.
fatal.deprecated_java_version.update=HMCL requerirá Java 17 o posterior para funcionar en el futuro. Por favor, instalar la última versión de Java para asegurarse de que el lanzador puede completar la actualización.\n\
\n\
Puedes conservar la versión antigua de Java.\
HMCL puede reconocer y gestionar varias versiones de Java \
y elegirá automáticamente la adecuada para usted en función de la versión del juego.
fatal.deprecated_java_version.download_link=Descargar Java %d
fatal.samba=Si ha abierto Hello Minecraft! Launcher desde una unidad de red Samba, es posible que algunas funciones no funcionen. Intenta actualizar tu Java o mover el launcher a otro directorio.
fatal.illegal_char=Su ruta de usuario contiene un carácter ilegal «=», por lo que algunas características podrían no funcionar correctamente.\n\
Por ejemplo, no podrá utilizar authlib-injector o cambiar el skin de su cuenta offline.
@@ -443,12 +454,6 @@ game.crash.info=Información de colgado
game.crash.reason=Motivo del colgado
game.crash.reason.analyzing=Analizando...
game.crash.reason.multiple=Se han detectado varias razones:\n\n
game.crash.reason.block=El juego se ha colgado debido a un bloqueo en el mundo.\n\
\n\
Puedes intentar eliminar este bloque usando MCEdit o borrando el mod que lo añadió.\n\
\n\
Tipo de bloque: %1$s\n\
Ubicación: %2$s
game.crash.reason.bootstrap_failed=El juego se ha colgado debido al mod «%1$s».\n\
\n\
Puedes intentar borrarlo o actualizarlo.
@@ -462,12 +467,6 @@ game.crash.reason.duplicated_mod=El juego no puede iniciarse debido a la existen
%2$s\n\
\n\
Cada mod sólo puede instalarse una vez, por favor, elimine el mod duplicado e inténtelo de nuevo.
game.crash.reason.entity=El juego se ha colgado debido a una entidad en el mundo.\n\
\n\
Puedes intentar eliminar la entidad usando MCEdit o borrando el mod que la añadió.\n\
\n\
Tipo de entidad: %1$s\n\
Ubicación: %2$s
game.crash.reason.fabric_version_0_12=La versión de Fabric Loader 0.12 o posterior es incompatible con los mods instalados actualmente. Necesitas bajarlo a la versión 0.11.7.
game.crash.reason.fabric_warnings=El cargador de Fabric Loader ha advertido:\n\
\n\
@@ -488,7 +487,7 @@ game.crash.reason.graphics_driver=El juego se ha colgado debido a un problema co
Si tu ordenador tiene una tarjeta gráfica discreta, tienes que comprobar si el juego utiliza gráficos integrados/núcleo. Si es así, inicie el launcher utilizando su tarjeta gráfica discreta. Si el problema persiste, probablemente debería considerar la posibilidad de adquirir una nueva tarjeta gráfica o un nuevo ordenador.\n\
\n\
Si utiliza su tarjeta gráfica integrada, tenga en cuenta que Minecraft 1.16.5 o anterior requiere <a href="https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html">Java 1.8.0_51 o más antiguo</a> para la serie de procesadores Intel(R) Core(TM) 3000 o anteriores.
game.crash.reason.macos_failed_to_find_service_port_for_display=El juego no puede continuar debido a un fallo al inicializar la ventana OpenGL en la plataforma Apple silicon.\n\
game.crash.reason.macos_failed_to_find_service_port_for_display=El juego no puede continuar debido a un fallo al inicializar la ventana OpenGL en la plataforma con chips de Apple.\n\
\n\
Para este problema, HMCL no tiene soluciones directas por el momento. Intente abrir cualquier navegador y ponerlo en pantalla completa, luego vuelva a HMCL, inicie el juego y <b>vuelva rápidamente a la página del navegador</b> antes de que aparezca la ventana del juego, espere a que aparezca la ventana del juego y luego vuelva a la ventana del juego.
game.crash.reason.illegal_access_error=El juego se ha colgado por culpa de algún(os) mod(s).\n\
@@ -865,7 +864,7 @@ modpack.choose.remote=Descargar desde Internet
modpack.choose.remote.detail=Se requiere un enlace de descarga directa al archivo modpack remoto
modpack.choose.remote.tooltip=Por favor, introduzca la URL de su modpack
modpack.choose.repository=Descargar Modpack de CurseForge o Modrinth
modpack.choose.repository.detail=Recuerda volver a esta página y soltar aquí el archivo del modpack una vez descargado.
modpack.choose.repository.detail=Puedes elegir el modpack que desees en la página siguiente.
modpack.completion=Descargando dependencias
modpack.desc=Describa su modpack, incluyendo una introducción y probablemente algún registro de cambios. Actualmente se admiten Markdown e imágenes desde URL.
modpack.description=Descripción
@@ -1037,7 +1036,7 @@ mods.category=Categoría
mods.channel.alpha=Alpha
mods.channel.beta=Beta
mods.channel.release=Release
mods.check_updates=Comprobar actualizaciones
mods.check_updates=Actualizar
mods.check_updates.current_version=Versión actual
mods.check_updates.empty=Todos los mods están actualizados
mods.check_updates.failed_check=No se ha podido comprobar si hay actualizaciones.
@@ -1263,7 +1262,7 @@ settings.advanced.natives_directory=Ruta de la biblioteca nativa
settings.advanced.natives_directory.choose=Elija la ubicación de la biblioteca nativa deseada
settings.advanced.natives_directory.custom=Personalizado
settings.advanced.natives_directory.default=Por defecto
settings.advanced.natives_directory.hint=Esta opción está pensada sólo para usuarios de Apple M1 u otras plataformas no soportadas oficialmente. Por favor, no modifique esta opción a menos que sepa lo que está haciendo.\n\
settings.advanced.natives_directory.hint=Esta opción está pensada sólo para usuarios de chips de Apple u otras plataformas no soportadas oficialmente. Por favor, no modifique esta opción a menos que sepa lo que está haciendo.\n\
\n\
Antes de proceder, por favor, asegúrese de que todas las bibliotecas (por ejemplo, lwjgl.dll, libopenal.so) se proporcionan en su directorio deseado.
settings.advanced.no_jvm_args=No añadir argumentos JVM por defecto
@@ -1408,6 +1407,8 @@ update.note=Advertencia: Las versiones beta y las versiones nocturnas pueden ten
update.latest=Esta es la última versión.
update.no_browser=No se puede abrir en el navegador del sistema. Pero, hemos copiado el enlace a su portapapeles y puede abrirlo manualmente.
update.tooltip=Actualización
update.preview=Vista previa de actualizaciones anticipadas
update.preview.tooltip=Activa esta opción para recibir nuevas versiones del lanzador antes de su lanzamiento oficial para probarlas
version=Juegos
version.name=Nombre de instancia
@@ -1439,6 +1440,7 @@ version.manage.manage=Editar instancia
version.manage.manage.title=Editar instancia - %1s
version.manage.redownload_assets_index=Actualizar activos del juego
version.manage.remove=Eliminar instancia
version.manage.remove.confirm=¿Está seguro de que quiere eliminar permanentemente la instancia «%s»? ¡Esta operación no se puede deshacer!
version.manage.remove.confirm.trash=¿Estás seguro de que quieres eliminar la instancia «%s»? Todavía puedes encontrar sus archivos en tu papelera de reciclaje con el nombre de «%s».
version.manage.remove.confirm.independent=Dado que esta instancia está almacenada en un directorio aislado, al eliminarla también se eliminarán sus guardados y otros datos. ¿Aún quieres borrar la instancia %s?
version.manage.remove_assets=Borrar todas las activos del juego
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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
@@ -20,7 +20,6 @@
about=について
about.copyright=著作権
about.copyright.statement=Copyright © 2025 huangyuhui
about.author=著者
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -352,13 +351,11 @@ game.crash.info=ゲームステータス
game.crash.reason=クラッシュアナライザー
game.crash.reason.analyzing=分析中..
game.crash.reason.multiple=複数の理由が検出されました:\n\n
game.crash.reason.block=ブロックが原因でゲームを実行できません。\nMCEditでこのブロックを削除するか、関連するModを直接削除できます。\nブロックタイプ:%1$s\nブロックの場所:%2$s
game.crash.reason.bootstrap_failed=mod %1$s がクラッシュしたため、ゲームを実行できません。\n削除または更新を試みることができます。
game.crash.reason.config=modが構成ファイルを解析できないため、ゲームを実行できません。\nMod %1$s は構成ファイル %2$s を解析できません。
game.crash.reason.debug_crash=クラッシュが手動でトリガーされるため、ゲームを実行できません。\n実際、ゲームは無実であり、すべてあなたの責任です。
game.crash.reason.duplicated_mod=Mod %1$s が重複してインストールされているため、現在のゲームを継続して実行できません。\n%2$s\n各Modは1つしかインストールできませんので、余分なModを削除して再度お試しください。
game.crash.reason.mixin_apply_mod_failed=Mixin が %1$s モジュールを適用できないため、現在のゲームを続行できません。\n問題を解決するために、このモジュールを削除または更新してみてください。
game.crash.reason.entity=エンティティが原因でゲームを実行できません。\nMCEditでこのエンティティを削除するか、関連するModを直接削除できます。\nエンティティタイプ:%1$s\nエンティティの場所:%2$s
game.crash.reason.fabric_version_0_12=Fabric 0.12以降は、現在インストールされているmodと互換性がありません。ファブリックを0.11.7にダウングレードする必要があります。
game.crash.reason.fabric_warnings=Fabricはいくつかの警告を出します:\n%1$s
game.crash.reason.modmixin_failure=mod インジェクションの失敗により、現在のゲームを続行できません。\nこれは通常、MOD にバグがあるか、現在の環境と互換性がないことを意味します。\n間違った mod のログを確認できます。
@@ -791,7 +788,7 @@ settings.advanced.natives_directory=ネイティブライブラリパス(例
settings.advanced.natives_directory.choose=ネイティブライブラリパスを選択してください
settings.advanced.natives_directory.custom=Custom(ゲームが実行時に必要とするネイティブライブラリを提供する必要があります)
settings.advanced.natives_directory.default=Standard(ネイティブライブラリは当社が提供します)
settings.advanced.natives_directory.hint=このオプションは、AppleM1またはその他の公式にサポートされていないプラットフォーム/アーキテクチャでゲームをプレイする場合にのみ使用されます。\nこのオプションを変更する意味がわからない場合は、このオプションを変更しないことをお勧めします。、またはゲームが起動しません。\nこのオプションを変更する必要がある場合は、ゲームを実行するために必要なすべてのネイティブライブラリをカスタムネイティブディレクトリの下に配置する必要があります。lwjgl.dll(liblwjgl.so)、openal.dll(libopenal.so)。HMCLは、不足しているネイティブライブラリを提供しません。
settings.advanced.natives_directory.hint=このオプションは、Appleシリコンまたはその他の公式にサポートされていないプラットフォーム/アーキテクチャでゲームをプレイする場合にのみ使用されます。\nこのオプションを変更する意味がわからない場合は、このオプションを変更しないことをお勧めします。、またはゲームが起動しません。\nこのオプションを変更する必要がある場合は、ゲームを実行するために必要なすべてのネイティブライブラリをカスタムネイティブディレクトリの下に配置する必要があります。lwjgl.dll(liblwjgl.so)、openal.dll(libopenal.so)。HMCLは、不足しているネイティブライブラリを提供しません。
settings.advanced.no_jvm_args=デフォルトのJVM引数なし
settings.advanced.precall_command=起動前コマンド
settings.advanced.precall_command.prompt=ゲーム開始前に実行されます
@@ -949,6 +946,7 @@ version.manage.manage=バージョンの管理
version.manage.manage.title=バージョンの管理 - %1s
version.manage.redownload_assets_index=ゲームアセットファイルの更新
version.manage.remove=このバージョンを削除します
version.manage.remove.confirm=このバージョン %s を削除してもよろしいですか?このバージョンを再度復元することはできません。
version.manage.remove.confirm.trash=このバージョン %s を削除してもよろしいですか?このバージョンは、システムのゴミ箱に %s という名前で復元できます。
version.manage.remove.confirm.independent=このバージョンは独立モードであるため、このバージョンを削除すると、このバージョンに属するすべての保存済みワールドも削除されます。このバージョン %s を削除しますか?
version.manage.remove_assets=すべてのゲームリソースファイルの削除
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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
@@ -20,7 +20,6 @@
about=О лаунчере
about.copyright=Авторские права
about.copyright.statement=Авторские права © 2025 huangyuhui.
about.author=Автор
about.author.statement=bilibili @huanghongxun
about.claim=EULA
@@ -398,6 +397,17 @@ fatal.apply_update_failure=Мы сожалеем, лаунчер не смог
Вы можете обновить программу вручную, скачав более новую версию с %s.
fatal.apply_update_need_win7=Лаунчер не может автоматически обновляться на Windows XP/Vista.\n\
Вы можете обновить программу вручную, скачав более новую версию с %s.
fatal.deprecated_java_version=В будущем для работы HMCL потребуется Java 17 или более поздняя версия, однако запуск игр будет поддерживаться и на Java 6~16.\n\
\n\
Рекомендуется установить последнюю версию Java, чтобы обеспечить правильную работу HMCL.\n\
\n\
Вы можете продолжать использовать старую версию Java. HMCL может распознавать и управлять несколькими версиями Java и автоматически выбирать подходящую Java в зависимости от версии игры.
fatal.deprecated_java_version.update=В будущем для работы HMCL потребуется Java 17 или более поздняя версия. Пожалуйста, установить последнюю версию Java, чтобы убедиться, что приложение сможет завершить обновление.\n\
\n\
Вы можете продолжать использовать старую версию Java.\
HMCL может распознавать и управлять несколькими версиями Java \
и автоматически выбирать подходящую Java в зависимости от версии игры.
fatal.deprecated_java_version.download_link=Скачать Java %d
fatal.samba=Если вы пытаетесь открыть лаунчер в общей папке Samba, он может не работать, попробуйте обновить Java или запустить лаунчер из локальной папки.
fatal.illegal_char=Недопустимый символ «=» в пути к папке пользователя. Лаунчер может работать, но некоторые функции не будут работать.\n\
Вы не сможете использовать аккаунт authlib-injector или изменить скин для аккаунта в режиме офлайн.
@@ -441,12 +451,6 @@ game.crash.info=Сведения об отказе
game.crash.reason=Анализатор сбоев
game.crash.reason.analyzing=Анализирование...
game.crash.reason.multiple=Обнаружено несколько причин:\n\n
game.crash.reason.block=Игра отказала из-за блока в мире.\n\
\n\
Удалите этот блок с помощью MCEdit или удалите мод связанный с ним напрямую.\n\
\n\
Тип блока: %1$s\n\
Расположение блока: %2$s
game.crash.reason.bootstrap_failed=Игра отказала из-за мода «%1$s».\n\
\n\
Попробуйте удалить или обновить его.
@@ -457,12 +461,6 @@ game.crash.reason.duplicated_mod=Не удалось продолжить зап
%2$s\n\
\n\
Каждый мод должен быть установлен только один, удалите дублирующий мод и попробуйте снова.
game.crash.reason.entity=Игра отказала из-за сущность в мире.\n\
\n\
Удалите эту сущность с помощью MCEdit или удалите мод связанный с ним напрямую.\n\
\n\
Тип сущность: %1$s\n\
Расположение сущность: %2$s
game.crash.reason.fabric_version_0_12=Fabric 0.12 (и выше) несовместим с установленными модами. Следует понизить версию fabric до 0.11.7.
game.crash.reason.mixin_apply_mod_failed=Игра отказала из-за невозможности применить mixin к моду «%1$s».\n\
\n\
@@ -864,7 +862,7 @@ modpack.choose.remote=Скачать
modpack.choose.remote.detail=Необходима прямая ссылка на скачивание модпака
modpack.choose.remote.tooltip=Введите URL-адрес модпака
modpack.choose.repository=Скачать модпак с CurseForge или Modrinth
modpack.choose.repository.detail=Не забудьте вернуться на эту страницу и перетащить файл модпака сюда после того, как модпак будет скачан.
modpack.choose.repository.detail=Вы сможете выбрать нужный вам модпак на следующей странице.
modpack.completion=Скачивание зависимостей
modpack.desc=Опишите модпак, включая введение и журнал изменений. Поддерживаются Markdown и изображения по URL-адресу.
modpack.description=Описание
@@ -1439,6 +1437,7 @@ version.manage.manage=Изменить сборку
version.manage.manage.title=Изменить сборку - %1s
version.manage.redownload_assets_index=Обновить файлы игровых активов
version.manage.remove=Удалить
version.manage.remove.confirm=Удалить %s? Вы не сможете восстановить эту сборку снова!
version.manage.remove.confirm.trash=Удалить %s? Вы можете восстановить эту сборку с именем %s из корзины системы.
version.manage.remove.confirm.independent=Поскольку эта сборка находится в режиме разделения, удаление этой сборки приведет к удалению всех миров, принадлежащих этой сборке. Удалить %s?
version.manage.remove_assets=Удалить файлы игровых активов
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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
@@ -20,7 +20,7 @@
about=關於
about.copyright=著作權
about.copyright.statement=著作權所有 © 2025 huangyuhui
about.copyright.statement=著作權所有 © 2013-2026 huangyuhui 與貢獻者
about.author=作者
about.author.statement=bilibili @huanghongxun
about.claim=使用者協議
@@ -97,7 +97,7 @@ account.register=註冊
account.manage=帳戶清單
account.copy_uuid=複製該帳戶的 UUID
account.methods=登入方式
account.methods.authlib_injector=外置登入
account.methods.authlib_injector=外部登入
account.methods.microsoft=Microsoft 帳戶
account.methods.microsoft.birth=如何變更帳戶出生日期
account.methods.microsoft.deauthorize=移除應用存取權
@@ -186,6 +186,7 @@ button.export=匯出
button.no=否
button.ok=確定
button.ok.countdown=確定 (%d)
button.reveal_dir=開啟目錄
button.refresh=重新整理
button.remove=刪除
button.remove.confirm=你確認要刪除嗎?該操作無法復原!
@@ -376,10 +377,14 @@ fatal.config_change_owner_root=你正在使用 root 帳戶開啟 Hello Minecraft
fatal.config_in_temp_dir=你正在暫存目錄中開啟 Hello Minecraft! Launcher,你的設定和遊戲資料可能會遺失。建議將 HMCL 移動至其他位置再開啟。\n是否繼續開啟?
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 HMCL 對「%s」目錄及該目錄下的檔案擁有讀寫權限。
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 無法載入設定檔案,因為設定檔案是由使用者「%1$s」建立的。\n請使用 root 帳戶開啟 HMCL (不推薦),或在終端中執行以下指令將設定檔案的所有權變更為目前使用者:\n%2$s
fatal.config_unsupported_version=當前設定檔案是由更高版本的 Hello Minecraft! Launcher 建立的,目前版本的 HMCL 無法正常載入。請更新並重新啟動 HMCL。\n在更新啟動器之前,你所做的所有設定更改都不會被保存。
fatal.mac_app_translocation=由於 macOS 的安全機制,Hello Minecraft! Launcher 被系統隔離至暫存目錄中。\n請將 HMCL 移動到其他目錄後再嘗試開啟,否則你的設定和遊戲資料可能會在重啟後遺失。\n是否繼續開啟?
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 HMCL。
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。
fatal.apply_update_need_win7=Hello Minecraft! Launcher 無法在 Windows XP/Vista 上進行自動更新。請從 %s 處手動下載 HMCL 來完成升級。
fatal.deprecated_java_version=HMCL 未來需要 Java 17 或更高版本才能執行,但依然支援使用 Java 6~16 啟動遊戲。建議安裝最新版本的 Java 以確保 HMCL 正常執行。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
fatal.deprecated_java_version.update=更高版本的 HMCL 需要 Java 17 或更高版本才能執行,請安裝最新版本的 Java 以確保 HMCL 能夠完成升級。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
fatal.deprecated_java_version.download_link=下載 Java %d
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
fatal.illegal_char=由於您的使用者目錄路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
fatal.unsupported_platform=Minecraft 尚未你您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「軟繪製器」,以獲得更好的相容性。
@@ -415,13 +420,13 @@ game.crash.info=遊戲訊息
game.crash.reason=崩潰原因
game.crash.reason.analyzing=分析中……
game.crash.reason.multiple=檢測到多個原因:\n\n
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用 MCEdit 工具編輯存檔刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯存檔刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
game.crash.reason.bootstrap_failed=目前遊戲由於模組「%1$s」出現問題,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
game.crash.reason.mixin_apply_mod_failed=目前遊戲由於 Mixin 無法應用於「%1$s」模組,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
game.crash.reason.config=目前遊戲由於無法解析模組配置檔案,無法繼續執行\n模組「%1$s」的配置檔案「%2$s」無法被解析。
game.crash.reason.debug_crash=目前遊戲由於手動觸發崩潰,無法繼續執行。\n事實上遊戲並沒有問題,問題都是你造成的!
game.crash.reason.duplicated_mod=目前遊戲由於模組重複安裝,無法繼續執行。\n%s\n每種模組只能安裝一個,請你刪除多餘的模組再試。
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用 MCEdit 工具編輯存檔刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯存檔刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本與目前已經安裝的模組可能不相容,你需要將 Fabric Loader 降級至 0.11.7。
game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
game.crash.reason.modmixin_failure=目前遊戲由於某些模組注入失敗,無法繼續執行。\n這一般代表著該模組存在問題,或與目前環境不相容。\n你可以查看日誌尋找出錯模組。
@@ -434,7 +439,7 @@ game.crash.reason.mod_name=目前遊戲由於模組檔案名稱問題,無法
game.crash.reason.incomplete_forge_installation=目前遊戲由於 Forge 安裝不完整,無法繼續執行。\n請在「實例管理 - 自動安裝」中移除 Forge 並重新安裝。
game.crash.reason.file_already_exists=目前遊戲由於檔案「%1$s」已經存在,無法繼續執行。\n如果你認為這個檔案可以刪除,你可以在備份這個檔案後嘗試刪除它,並重新啟動遊戲。
game.crash.reason.file_changed=目前遊戲由於檔案校驗失敗,無法繼續執行。\n如果你手動修改了 Minecraft.jar 檔案,你需要回退修改,或者重新下載遊戲。
game.crash.reason.gl_operation_failure=目前遊戲由於你使用的某些模組/光影包/資源包/紋理包出現問題,無法繼續執行。\n請先嘗試禁用你所使用的模組/光影包/資源包/紋理包再試。
game.crash.reason.gl_operation_failure=目前遊戲由於你使用的某些模組/光影包/資源包/紋理包出現問題,無法繼續執行。\n請先嘗試停用你所使用的模組/光影包/資源包/紋理包再試。
game.crash.reason.graphics_driver=目前遊戲由於你的顯示卡驅動存在問題崩潰了。請嘗試升級你的顯示卡驅動到最新版本後再嘗試啟動遊戲。\n\
如果你的電腦同時存在獨立顯示卡與整合式顯示卡,你需要檢查遊戲是否使用整合式顯示卡啟動。如果是,請嘗試使用獨立顯示卡開啟 HMCL 與遊戲。如果仍有問題,你可能需要考慮換一個新顯示卡或新電腦。\n\
如果你確實需要使用整合式顯示卡,請檢查你電腦的 CPU 是否為 Intel(R) Core(TM) 3000 系列或更舊的處理器。如果是,對於 Minecraft 1.16.5 及更低版本,請你將遊戲所使用的 Java 版本降級至 1.8.0_51 及更低版本;否則你需要更換獨立顯示卡或新電腦。
@@ -541,9 +546,9 @@ install.success=安裝成功
java.add=添加 Java
java.add.failed=Java 無效或與目前平臺不相容
java.disable=禁用此 Java
java.disable.confirm=你確定要禁用此 Java 嗎?
java.disabled.management=管理已禁用的 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
@@ -667,6 +672,7 @@ message.info=提示
message.success=完成
message.unknown=未知
message.warning=警告
message.question=確認
modpack=模組包
modpack.choose=選取要安裝的遊戲模組包檔案
@@ -675,7 +681,7 @@ modpack.choose.local.detail=你可以直接將模組包檔案拖入本頁面以
modpack.choose.remote=從網路下載模組包
modpack.choose.remote.detail=需要提供模組包的下載連結
modpack.choose.repository=從 CurseForge/Modrinth 下載模組包
modpack.choose.repository.detail=下載後記得回到這個介面,把模組包拖進來哦
modpack.choose.repository.detail=可直接在跳轉到的頁面安裝模組包
modpack.choose.remote.tooltip=要下載的模組包的連結
modpack.completion=下載模組包相關檔案
modpack.desc=描述你要製作的模組包,比如模組包注意事項和更新紀錄。支援 Markdown (圖片請上傳至網路)。
@@ -800,7 +806,7 @@ modrinth.category.optifine=OptiFine
modrinth.category.optimization=最佳化
modrinth.category.ornithe=Ornithe
modrinth.category.paper=Paper
modrinth.category.path-tracing=路径追踪
modrinth.category.path-tracing=路徑追蹤
modrinth.category.pbr=PBR
modrinth.category.potato=極低
modrinth.category.purpur=Purpur
@@ -887,7 +893,7 @@ datapack.choose_datapack=選取要匯入的資料包壓縮檔
datapack.extension=資料包
datapack.title=世界 [%s] - 資料包
web.failed=加載頁面失敗
web.failed=載入頁面失敗
web.open_in_browser=是否要在瀏覽器中開啟此連結:\n%s
web.view_in_browser=在瀏覽器中查看完整日誌
@@ -895,10 +901,10 @@ world=世界
world.add=新增世界
world.backup=世界備份
world.backup.create.new_one=建立新備份
world.backup.create.failed=創建備份失敗。\n%s
world.backup.create.failed=建立備份失敗。\n%s
world.backup.create.locked=該世界正在使用中,請關閉遊戲後重試。
world.backup.create.success=成功創建新備份:%s
world.backup.delete=删除此備份
world.backup.create.success=成功建立新備份:%s
world.backup.delete=刪除此備份
world.backup.processing=正在備份中……
world.chunkbase=世界地圖
world.chunkbase.end_city=終界城地圖
@@ -937,6 +943,7 @@ world.info.player.food_level=饑餓值
world.info.player.game_type=遊戲模式
world.info.player.game_type.adventure=冒險
world.info.player.game_type.creative=創造
world.info.player.game_type.hardcore=極限
world.info.player.game_type.spectator=旁觀者
world.info.player.game_type.survival=生存
world.info.player.health=生命值
@@ -989,7 +996,7 @@ schematics.create_directory.prompt=請輸入新目錄名稱
schematics.create_directory.failed=建立目錄失敗
schematics.create_directory.failed.already_exists=目錄已存在
schematics.create_directory.failed.empty_name=名稱不能為空
schematics.create_directory.failed.invalid_name=名稱中包含非法字元
schematics.create_directory.failed.invalid_name=名稱中包含無效字元
schematics.info.description=描述
schematics.info.enclosing_size=框大小
schematics.info.name=名稱
@@ -1037,7 +1044,7 @@ settings.advanced.custom_commands.hint=自訂指令被呼叫時將包含如下
\ · $INST_FABRIC: 若安裝了 Fabric,將會存在本環境變數;\n\
\ · $INST_QUILT: 若安裝了 Quilt,將會存在本環境變數。
settings.advanced.dont_check_game_completeness=不檢查遊戲完整性
settings.advanced.dont_check_jvm_validity=不檢查 JVM 與遊戲的相容性
settings.advanced.dont_check_jvm_validity=不檢查 Java 虛擬機與遊戲的相容性
settings.advanced.dont_patch_natives=不嘗試自動取代本機庫
settings.advanced.environment_variables=環境變數
settings.advanced.game_dir.default=預設 (".minecraft/")
@@ -1047,7 +1054,7 @@ settings.advanced.java_permanent_generation_space.prompt=單位 MiB
settings.advanced.jvm=Java 虛擬機設定
settings.advanced.jvm_args=Java 虛擬機參數
settings.advanced.jvm_args.prompt=\ · 若在「Java 虛擬機參數」中輸入的參數與預設參數相同,則不會新增;\n\
\ · 若在「Java 虛擬機參數」輸入任何 GC 參數,預設參數的 G1 參數會被禁用;\n\
\ · 若在「Java 虛擬機參數」輸入任何 GC 參數,預設參數的 G1 參數會被停用;\n\
\ · 啟用下方「不新增預設的 Java 虛擬機參數」可在啟動遊戲時不新增預設參數。
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
@@ -1060,8 +1067,9 @@ settings.advanced.natives_directory=本機庫路徑 (LWJGL)
settings.advanced.natives_directory.choose=選取本機庫路徑
settings.advanced.natives_directory.custom=自訂 (由你提供遊戲需要的本機庫)
settings.advanced.natives_directory.default=預設 (由啟動器提供遊戲本機庫)
settings.advanced.natives_directory.hint=本選項提供給 Apple M1 等未受遊戲官方支援的平台來自訂遊戲本機庫。如果你不知道本選項的含義,請你不要修改本選項,否則會導致遊戲無法啟動。\n\n如果你要修改本選項,你需要保證自訂目錄下有遊戲所需的本機庫檔案,如 lwjgl.dll (liblwjgl.so), openal.dll (libopenal.so) 等檔案。啟動器不會幫你補全缺少的本機庫檔案。\n\n注意:建議指定的本機庫檔案路徑使用全英文字元,否則可能導致遊戲啟動失敗。
settings.advanced.no_jvm_args=不新增預設的 JVM 參數
settings.advanced.natives_directory.default.version_id=<版本 ID>
settings.advanced.natives_directory.hint=本選項提供給 Apple Silicon 等未受遊戲官方支援的平台來自訂遊戲本機庫。如果你不知道本選項的含義,請你不要修改本選項,否則會導致遊戲無法啟動。\n\n如果你要修改本選項,你需要保證自訂目錄下有遊戲所需的本機庫檔案,如 lwjgl.dll (liblwjgl.so), openal.dll (libopenal.so) 等檔案。啟動器不會幫你補全缺少的本機庫檔案。\n\n注意:建議指定的本機庫檔案路徑使用全英文字元,否則可能導致遊戲啟動失敗。
settings.advanced.no_jvm_args=不新增預設的 Java 虛擬機參數
settings.advanced.precall_command=遊戲啟動前執行指令
settings.advanced.precall_command.prompt=將在遊戲啟動前呼叫使用
settings.advanced.process_priority=處理程式優先度
@@ -1108,7 +1116,7 @@ settings.game.java_directory.template=%s (%s)
settings.game.management=管理
settings.game.working_directory=執行路徑 (建議使用模組時選取「各實例獨立」。修改後請自行移動相關遊戲檔案,如存檔、模組設定等)
settings.game.working_directory.choose=選取執行目錄
settings.game.working_directory.hint=在「執行路徑」選項中選取「各實例獨立」使目前實例獨立存放設定、存檔、模組等資料。使用模組時建議開啟此選項以避免不同版本模組衝突。修改此選項後需自行移動存檔等檔案。
settings.game.working_directory.hint=在「執行路徑」選項中選取「各實例獨立」使目前實例獨立存放設定、存檔、模組等資料。使用模組時建議開啟此選項以避免不同實例模組衝突。修改此選項後需自行移動存檔等檔案。
settings.icon=遊戲圖示
@@ -1124,10 +1132,6 @@ settings.launcher.download_source=下載來源
settings.launcher.download_source.auto=自動選取下載來源
settings.launcher.enable_game_list=在首頁內顯示遊戲清單
settings.launcher.font=字體
settings.launcher.font.anti_aliasing=抗鋸齒 (重啟後生效)
settings.launcher.font.anti_aliasing.auto=自動
settings.launcher.font.anti_aliasing.gray=灰度
settings.launcher.font.anti_aliasing.lcd=子像素
settings.launcher.general=一般
settings.launcher.language=語言 (重啟後生效)
settings.launcher.launcher_log.export=匯出啟動器日誌
@@ -1205,6 +1209,8 @@ update.note=開發版與預覽版包含更多的功能以及錯誤修復,但
update.latest=目前版本為最新版本
update.no_browser=無法開啟瀏覽器。網址已經複製到剪貼簿了,你可以手動複製網址開啟頁面。
update.tooltip=更新
update.preview=提前測試 HMCL 預覽版本
update.preview.tooltip=啟用此選項,你將可以提前取得 HMCL 的新版本,以便在正式發布前進行測試。
version=遊戲
version.name=遊戲實例名稱
@@ -1236,6 +1242,7 @@ version.manage.manage=實例管理
version.manage.manage.title=實例管理 - %1s
version.manage.redownload_assets_index=更新遊戲資源檔案
version.manage.remove=刪除該實例
version.manage.remove.confirm=真的要刪除實例「%s」嗎? 你將無法找回被刪除的檔案!!!
version.manage.remove.confirm.trash=真的要刪除實例「%s」嗎? 你可以在系統的資源回收筒 (或垃圾桶) 中還原目錄「%s」來找回該實例。
version.manage.remove.confirm.independent=由於該實例啟用了「(全域/實例特定) 遊戲設定 → 執行路徑 → 各實例獨立」設定,刪除該實例將導致該遊戲的存檔等資料一同被刪除!真的要刪除實例「%s」嗎?
version.manage.remove_assets=刪除所有遊戲資源檔案
@@ -1,6 +1,6 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
# Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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
@@ -20,7 +20,7 @@
about=关于
about.copyright=版权
about.copyright.statement=版权所有 © 2025 huangyuhui
about.copyright.statement=版权所有 © 2013-2026 huangyuhui 及贡献者
about.author=作者
about.author.statement=bilibili @huanghongxun
about.claim=用户协议
@@ -181,7 +181,7 @@ archive.version=版本
assets.download=下载资源
assets.download_all=检查资源文件完整性
assets.index.malformed=资源文件的索引文件损坏。你可以在相应版本的“版本管理”页面中,点击左下角“管理 → 更新游戏资源文件”以修复该问题。\n你可以点击右上角帮助按钮进行求助。
assets.index.malformed=资源文件的索引文件损坏。你可以在相应实例的“实例管理”页面中,点击左下角“管理 → 更新游戏资源文件”以修复该问题。\n你可以点击右上角帮助按钮进行求助。
button.cancel=取消
button.change_source=切换下载源
@@ -194,6 +194,7 @@ button.export=导出
button.no=否
button.ok=确定
button.ok.countdown=确定 (%d)
button.reveal_dir=打开文件夹
button.refresh=刷新
button.remove=删除
button.remove.confirm=你确定要删除吗?此操作无法撤销!
@@ -385,13 +386,17 @@ fatal.config_change_owner_root=你正在使用 root 账户启动 Hello Minecraft
fatal.config_in_temp_dir=你正在临时文件夹中启动 Hello Minecraft! Launcher,你的设置和游戏数据可能会丢失。建议将 HMCL 移动至其他位置再启动。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件。\n请确保 HMCL 对“%s”文件夹及该文件夹下的文件拥有读写权限。\n对于 macOS,尝试将 HMCL 放在除“桌面”“下载”“文稿”之外的有权限的地方再试。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 无法加载配置文件,因为配置文件是由用户“%1$s”创建的。\n请使用 root 账户启动 HMCL (不推荐),或在终端中执行以下命令将配置文件的所有权变更为当前用户:\n%2$s\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.config_unsupported_version=当前配置文件是由更高版本的 Hello Minecraft! Launcher 创建的,当前版本的 HMCL 无法正常加载。请更新并重启 HMCL。\n在更新启动器之前,你修改的所有设置都不会被保存。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.mac_app_translocation=由于 macOS 的安全机制,Hello Minecraft! Launcher 被系统隔离至临时文件夹中。\n请将 HMCL 移动到其他文件夹后再尝试启动,否则你的设置和游戏数据可能会在重启后丢失。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 HMCL。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题。\n但你依可以从 %s 手动下载 HMCL 来完成升级。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.apply_update_need_win7=Hello Minecraft! Launcher 无法在 Windows XP/Vista 上进行自动更新。请从 %s 手动下载 HMCL 来完成升级。
fatal.deprecated_java_version=HMCL 未来需要 Java 17 或更高版本才能运行,但依然支持使用 Java 6~16 启动游戏。建议安装最新版本的 Java 以确保 HMCL 能正常工作。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.deprecated_java_version.update=更高版本的 HMCL 需要 Java 17 或更高版本才能运行,请安装最新版本的 Java 以确保 HMCL 能够完成升级。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。
fatal.deprecated_java_version.download_link=下载 Java %d
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.illegal_char=由于你的用户文件夹路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/版本特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“软渲染器”,以获得更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“软渲染器”,以获得更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已为 Apple Silicon 平台提供支持。使用 ARM 原生 Java 启动游戏以获得更流畅的游戏体验。\n如果你在游戏中遇到问题,使用 x86-64 架构的 Java 启动游戏可能有更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
@@ -408,7 +413,7 @@ feedback.qq_group.statement=欢迎加入 HMCL 用户群,加入后请遵守群
file=文件
folder.config=配置文件夹
folder.game=游戏文件夹
folder.game=实例运行文件夹
folder.logs=日志文件夹
folder.mod=模组文件夹
folder.resourcepacks=资源包文件夹
@@ -423,14 +428,14 @@ game.crash.feedback=<b>请不要将本界面截图给他人!</b>如果你要
game.crash.info=游戏信息
game.crash.reason=崩溃原因
game.crash.reason.analyzing=分析中……
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用 <a href="https://www.mcedit.net">MCEdit</a> 工具编辑存档删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑存档删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
game.crash.reason.bootstrap_failed=当前游戏由于模组“%1$s”出现问题,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.mixin_apply_mod_failed=当前游戏由于 Mixin 无法应用于“%1$s”模组,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.config=当前游戏由于无法解析模组配置文件,无法继续运行。\n无法解析模组“%1$s”的配置文件“%2$s”。
game.crash.reason.multiple=检测到多个原因:\n\n
game.crash.reason.debug_crash=当前游戏由于手动触发崩溃,无法继续运行。\n事实上游戏并没有问题,问题都是你造成的!
game.crash.reason.duplicated_mod=当前游戏由于模组“%1$s”重复安装,无法继续运行。\n%2$s\n每种模组只能安装一个,请你删除多余的模组再试。
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用 <a href="https://www.mcedit.net">MCEdit</a> 工具编辑存档删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑存档删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本与当前已经安装的模组可能不兼容,你需要将 Fabric Loader 降级至 0.11.7。
game.crash.reason.fabric_warnings=Fabric 提供了一些警告信息:\n%1$s
game.crash.reason.file_already_exists=当前游戏由于文件“%1$s”已经存在,无法继续运行。\n如果你认为这个文件可以删除,你可以在备份这个文件后尝试删除它,并重新启动游戏。
@@ -444,12 +449,12 @@ game.crash.reason.graphics_driver=当前游戏由于显卡驱动问题而崩溃
game.crash.reason.macos_failed_to_find_service_port_for_display=当前游戏由于 Apple Silicon 平台下初始化 OpenGL 窗口失败,无法继续运行。\n对于该问题,HMCL 暂无直接性的解决方案。请你尝试任意打开一个浏览器并全屏,然后再回到 HMCL 启动游戏。在弹出游戏窗口前<b>迅速切回浏览器页面</b>,等待游戏窗口出现后再切回游戏窗口。
game.crash.reason.illegal_access_error=当前游戏由于某些模组的问题,无法继续运行。\n如果你认识“%1$s”,你可以更新或删除对应模组再试。
game.crash.reason.install_mixinbootstrap=当前游戏由于缺失 MixinBootstrap,无法继续运行。\n你可以尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/mixinbootstrap">MixinBootstrap</a> 解决该问题。若安装后崩溃,可以尝试在该模组的文件名开头添加半角感叹号 (!) 解决。
game.crash.reason.need_jdk11=当前游戏由于 Java 版本不合适,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 11</a>,并在“(全局/版本特定) 游戏设置 → 游戏 Java”中将 Java 设置为 11.x 的版本。
game.crash.reason.jdk_9=当前游戏由于 Java 版本过高,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 8</a>,并在“(全局/版本特定) 游戏设置 → 游戏 Java”中将 Java 设置为 1.8.x 的版本。
game.crash.reason.jvm_32bit=当前游戏由于内存分配过大,超过了 32 位 Java 虚拟机内存限制,无法继续运行。\n如果你的电脑是 64 位系统,请下载安装并更换 64 位 Java。<a href="https://bell-sw.com/pages/downloads/#downloads">下载 Java</a>\n如果你的电脑是 32 位系统,你或许可以重新安装 64 位系统,或换一台新电脑。\n或者,你可以在“(全局/版本特定) 游戏设置 → 游戏内存”中关闭“自动分配内存”,并把内存分配值调整为 1024 MiB 或以下。
game.crash.reason.need_jdk11=当前游戏由于 Java 版本不合适,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 11</a>,并在“(全局/实例特定) 游戏设置 → 游戏 Java”中将 Java 设置为 11.x 的版本。
game.crash.reason.jdk_9=当前游戏由于 Java 版本过高,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 8</a>,并在“(全局/实例特定) 游戏设置 → 游戏 Java”中将 Java 设置为 1.8.x 的版本。
game.crash.reason.jvm_32bit=当前游戏由于内存分配过大,超过了 32 位 Java 虚拟机内存限制,无法继续运行。\n如果你的电脑是 64 位系统,请下载安装并更换 64 位 Java。<a href="https://bell-sw.com/pages/downloads/#downloads">下载 Java</a>\n如果你的电脑是 32 位系统,你或许可以重新安装 64 位系统,或换一台新电脑。\n或者,你可以在“(全局/实例特定) 游戏设置 → 游戏内存”中关闭“自动分配内存”,并把内存分配值调整为 1024 MiB 或以下。
game.crash.reason.loading_crashed_forge=当前游戏由于模组“%1$s (%2$s)”错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.loading_crashed_fabric=当前游戏由于模组“%1$s”错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
game.crash.reason.memory_exceeded=当前游戏由于内存分配过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在“(全局/版本特定) 游戏设置”中关闭“自动分配内存”,并将内存分配值调低至游戏能正常启动为止。\n你还可以尝试将虚拟内存设置调整为“自动管理所有驱动器分页文件大小”,<a href="https://docs.hmcl.net/assets/img/hmcl/自动管理所有驱动器分页文件大小.webp">详情</a>。
game.crash.reason.memory_exceeded=当前游戏由于内存分配过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在“(全局/实例特定) 游戏设置”中关闭“自动分配内存”,并将内存分配值调低至游戏能正常启动为止。\n你还可以尝试将虚拟内存设置调整为“自动管理所有驱动器分页文件大小”,<a href="https://docs.hmcl.net/assets/img/hmcl/自动管理所有驱动器分页文件大小.webp">详情</a>。
game.crash.reason.mac_jdk_8u261=当前游戏由于你所使用的 Forge/OptiFine 与 Java 冲突而崩溃。\n请尝试更新 Forge/OptiFine,或使用 Java 8u251 及更早版本启动。
game.crash.reason.mod=当前游戏由于“%1$s”的问题,无法继续运行。\n你可以更新或删除已经安装的“%1$s”再试。
game.crash.reason.mod_resolution=当前游戏由于模组依赖问题,无法继续运行。Fabric 提供了如下信息:\n%1$s
@@ -459,37 +464,37 @@ game.crash.reason.mod_resolution_missing=当前游戏由于缺少前置模组,
game.crash.reason.mod_resolution_missing_minecraft=当前游戏由于模组和 Minecraft 游戏版本不匹配,无法继续运行。\n“%1$s”需要 Minecraft %2$s 才能运行。\n如果你要继续使用你已经安装的模组,你可以选择安装对应的 Minecraft 版本。如果你要继续使用当前 Minecraft 版本,你需要安装对应版本的模组。
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 重复安装,无法继续运行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此为已知问题</a>\n建议将日志上传并反馈至 GitHub,以便我们找到更多线索并修复此问题。\n目前你可以在“版本管理 → 自动安装”中卸载 Forge 并重新安装。
game.crash.reason.optifine_repeat_installation=当前游戏由于 OptiFine 重复安装,无法继续运行。\n请删除模组文件夹下的 OptiFine 或前往“版本管理 → 自动安装”卸载安装的 OptiFine。
game.crash.reason.forge_repeat_installation=当前游戏由于 Forge 重复安装,无法继续运行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此为已知问题</a>\n建议将日志上传并反馈至 GitHub,以便我们找到更多线索并修复此问题。\n目前你可以在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
game.crash.reason.optifine_repeat_installation=当前游戏由于 OptiFine 重复安装,无法继续运行。\n请删除模组文件夹下的 OptiFine 或前往“实例管理 → 自动安装”卸载安装的 OptiFine。
game.crash.reason.forgemod_resolution=当前游戏由于模组依赖问题,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
game.crash.reason.forge_found_duplicate_mods=当前游戏由于模组重复安装,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
game.crash.reason.modmixin_failure=当前游戏由于某些模组注入失败,无法继续运行。\n这一般代表着该模组存在问题,或与当前环境不兼容。\n你可以查看日志寻找出错模组。
game.crash.reason.night_config_fixes=当前游戏由于 Night Config 库的一些问题,无法继续运行。\n你可以尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模组,这或许能帮助你解决这个问题。\n更多详情,可访问该模组的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 仓库</a>。
game.crash.reason.forge_error=Forge/NeoForge 可能已经提供了错误信息。\n你可以查看日志,并根据错误报告中的日志信息进行对应处理。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\n%1$s
game.crash.reason.mod_resolution0=当前游戏由于一些模组出现问题,无法继续运行。\n你可以查看日志寻找出错模组。
game.crash.reason.java_version_is_too_high=当前游戏由于 Java 版本过高,无法继续运行。\n请在“(全局/版本特定) 游戏设置 → 游戏 Java”中改用较低版本的 Java,然后再启动游戏。\n如果没有,可以从 <a href="https://www.java.com/download/">java.com (Java 8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java 17)</a> 等平台下载、安装一个 (安装完后需重启启动器)。
game.crash.reason.java_version_is_too_high=当前游戏由于 Java 版本过高,无法继续运行。\n请在“(全局/实例特定) 游戏设置 → 游戏 Java”中改用较低版本的 Java,然后再启动游戏。\n如果没有,可以从 <a href="https://www.java.com/download/">java.com (Java 8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java 17)</a> 等平台下载、安装一个 (安装完后需重启启动器)。
game.crash.reason.mod_name=当前游戏由于模组文件名称问题,无法继续运行。\n模组文件名称应只使用半角的大小写字母 (Aa~Zz)、数字 (0~9)、横线 (-)、下划线 (_)和点 (.)。\n请到模组文件夹中将所有不合规的模组文件名修改为上述合规字符。
game.crash.reason.incomplete_forge_installation=当前游戏由于 Forge/NeoForge 安装不完整,无法继续运行。\n请在“版本管理 → 自动安装”中卸载 Forge 并重新安装。
game.crash.reason.optifine_is_not_compatible_with_forge=当前游戏由于 OptiFine 与当前版本的 Forge 不兼容,导致游戏崩溃。\n点击 <a href="https://zkitefly.github.io/optifine-forge-support-list">此处</a> 查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏或在“版本管理 → 自动安装”中更换版本。\n经测试,Forge 版本过高或过低都可能导致崩溃。
game.crash.reason.incomplete_forge_installation=当前游戏由于 Forge/NeoForge 安装不完整,无法继续运行。\n请在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
game.crash.reason.optifine_is_not_compatible_with_forge=当前游戏由于 OptiFine 与当前版本的 Forge 不兼容,导致游戏崩溃。\n点击 <a href="https://zkitefly.github.io/optifine-forge-support-list">此处</a> 查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏或在“实例管理 → 自动安装”中更换版本。\n经测试,Forge 版本过高或过低都可能导致崩溃。
game.crash.reason.mod_files_are_decompressed=当前游戏由于模组文件被解压了,无法继续运行。\n请直接把整个模组文件放进模组文件夹中即可。\n解压模组文件会导致游戏出错。请删除模组文件夹中已被解压的模组,然后再启动游戏。
game.crash.reason.shaders_mod=当前游戏由于同时安装了 OptiFine 和 Shaders 模组,无法继续运行。\n由于 OptiFine 已集成 Shaders 模组的功能,所以只需删除 Shaders 模组即可。
game.crash.reason.rtss_forest_sodium=当前游戏由于 RivaTuner Statistics Server (RTSS) 与 Sodium 不兼容,导致游戏崩溃。\n点击 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此处</a> 查看详情。
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=当前游戏由于你所安装的模组过多,超出了游戏的 ID 限制,无法继续运行。\n请尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a> 等修复模组,或删除部分大型模组。
game.crash.reason.optifine_causes_the_world_to_fail_to_load=当前游戏可能由于 OptiFine 而无法继续运行。\n该问题只在特定 OptiFine 版本中出现,你可以尝试在“版本管理 → 自动安装”中更换 OptiFine 的版本。
game.crash.reason.optifine_causes_the_world_to_fail_to_load=当前游戏可能由于 OptiFine 而无法继续运行。\n该问题只在特定 OptiFine 版本中出现,你可以尝试在“实例管理 → 自动安装”中更换 OptiFine 的版本。
game.crash.reason.modlauncher_8=当前游戏由于你所使用的 Forge 版本与当前使用的 Java 冲突而崩溃。请尝试更新 Forge 到 36.2.26 或更高版本或换用版本低于 1.8.0.320 的 Java。<a href="https://bell-sw.com/pages/downloads/?version=java-8&package=jre-full">Liberica JDK 8u312+7</a>
game.crash.reason.no_class_def_found_error=当前游戏由于代码不完整,无法继续运行。\n你的游戏可能缺失了某个模组,或者某些模组文件不完整,或者模组与游戏的版本不匹配。\n你可能需要重新安装游戏和模组,或寻求他人帮助。\n缺失:\n%1$s
game.crash.reason.no_such_method_error=当前游戏由于代码不完整,无法继续运行。\n你的游戏可能缺失了某个模组,或者某些模组文件不完整,或者模组与游戏的版本不匹配。\n你可能需要重新安装游戏和模组,或寻求他人帮助。
game.crash.reason.opengl_not_supported=当前游戏由于你的显卡驱动存在问题,无法继续运行。\n原因是 OpenGL 不受支持,你现在是否在远程桌面或者串流模式下?如果是,请直接使用原电脑启动游戏。\n或者尝试升级你的显卡驱动到最新版本后再尝试启动游戏。如果你的电脑同时存在独立显卡和集成显卡,你需要检查游戏是否使用集成显卡启动。如果是,请尝试使用独立显卡启动 HMCL 与游戏。如果仍有问题,你可能需要考虑换一个新显卡或新电脑。
game.crash.reason.openj9=当前游戏无法在 OpenJ9 虚拟机上运行。请你在“(全局/版本特定) 游戏设置 → 游戏 Java”中更换为使用 Hotspot 虚拟机的 Java,并重新启动游戏。如果没有下载安装,你可以在网上自行下载。
game.crash.reason.out_of_memory=当前游戏由于内存不足,无法继续运行。\n这可能是内存分配过小,或者模组数量过多导致的。\n你可以在“(全局/版本特定) 游戏设置 → 游戏内存”中调整内存分配值以允许游戏在更大内存下运行。\n如果仍然出现该错误,你可能需要换一台更好的电脑。
game.crash.reason.openj9=当前游戏无法在 OpenJ9 虚拟机上运行。请你在“(全局/实例特定) 游戏设置 → 游戏 Java”中更换为使用 Hotspot 虚拟机的 Java,并重新启动游戏。如果没有下载安装,你可以在网上自行下载。
game.crash.reason.out_of_memory=当前游戏由于内存不足,无法继续运行。\n这可能是内存分配过小,或者模组数量过多导致的。\n你可以在“(全局/实例特定) 游戏设置 → 游戏内存”中调整内存分配值以允许游戏在更大内存下运行。\n如果仍然出现该错误,你可能需要换一台更好的电脑。
game.crash.reason.resolution_too_high=当前游戏由于资源包/纹理包分辨率过高,无法继续运行。\n你可以更换一个分辨率更低的资源包/纹理包,或者更换一个显存更大的显卡。
game.crash.reason.stacktrace=原因未知。请点击日志按钮查看详细信息。\n下面是一些关键词,其中可能包含模组名称,你可以通过搜索的方式查找有关信息。\n%s
game.crash.reason.too_old_java=当前游戏由于 Java 版本过低,无法继续运行。\n你需要在“(全局/版本特定) 游戏设置 → 游戏 Java”中更换 Java %1$s 或更新版本的 Java,并重新启动游戏。如果没有下载安装,你可以点击 <a href="https://learn.microsoft.com/java/openjdk/download">此处</a> 下载 Microsoft JDK。
game.crash.reason.too_old_java=当前游戏由于 Java 版本过低,无法继续运行。\n你需要在“(全局/实例特定) 游戏设置 → 游戏 Java”中更换 Java %1$s 或更新版本的 Java,并重新启动游戏。如果没有下载安装,你可以点击 <a href="https://learn.microsoft.com/java/openjdk/download">此处</a> 下载 Microsoft JDK。
game.crash.reason.unknown=原因未知。请点击日志按钮查看详细信息。
game.crash.reason.unsatisfied_link_error=当前游戏由于缺少本地库,无法继续运行。\n这些本地库缺失:%1$s。\n如果你在“(全局/版本特定) 游戏设置 → 高级设置”中修改了本地库路径选项,请你修改回默认模式。\n如果你正在使用默认模式,请检查游戏文件夹路径是否只包含英文字母、数字和下划线。\n如果是,那么请检查是否为模组或 HMCL 导致了本地库缺失的问题。如果你确定是 HMCL 引起的,建议你向我们反馈。\n<b>对于 Windows 用户,你还可以尝试在“控制面板 → 时钟和区域 → 区域 → 管理 → 更改系统区域设置”中将“当前系统区域设置”修改为“中文(简体,中国大陆)”,并关闭“Beta 版:使用 Unicode UTF-8 提供全球语言支持”选项;</b>\n<b>或将游戏文件夹路径中的所有非英文字符的名称 (例如中文、空格等) 修改为英文字符。</b>\n如果你确实需要自定义本地库路径,你需要保证其中包含缺失的本地库!
game.crash.reason.unsatisfied_link_error=当前游戏由于缺少本地库,无法继续运行。\n这些本地库缺失:%1$s。\n如果你在“(全局/实例特定) 游戏设置 → 高级设置”中修改了本地库路径选项,请你修改回默认模式。\n如果你正在使用默认模式,请检查游戏文件夹路径是否只包含英文字母、数字和下划线。\n如果是,那么请检查是否为模组或 HMCL 导致了本地库缺失的问题。如果你确定是 HMCL 引起的,建议你向我们反馈。\n<b>对于 Windows 用户,你还可以尝试在“控制面板 → 时钟和区域 → 区域 → 管理 → 更改系统区域设置”中将“当前系统区域设置”修改为“中文(简体,中国大陆)”,并关闭“Beta 版:使用 Unicode UTF-8 提供全球语言支持”选项;</b>\n<b>或将游戏文件夹路径中的所有非英文字符的名称 (例如中文、空格等) 修改为英文字符。</b>\n如果你确实需要自定义本地库路径,你需要保证其中包含缺失的本地库!
game.crash.title=游戏意外退出
game.directory=游戏文件夹路径
game.version=游戏版本
game.version=游戏实例
help=帮助
help.doc=Hello Minecraft! Launcher 帮助文档
@@ -500,7 +505,7 @@ input.number=必须是数字
input.not_empty=必填项
input.url=必须是合法的链接
install=添加游戏
install=添加实例
install.change_version=更换版本
install.change_version.confirm=你确定要将 %s 从 %s 更换成 %s 吗?
install.change_version.process=更换版本
@@ -542,8 +547,8 @@ install.modpack=安装整合包
install.modpack.installation=安装整合包
install.name.invalid=名称中包含非 ASCII 字符(如 Emoji 表情或中文字符)。\n建议修改名称。名称建议仅包含英文字母、数字和下划线,以防启动游戏时出现问题。是否继续安装?
install.new_game=安装新游戏
install.new_game.already_exists=此版本已经存在,请换一个名字
install.new_game.current_game_version=当前游戏版本
install.new_game.already_exists=此实例已经存在,请换一个名字
install.new_game.current_game_version=当前游戏实例
install.new_game.installation=安装新游戏
install.new_game.malformed=名字不合法
install.select=请选择安装方式
@@ -585,10 +590,10 @@ lang.default=跟随系统语言
launch.advice=%s 是否继续启动?
launch.advice.multi=检测到以下问题:\n\n%s\n\n这些问题可能导致游戏无法正常启动或影响游戏体验,是否继续启动?\n你可以点击右上角帮助按钮进行求助。
launch.advice.java.auto=当前选择的 Java 版本不满足游戏要求。\n点击“是”即可由 HMCL 来自动选取合适的 Java 版本。\n或者你可以在“(全局/版本特定) 游戏设置 → 游戏 Java”中选择一个合适的 Java 版本。
launch.advice.java.auto=当前选择的 Java 版本不满足游戏要求。\n点击“是”即可由 HMCL 来自动选取合适的 Java 版本。\n或者你可以在“(全局/实例特定) 游戏设置 → 游戏 Java”中选择一个合适的 Java 版本。
launch.advice.java.modded_java_7=Minecraft 1.7.2 及更低版本需要 Java 7 及更低版本。
launch.advice.corrected=我们已经修复了 Java 版本问题。如果你确实希望使用你自定义的 Java,你可以在“(全局/版本特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
launch.advice.uncorrected=如果你确实希望使用你自定义的 Java,你可以在“(全局/版本特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
launch.advice.corrected=我们已经修复了 Java 版本问题。如果你确实希望使用你自定义的 Java,你可以在“(全局/实例特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
launch.advice.uncorrected=如果你确实希望使用你自定义的 Java,你可以在“(全局/实例特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
launch.advice.different_platform=你正在使用 32 位 Java 启动游戏。建议更换至 64 位 Java。
launch.advice.forge2760_liteloader=Forge 14.23.5.2760 与 LiteLoader 不兼容。请更新 Forge 至 14.23.5.2773 或更高版本。
launch.advice.forge28_2_2_optifine=Forge 28.2.2 及更高版本与 OptiFine 不兼容。请降级 Forge 至 28.2.1 或更低版本。
@@ -606,7 +611,7 @@ launch.advice.vanilla_linux_java_8=对于 Linux x86-64 平台,Minecraft 1.12.2
launch.advice.vanilla_x86.translation=Minecraft 尚未为你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n你可以在 <a href="https://learn.microsoft.com/java/openjdk/download">这里</a> 下载 <b>x86-64</b> 架构的 Java 以获得更完整的体验。
launch.advice.unknown=由于以下原因,无法继续启动游戏:
launch.failed=启动失败
launch.failed.cannot_create_jvm=无法创建 Java 虚拟机。可能是虚拟机参数有问题。你可以在“(全局/版本特定) 游戏设置 → 高级设置 → Java 虚拟机设置”中移除所有虚拟机参数后,尝试再次启动游戏。
launch.failed.cannot_create_jvm=无法创建 Java 虚拟机。可能是虚拟机参数有问题。你可以在“(全局/实例特定) 游戏设置 → 高级设置 → Java 虚拟机设置”中移除所有虚拟机参数后,尝试再次启动游戏。
launch.failed.creating_process=启动失败。在创建新进程时发生错误,可能是 Java 路径错误。
launch.failed.command_too_long=命令长度超过限制,无法创建批处理脚本。请导出为 PowerShell 脚本。
launch.failed.decompressing_natives=未能解压游戏本地库。
@@ -617,7 +622,7 @@ 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”中选择一个合适的 Java 版本。\n你可以点击右上角帮助按钮进行求助。
launch.failed.no_accepted_java=找不到适合当前游戏使用的 Java。是否使用默认 Java 启动游戏?点击“是”使用默认 Java 继续启动游戏,\n或者在“(全局/实例特定) 游戏设置 → 游戏 Java”中选择一个合适的 Java 版本。\n你可以点击右上角帮助按钮进行求助。
launch.failed.sigkill=游戏被用户或系统强制终止。
launch.state.dependencies=处理游戏依赖
launch.state.done=启动完成
@@ -677,6 +682,7 @@ message.info=提示
message.success=完成
message.unknown=未知
message.warning=警告
message.question=确认
modpack=整合包
modpack.choose=选择要安装的游戏整合包文件
@@ -685,7 +691,7 @@ modpack.choose.local.detail=你可以直接将整合包文件拖入本页面以
modpack.choose.remote=从互联网下载整合包
modpack.choose.remote.detail=需要提供整合包的下载链接
modpack.choose.repository=从 CurseForge/Modrinth 下载整合包
modpack.choose.repository.detail=下载后记得回到这个界面,把整合包拖进来哦
modpack.choose.repository.detail=可直接在跳转到的页面安装整合包
modpack.choose.remote.tooltip=要下载的整合包的链接
modpack.completion=下载整合包相关文件
modpack.desc=描述你要制作的整合包,比如整合包注意事项和更新记录。支持 Markdown (图片请用网络链接)。
@@ -744,11 +750,11 @@ modpack.wizard.step.2=文件选择
modpack.wizard.step.2.title=选中你想添加到整合包中的文件或文件夹
modpack.wizard.step.3=整合包类型
modpack.wizard.step.3.title=选择整合包导出类型
modpack.wizard.step.initialization.exported_version=要导出的游戏版本
modpack.wizard.step.initialization.exported_version=要导出的游戏实例
modpack.wizard.step.initialization.force_update=强制升级整合包至最新版本 (需要自建服务器)
modpack.wizard.step.initialization.include_launcher=包含启动器
modpack.wizard.step.initialization.save=选择要导出到的游戏整合包位置
modpack.wizard.step.initialization.warning=在制作整合包前,请你确认你选择的版本可以正常启动,\n并保证你的 Minecraft 是正式版而非快照,\n而且不应当将不允许非官方途径传播的模组、资源 (纹理) 包等纳入整合包。\n整合包会保存你目前的下载源设置。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
modpack.wizard.step.initialization.warning=在制作整合包前,请你确认你选择的实例可以正常启动,\n并保证你的 Minecraft 是正式版而非快照,\n而且不应当将不允许非官方途径传播的模组、资源 (纹理) 包等纳入整合包。\n整合包会保存你目前的下载源设置。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
modpack.wizard.step.initialization.server=点击此处查看有关服务器自动更新整合包的制作教程
modrinth.category.adventure=冒险
@@ -883,7 +889,7 @@ mods.not_modded=你需要先在“自动安装”页面安装 Forge、NeoForge
mods.restore=回退
mods.url=官方页面
mods.update_modpack_mod.warning=更新整合包中的模组可能导致整合包损坏,使整合包无法正常启动。该操作不可逆,确定要更新吗?
mods.install=安装到当前版本
mods.install=安装到当前实例
mods.save_as=下载到本地文件夹
nbt.entries=%s 个条目
@@ -948,6 +954,7 @@ world.info.player.food_level=饥饿值
world.info.player.game_type=游戏模式
world.info.player.game_type.adventure=冒险
world.info.player.game_type.creative=创造
world.info.player.game_type.hardcore=极限
world.info.player.game_type.spectator=旁观者
world.info.player.game_type.survival=生存
world.info.player.health=生命值
@@ -1035,9 +1042,9 @@ settings.advanced.modify=编辑高级设置
settings.advanced.title=高级设置 - %s
settings.advanced.custom_commands=自定义命令
settings.advanced.custom_commands.hint=自定义命令被调用时将包含如下的环境变量:\n\
\ · $INST_NAME: 版本名称;\n\
\ · $INST_ID: 版本名称;\n\
\ · $INST_DIR: 当前版本运行路径;\n\
\ · $INST_NAME: 实例名称;\n\
\ · $INST_ID: 实例名称;\n\
\ · $INST_DIR: 当前实例运行路径;\n\
\ · $INST_MC_DIR: 当前游戏文件夹路径;\n\
\ · $INST_JAVA: 游戏运行使用的 Java 路径;\n\
\ · $INST_FORGE: 若安装了 Forge,将会存在本环境变量;\n\
@@ -1047,11 +1054,11 @@ settings.advanced.custom_commands.hint=自定义命令被调用时将包含如
\ · $INST_FABRIC: 若安装了 Fabric,将会存在本环境变量;\n\
\ · $INST_QUILT: 若安装了 Quilt,将会存在本环境变量。
settings.advanced.dont_check_game_completeness=不检查游戏完整性
settings.advanced.dont_check_jvm_validity=不检查 JVM 与游戏的兼容性
settings.advanced.dont_check_jvm_validity=不检查 Java 虚拟机与游戏的兼容性
settings.advanced.dont_patch_natives=不尝试自动替换本地库
settings.advanced.environment_variables=环境变量
settings.advanced.game_dir.default=默认 (".minecraft/")
settings.advanced.game_dir.independent=各版本独立 (存放在 ".minecraft/versions/<版本名>/",除 assets、libraries 外)
settings.advanced.game_dir.independent=各实例独立 (存放在 ".minecraft/versions/<实例名>/",除 assets、libraries 外)
settings.advanced.java_permanent_generation_space=内存永久保存区域
settings.advanced.java_permanent_generation_space.prompt=单位 MiB
settings.advanced.jvm=Java 虚拟机设置
@@ -1070,8 +1077,9 @@ settings.advanced.natives_directory=本地库路径 (LWJGL)
settings.advanced.natives_directory.choose=选择本地库路径
settings.advanced.natives_directory.custom=自定义 (由你提供游戏需要的本地库)
settings.advanced.natives_directory.default=默认 (由启动器提供游戏本地库)
settings.advanced.natives_directory.hint=本选项提供给 Apple M1 等未受游戏官方支持的平台来自定义游戏本地库。如果你不知道本选项的含义,请不要修改本选项,否则会导致游戏无法启动!\n\n如果你要修改本选项,你需要保证自定义文件夹下有游戏所需的本地库文件,如 lwjgl.dll (liblwjgl.so)、openal.dll (libopenal.so) 等文件。启动器不会帮你补全缺少的本地库文件!\n\n注意:指定的本地库文件路径建议只包含英文大小写字母、数字和下划线,否则可能会导致启动游戏失败。
settings.advanced.no_jvm_args=不添加默认的 JVM 参数
settings.advanced.natives_directory.default.version_id=<实例名称>
settings.advanced.natives_directory.hint=本选项提供给 Apple Silicon 等未受游戏官方支持的平台来自定义游戏本地库。如果你不知道本选项的含义,请不要修改本选项,否则会导致游戏无法启动!\n\n如果你要修改本选项,你需要保证自定义文件夹下有游戏所需的本地库文件,如 lwjgl.dll (liblwjgl.so)、openal.dll (libopenal.so) 等文件。启动器不会帮你补全缺少的本地库文件!\n\n注意:指定的本地库文件路径建议只包含英文大小写字母、数字和下划线,否则可能会导致启动游戏失败。
settings.advanced.no_jvm_args=不添加默认的 Java 虚拟机参数
settings.advanced.precall_command=游戏启动前执行命令
settings.advanced.precall_command.prompt=将在游戏启动前调用
settings.advanced.process_priority=进程优先级
@@ -1102,7 +1110,7 @@ settings.custom=自定义
settings.game=游戏设置
settings.game.copy_global=复制全局游戏设置
settings.game.copy_global.copy_all=复制全部
settings.game.copy_global.copy_all.confirm=你确定要覆盖当前版本特定游戏设置吗?此操作无法撤销!
settings.game.copy_global.copy_all.confirm=你确定要覆盖当前实例特定游戏设置吗?此操作无法撤销!
settings.game.current=游戏
settings.game.dimension=游戏窗口分辨率
settings.game.exploration=浏览
@@ -1116,9 +1124,9 @@ 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=版本隔离 (建议使用模组时选择“各版本独立”。改后需移动存档、模组等相关游戏文件)
settings.game.working_directory=版本隔离 (建议使用模组时选择“各实例独立”。改后需移动存档、模组等相关游戏文件)
settings.game.working_directory.choose=选择运行文件夹
settings.game.working_directory.hint=在“版本隔离”中选择“各版本独立”使当前版本独立存放设置、存档、模组等数据。使用模组时建议启用此选项以避免不同版本模组冲突。修改此选项后需自行移动存档等文件。
settings.game.working_directory.hint=在“版本隔离”中选择“各实例独立”使当前实例独立存放设置、存档、模组等数据。使用模组时建议启用此选项以避免不同实例模组冲突。修改此选项后需自行移动存档等文件。
settings.icon=游戏图标
@@ -1134,10 +1142,6 @@ settings.launcher.download_source=下载源
settings.launcher.download_source.auto=自动选择下载源
settings.launcher.enable_game_list=在主页内显示版本列表
settings.launcher.font=字体
settings.launcher.font.anti_aliasing=抗锯齿 (重启后生效)
settings.launcher.font.anti_aliasing.auto=自动
settings.launcher.font.anti_aliasing.gray=灰度
settings.launcher.font.anti_aliasing.lcd=子像素
settings.launcher.general=通用
settings.launcher.language=语言 (重启后生效)
settings.launcher.launcher_log.export=导出启动器日志
@@ -1174,13 +1178,13 @@ settings.physical_memory=物理内存大小
settings.show_log=查看日志
settings.tabs.installers=自动安装
settings.take_effect_after_restart=重启后生效
settings.type=版本游戏设置类型
settings.type.global=全局游戏设置 (未启用“版本特定游戏设置”的版本共用此设置)
settings.type=实例游戏设置类型
settings.type.global=全局游戏设置 (未启用“实例特定游戏设置”的实例共用此设置)
settings.type.global.manage=全局游戏设置
settings.type.global.edit=编辑全局游戏设置
settings.type.special.enable=启用版本特定游戏设置 (不影响其他游戏版本)
settings.type.special.edit=编辑版本特定游戏设置
settings.type.special.edit.hint=当前游戏版本“%s”启用了“版本特定游戏设置”,因此本页面选项不对该版本生效。点击链接前往该版本的“游戏设置”页。
settings.type.special.enable=启用实例特定游戏设置 (不影响其他游戏实例)
settings.type.special.edit=编辑实例特定游戏设置
settings.type.special.edit.hint=当前游戏实例“%s”启用了“实例特定游戏设置”,因此本页面选项不对该实例生效。点击链接前往该实例的“游戏设置”页。
sponsor=赞助
sponsor.bmclapi=国内下载源由 BMCLAPI 提供高速下载服务。BMCLAPI 为公益服务,赞助 BMCLAPI 可以帮助作者更好地提供稳定高速的下载服务。[点击此处查阅详细信息]
@@ -1215,13 +1219,15 @@ update.note=开发版与预览版包含更多的功能以及错误修复,但
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器。网址已经复制到剪贴板,你可以手动粘贴网址打开页面。
update.tooltip=更新
update.preview=提前预览 HMCL 版本
update.preview.tooltip=启用此选项,你将可以提前获取 HMCL 的新版本,以便在正式发布前进行测试。
version=游戏
version.name=游戏版本名称
version.cannot_read=读取游戏版本失败,无法进行自动安装
version.empty=没有游戏版本
version.name=游戏实例名称
version.cannot_read=读取游戏实例失败,无法进行自动安装
version.empty=没有游戏实例
version.empty.add=进入下载页安装游戏
version.empty.launch=没有可启动的游戏。点击“确定”将进入“下载”页面。\n你也可以点击 HMCL 主界面左侧的“下载”按钮安装游戏,或在“版本列表”切换游戏文件夹。
version.empty.launch=没有可启动的游戏。点击“确定”将进入“下载”页面。\n你也可以点击 HMCL 主界面左侧的“下载”按钮安装游戏,或在“实例列表”切换游戏文件夹。
version.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹,或者点击此处进入游戏下载页面。
version.game.old=远古版
version.game.release=正式版
@@ -1230,29 +1236,30 @@ version.game.snapshot=快照
version.game.snapshots=快照
version.launch=启动游戏
version.launch.test=测试游戏
version.switch=切换版本
version.switch=切换实例
version.launch_script=生成启动脚本
version.launch_script.failed=生成启动脚本失败
version.launch_script.save=保存启动脚本
version.launch_script.success=启动脚本已生成完毕:%s
version.manage=版本列表
version.manage=实例列表
version.manage.clean=清理游戏文件夹
version.manage.clean.tooltip=清理“logs”和“crash-reports”文件夹
version.manage.duplicate=复制游戏版本
version.manage.duplicate=复制游戏实例
version.manage.duplicate.duplicate_save=复制存档
version.manage.duplicate.prompt=请输入新游戏名称
version.manage.duplicate.confirm=新的游戏版本将复制该版本文件夹 (".minecraft/versions/<版本名>") 下的文件,并带有独立的运行文件夹和设置。
version.manage.manage=版本管理
version.manage.manage.title=版本管理 - %1s
version.manage.duplicate.confirm=新的游戏实例将复制该实例文件夹 (".minecraft/versions/<实例名>") 下的文件,并带有独立的运行文件夹和设置。
version.manage.manage=实例管理
version.manage.manage.title=实例管理 - %1s
version.manage.redownload_assets_index=更新游戏资源文件
version.manage.remove=删除该版本
version.manage.remove.confirm.trash=真的要删除版本“%s”吗?你可以在系统的回收站中还原“%s”文件夹来找回该版本。
version.manage.remove.confirm.independent=由于该游戏启用了“(全局/版本特定) 游戏设置 → 版本隔离 → 各版本独立”选项,删除该版本将导致该游戏的存档等数据一同被删除!真的要删除版本“%s”吗?
version.manage.remove=删除该实例
version.manage.remove.confirm=真的要删除实例“%s”吗?你将无法找回被删除的文件!!!
version.manage.remove.confirm.trash=真的要删除实例“%s”吗?你可以在系统的回收站中还原“%s”文件夹来找回该实例。
version.manage.remove.confirm.independent=由于该游戏启用了“(全局/实例特定) 游戏设置 → 版本隔离 → 各实例独立”选项,删除该实例将导致该游戏的存档等数据一同被删除!真的要删除实例“%s”吗?
version.manage.remove_assets=删除所有游戏资源文件
version.manage.remove_libraries=删除所有库文件
version.manage.rename=重命名该版本
version.manage.rename=重命名该实例
version.manage.rename.message=请输入要修改的名称
version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符。
version.manage.rename.fail=重命名实例失败,可能文件被占用或者名字有特殊字符。
version.settings=游戏设置
version.update=更新整合包
Binary file not shown.
@@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="%lang%">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
File diff suppressed because it is too large. Load diff
+185 -41
View File
@@ -170,7 +170,7 @@ all-the-mods-6;164;;All the Mods 6;;ATM6
garden-of-glass-questbook-edition;165;;Garden of Glass (Questbook Edition);;
;166;;圣剑传奇;Sword Legendary;
;167;;FTB Presents Direwolf20 1.16;;
team-rustic;168;;Rusty Sink 1.20;;
team-rustic;168;;Rusty Sink 2;;
enigmatica2expertskyblock;169;;Enigmatica 2: Expert Skyblock;;E2ES
;170;;休闲-生存-2021版;Leisure Survival-2021;ls21
hexxit-updated;171;;Hexxit Updated;;
@@ -195,7 +195,7 @@ mineshafts-monsters;189;;矿井与怪物;Mineshafts & Monsters;
crazy-craft-remastered;190;;Crazy Craft Remastered;;
engineers-life-2;191;;Engineer's Life 2;;EL2
hr-new-beginnings-2;192;;HR: New Beginnings 2;;
minecolonies-testpack;193;;模拟殖民地;MineColonies Official;
minecolonies-official;193;;模拟殖民地;MineColonies Official;
tnp-limitless;194;;TNP Limitless;;
tnp-limitless-2;195;;TNP Limitless 2;;
tnp-limitless-3;196;;TNP Limitless 3;;
@@ -251,9 +251,9 @@ fabulously-optimized;248;;难以置信的优化;Fabulously Optimized;FO
better-minecraft-otv-edition;249;;Better Minecraft (OfflineTV Edition);;
evolution-reset;250;;进化:重启;Evolution: Reset;ER
the-nuclear-wasteland;251;;核荒原;The Nuclear Wasteland;
blessed-or-cursed-expedition-to-infinite-force;252;;祝福或诅咒:无限力量之征;Blessed or Cursed: Expedition to Infinite Force;BOC
blessed-or-cursed-expedition-to-infinite-force;252;;祝福或诅咒:无限力量之征;Blessed or Cursed: Expedition to Infinite Force;BoC
;253;;边界生存;Border Survival;BSV
medieval-mc-fabric;255;;Medieval Minecraft;;MMC
medieval-mc-forge-mmc1;255;;Medieval Minecraft;;MMC
heavens-of-sorcery;256;;巫术天堂;Heavens of Sorcery;
herodotus;257;;希罗多德;Herodotus-Authentic Edition:Official;HDS
;258;;重生世界;Rebirth world;RW
@@ -282,7 +282,7 @@ world-of-dragons-ii;282;;World of Dragons II;;
tensura-unleashed;283;;Tensura Unleashed;;
skyopolis-4;284;;Skyopolis 4;;
ftb-oceanblock;286;;FTB OceanBlock;;
tolkiencraft-iii-return-to-middle-earth;287;;TolkienCraft III - Return to Middle-earth;;
tolkiencraft-iii-return-to-middle-earth;287;;托尔金工艺3 - 重返中洲;TolkienCraft III - Return to Middle-earth;TC3
delivery-inc;288;;Delivery Inc.;;
poetica;289;;Poetica;;
divine-journey-2;290;;神圣之旅2;Divine Journey 2;DJ2
@@ -354,7 +354,7 @@ all-the-mods-expert-remastered;355;;All The Mods Expert: Remastered;;
all-the-mods-0;356;;All the Mods 0;;ATM0
all-the-mods-fabric;357;;All the Mods Fabric;;ATMF
the-pixelmon-modpack;358;;The Pixelmon Modpack;;
better-minecraft-skyblock;359;;更好的我的世界:空岛;Better Minecraft Skyblock;
better-mc-skyblock;359;;更好的我的世界空岛;Better MC Skyblock;BMCS
age-of-fate;360;;命运时代;Age of Fate;AOF
minecraft-kingdoms;361;;MC Kingdoms;;
;362;;幻想旅程;Imaginary Journey;
@@ -413,7 +413,6 @@ epoch-runner-formerly-nuclear-beyond-modpack;417;;时代领跑人;Epoch Runner;
;418;;星海征途;The Journey of Star Sea;TJoSS
roguelike-adventures-and-dungeons-2;419;;冒险与地牢2;Roguelike Adventures and Dungeons 2;RAD2
multiskyblock;420;;模块化空岛;MultiSkyBlock;MSB
;421;;人性化;Humanized;H
create-live-3;422;;Create Live 3;;
magnificia;423;;Magnificia;;
;424;;魔法金属-新手向导;ManaMetalMod - WithNewbHelper;
@@ -484,7 +483,7 @@ dimensionhopper;490;;Dimension Hopper:The Fall;;
technology-empty-island;491;;科技空岛;RxanGrben;
greed-the-resource-4-infinity;492;;Greed the Resource 4 - Infinity;;
caveopolis;493;;Caveopolis;;
;494;;硬核生存;HardCraft;HC
;494;;坚毅征途;HardCraft;HC
;495;;怀末韵云;Institutions mz;MZ1
feed-the-factory;496;;Feed the Factory;;FTF
fantasy-realm;497;;幻想领域;Fantasy Realm;
@@ -567,7 +566,6 @@ expert-of-magic-art-world;573;;魔艺世界;Expert of Magic Art World;EOMAW
nuclear-radiation-2;576;;核污染二;nuclear radiation 2;NR2
;577;;白狐铃的奇思妙想;Arctic Fox Ling Fantasy;AFLF
;578;;MineSpace;;MS
;579;;史诗战斗:兵团战争;EPIC Fight Corps War;EFC
;580;;月见清兰的个人优化;TskimiSeiran's Idea;TkSrI
;581;;兔头:真视世界;;
;582;;灾厄战争;;
@@ -584,7 +582,7 @@ the-winter-frontier;591;;冬境边域;The Winter Frontier;TWF
;593;;太阳机械厂;Create Sun;CS
dawn-craft;594;;破晓之界;DawnCraft: Echoes of Legends;DC
aof-presents-skylore;595;;AOF: Skylore;;
keletus-gregpack;596;;可乐兔的格雷整合包;Keletu's GregPack;
keletus-gregpack;596;;可乐兔的格雷整合包;Keletu's GregPack;GtMagic
ftb-skies;597;;FTB Skies;;
tnp-limitless-5;598;;TNP Limitless 5;;
create-back-on-track;599;;Create: Back on Track;;
@@ -614,8 +612,8 @@ eternal-night-modpacks;622;;永夜;Eternal Night;ETN
all-the-mods-9;623;;All the Mods 9;;ATM9
spoornpack;624;;Spoornpack;;
;625;;起源RPG;;ORPG
;626;;钓月织梦;Tsuri No Gatsu;TSNG
;627;;冰河物语;Ice Age Fairy;IAF
;626;;织彩钓月;Oriun Tagayasu;OTSU
;627;;冰河物语;;
hommage;628;;Hommage;;
days-to-craft;629;;Seven Days To Craft;;
emcworld;630;;质能世界;EMCWorld;
@@ -668,7 +666,7 @@ journey-watch-ytz;676;;征途 守望;Journey Watch;JW
;677;;Arcomua Lite 原版整合;Arcomua Lite;AL
incremental-industries;678;;Incremental Industries;;
;679;;海王星计划:重构;NeptunePlan:Reboot;NPR
;680;;中世纪生存日记;Medieval Survival Diary;MSD
;680;;求生日记:荒野;Survival Diary: Wilderness;SWD
a-new-technological-era;681;;A New Technological Era;;ANTE
;682;;超能力之旅;Meet Your Superpower;MYS
musketeer;683;;Musketeer - One for All;;
@@ -692,7 +690,7 @@ enigmatica9;699;;Enigmatica 9;;E9
create-arcane-engineering;701;;机械动力:奥术工程;Create: Arcane Engineering;CAE
chroma-endless-2;702;;Chroma Endless 2;;
techrevolution;703;;TechRevolution;;
cabin;704;;CABIN: Create Above & Beyond In Newer;;CABIN
cabin;704;;Create Above & Beyond In Newer;;CABIN
aoc;705;;All of Create;;
;706;;交错维度3;Among Dimensions 3;AD
gregfactory-sky;707;;GregFactory Sky;;GFS
@@ -735,8 +733,8 @@ life-in-the-village-3;740;;Life in the village 3;;LitV3
;745;;FTB Genesis;;
cook-in-skyblock;746;;天空厨房;Cook in skyland;CiS
;747;;被恐惧支配 1;Dominated by Fear 1;DoF
new-vanilla-modpack;748;;新的原版;New Vanilla;NVA
;749;;群峦救援非官方版;Terrafirma Rescue Unofficial;TFRU
new-vanilla-newhome;748;;新的原版;New Vanilla;NVA
;749;;峦屿梦星群;Terrafirma Rescue Unofficial;TFRU
stoneopolis;750;;Stoneopolis;;
ftb-skies-expert;751;;FTB Skies Expert;;
wmshd-pack-what-mojang-should-have-done;752;;What Mojang Should Have Done;;WMSHD
@@ -744,7 +742,7 @@ wmshd-pack-what-mojang-should-have-done;752;;What Mojang Should Have Done;;WMSHD
;754;;随心之旅;spontaneous journey;
oxmodpack;755;;OxMODPACK 8 +Online;;
gregtech-community-pack-modern;756;;GregTech Community Pack Modern;;GCPM
;757;;锻造大师3;Master Blacksmith3;MB3
;757;;锻造大师4;Master Blacksmith4;MB4
;758;;科技用于探索世界;Technology Exploration World;
;759;;幻梦闲笔;Aetherial Voyage;
fear-the-mist;760;;雾中的恐惧;Fear The Mist;
@@ -764,7 +762,7 @@ infernal-rpg;771;;Infernal Origins;;
;774;;AIR for JAVA;;AFJ
;775;;Create Extra;;
frs-optimisation-for-survival;776;;Fr的生存优化;Fr's Optimisation for Survival;FROS
;777;;Exile Magic Constructor;;EMC
exile-magic-constructor;777;;Exile Magic Constructor;;EMC
;778;;Landscapes Reimagined Genesis;;
al-nl;779;;冒险生活 - 新生活;Adventure Life - New Life;AL-NL
harvista;780;;拾穗;Harvista;
@@ -819,19 +817,19 @@ all-the-mods-9-to-the-sky;828;;All The Mod 9 - To The Sky;;ATM9S
fallenpetals;830;;落英;FallenPetals;FP
dungeon-heroes;831;;Dungeon Heroes (RPG Series);;
play-as-dragon-gothic-edition;832;;Play as Dragon: Gothic Edition;;
skyblock-burgeria;833;;空岛汉堡店;;
skyblock-burgeria;833;;空岛汉堡店;Skyblock Burgeria;
crafting-craft-2;834;;合成工艺2;Crafting Craft 2;
;835;;僵尸入侵100天;Zombie Invade 100 Days;ZI100D
compact-world-expert;836;;Compact World Expert;;CWE
a-fragment-of-peace;837;;宁然一隅;A Fragment of Peace;AFoP
;837;;宁然一隅;A Fragment of Peace;AFoP
easy-create-modpack;838;;Easy Create;;
;839;;流线鱼概念;Fish Spi Rally;FSRMP
meatballcraft;840;;肉丸工艺,次元提升;MeatballCraft, Dimensional Ascension;
meatballcraft;840;;肉丸工艺,次元飞升;MeatballCraft, Dimensional Ascension;MBC
seaopolis-submerged;841;;海之城: 潜没水中;Seaopolis: Submerged;
;842;;空中厕所;PoopSky;
compact-sky;843;;收缩空岛;Compact Sky;
back-to-back;844;;背靠背;Back to Back;BTB
adventure-exploration-part2;845;;冒险与探索;Adventure & Exploration;AnE
ane2;845;;冒险与探索;Adventure & Exploration;AnE
;846;;复兴;Renaissance;
live-like-a-castaway;847;;木筏求生2;Raftcraft2;RC2
mermaid-legend;848;;人鱼传说;Mermaid legend;
@@ -847,7 +845,6 @@ create-prepare-to-dye;856;;Create Prepare to Dye;;
create-leisurely-delight;858;;机械动力:悠然乐事;Create Delight;CD
;859;;蔚蓝悠悠;Azure Leisurely;AL
infinityevolved-reloaded;860;;Infinity Evolved: Reloaded;;
;861;;空中厕所:生产线;Poopsky:Assembly Line;PAL
;862;;佛罗伦萨迷思;Fantasy in Florence;FIF
craftoria;863;;Craftoria;;
mechanic-craft;864;;MechanicCraft;;
@@ -878,7 +875,7 @@ create-stellar;888;;Create Stellar;;
monifactory;889;;Monifactory;;
create-factory-builder;890;;Create: Factory Builder;;
;891;;群峦传说:宝宝快乐版;TerraFirma: Baby Happy Edition;TFBH
;892;;阶段: 残骸;Phase: the Debris;
;892;;阶段: 残骸;Phase: the Debris;PTD
;893;;AE管道;AE Pipes;APE
skyt-2;894;;SkyT 2;;
stagecraft;895;;StageCraft;;
@@ -909,7 +906,7 @@ strictly-medieval-3-forge;917;;Strictly Medival 3;;
easy-to-ascension;920;;易如飞升;Easy to Ascension;
;921;;幽匿末日;The Last Day of Sculk;TLDS
;922;;交错空岛;Crossisland;CrI
;923;;机械动力:魔法乐事;Create Magic's Delight;CRMD
;923;;机动工程:刀剑与魔法;Mechanical Engineering: SlashBlade and Magic;CRMD
minetech-evolution;924;;科技进化;Minetech Evolution;MTE
gregified-sky-edition;925;;Gregified: Sky Edition;;
;926;;原版扩展;Additonal Vanilla;AV
@@ -917,14 +914,14 @@ create-on-a-potato;927;;Create on a Potato PC: Ultimate;;CPU
;928;;现实主义;realism;RLM
;929;;晋升之路;path of advance;POA
;930;;永无止境:超越常新;Above and Beyond : Refresh;CABR
cosmic-frontiers;931;;Cosmic Frontiers;;
cosmic-frontiers;931;;星河边疆;Cosmic Frontiers;CF
integrated-minecraft;932;;Integrated MC;;
;933;;Rising Legends;;
;934;;城乡之旅;Travel Township;TT
;934;;城乡之旅;Harvest's Journey;HJ
;935;;龙鸣牢大;FarmerKobeBryant;FKB
setgamedifficulty-reika;936;;SetGameDifficulty Reika;;
epic-journey-gtceu;937;;Epic Journey GTCEu;;
;938;;飞渡群峦;TerraFirmaCraft:Leap The Top;TFC:LTT
terrafirmacraft-leap-the-top;938;;飞渡群峦;TerraFirmaCraft:Leap The Top;TFC:LTT
gregtech-skybound;939;;Gregtech: Skybound;;
simple-tfc;940;;简单群峦;SimpleTFC;
ftb-presents-direwolf20-s14;941;;FTB Presents Direwolf20 1.21;;
@@ -948,10 +945,10 @@ gregtania6-skyblock;959;;Gregtania6 Skyblock;;
harmonious-engineering;960;;Harmonious Engineering;;
valley-of-farming-and-ranching;961;;农牧物语;Valley of Farming and Ranching;
;962;;机械动力:火车;Create Train;
;963;;理想国冒险之旅;;AOIC
the-old-era-of-the-republic-fot-yifei;963;;理想国冒险之旅;;AOIC
;964;;生欲:求生2;Vivere The Zombie Apocalypse-II;VZA2
;965;;起源-世界;OriginCraft Pulses;
;966;;Legacies;;
legacies-mc;966;;Legacies;;
the-legend-of-2900;967;;海上机械师;The Legend Of 2900;TLOU
mirasites-100-days;968;;畸变:蛊之奇境;Mirasites-BrokenMemories;MBM
;969;;末日余晖:独自一人;;MRYH
@@ -970,7 +967,7 @@ venoms-craft;981;;Venom's Craft;;
;982;;慢生活冒险;;
;983;;多元工艺:绘镇曲;Painted Town Melody;PTM
;984;;遗忘之海;Leisurely Life;
;985;;幻想国度;F_Land;
;985;;幻想国度;Fancy_Land;
;986;;群峦传说:重制;TerraFirmaCraft Remake;TFCRe
;987;;未来;;
;988;;起源生物—崛起;;
@@ -988,12 +985,12 @@ industrial-machines;1000;;Industrial Machines;;
auto-terrafirmacraft-tfc-create;1001;;Auto-TerraFirmaCraft (TFC + Create);;ATFC
;1002;;时间之书:星的日记;The Book of Time: A Star's Journal;TASL
;1003;;特摄世界;Tokusatsu World;TW
journey-of-reincarnation;1004;;转世之旅;Journey of Reincarnation;
journey-of-reincarnation;1004;;转世之旅;Tensura: Journey of Reincarnation;
;1005;;冒险与休闲;Adventure and Leisure;AL
enigmatica10;1006;;Enigmatica 10;;E10
mc-chocolate-edition;1007;;The Chocolate Edition;;
mc-chocolate-edition;1007;;Chocolate Edition Minecraft;;
civilization-l;1008;;Civilization-L;;
;1009;;群峦:降临;TerraFirma:Advent;TFA
terrafirma-advent;1009;;群峦:降临;TerraFirma:Advent;TFA
valhelsia-7;1010;;Valhelsia 7;;
mechanical-mastery-plus;1011;;Mechanical Mastery Plus;;
ashes-to-ashes;1012;;尘归尘;Ashes To Ashes;ATA
@@ -1001,23 +998,23 @@ ashes-to-ashes;1012;;尘归尘;Ashes To Ashes;ATA
create-silence-and-prosperity;1014;;机械动力:寂静与繁荣;Create Silence and Prosperity;CSP
mif;1015;;MI:Foundation;;MIF
c2mse2;1016;;Create 2 Mekanism: Sky Edition 2;;C2MSE2
;1017;;Tsuri No Gatsu 5 - FDCraft Version;;TSNG5-FDC
;1017;;耕云钓月;Kumo O Tagayasu;KOTS
;1018;;无限战争4;Infinity War4;INF4
;1019;;群峦传说:工业长路;;
;1020;;自然冒险 初生;Nature Adventure Newborn;NAB
;1021;;羽舲的基础优化;Hanekmio's Basis Optimization;
;1020;;自然冒险 新生;Nature Adventure Newborn;NAB
hmbo;1021;;羽舲的基础优化;Hanekmio's Basis Optimization;
create-star-valley;1022;;Create: Gears of History;;
;1023;;阿尔法计划;AlphaPlan;AP
spell-dimension;1024;;咒次元;Spell Dimension;SD
skyfactory-5;1025;;天空工厂 5;SkyFactory 5;SF5
;1026;;破劫之刃;Break Disaster;
reminiscent-create;1027;;Reminiscent Create;;
techsert;1028;;Techsert;;TH
techsert;1028;;匿踪之城;Techsert;TH
prehistoric-world-dinosaurs-adventure;1029;;Prehistoric World - Dinosaurs & Adventure;;
terminal;1030;;终末;Terminal;
;1031;;异次元水晶 3;Isolated Crystal 3;
isolated-crystal-3;1031;;异次元水晶 3;Isolated Crystal 3;
;1032;;机械动力:无限总是小于零;Create:Infinity Always Smaller Than Zero;CIASZ
;1033;;踪灭I-开端;;
;1033;;踪灭I-起源;;
;1034;;踪灭II;;
rabbits-dawn-craft;1035;;某兔子的破晓之界;A Certain Rabbit's DawnCraft - Dawn of the Deep Soul;RDC
;1036;;铁砧天空;;ASKY
@@ -1027,7 +1024,7 @@ the-adventure-of-valkyriens;1038;;瓦尔基里大冒险;The Adventure of Valkyri
;1040;;群峦传说:天才版;;
compression;1041;;Compression;;
compact-sky-easy;1042;;Compact Sky: Easy;;CSE
;1043;;丰收与探索;;
;1043;;丰收与探索;Harvest and Exploration;
;1044;;脆骨症:黯光;No Flesh Within Chest-DIM;NFWC-DIM
;1045;;小雷的优化冒险;;
cobblemon-eternal-edition;1046;;Cobblemon Eternal;;
@@ -1035,3 +1032,150 @@ cobblemon-eternal-edition;1046;;Cobblemon Eternal;;
gregtech-expert;1048;;GregTech Expert;;
;1049;;Beta Horizons;;
minecraft-legendary-edition;1050;;Legendary Edition;;
;1051;;Origenceal LPackage;;OLP
;1052;;我的世界:未闻的故事;;mtus
;1053;;Versatile;;
;1054;;勇者之章 Ⅲ;Chapter of Yuusha Ⅲ;CY3
forge-frontier;1055;;Create - Forge Frontier;;
create-workshop-uwu;1056;;Create: Workshop UwU Edition;;
;1057;;古钠重铸;Oldium Optimized;OO
;1058;;越寰;Leaping across the world;LTW
ftb-oceanblock-2;1059;;FTB OceanBlock 2;;
;1060;;伊甸空天2;Eden Aerospace 2;EAS2
create-infinity-mechanism;1061;;机械动力:无限构件;Create:Infinity Mechanism;CIM
beyond-depth;1062;;Beyond Depth;;
;1063;;枪械交界地;TACZ Amid Crucible Zone;
;1064;;真幻世界;Realfantacy World;R.F.
minenufactory;1065;;创世工厂;Minenufactory;MF
craft-to-exile-2;1066;;放逐之路2;Craft to Exile 2 (VR Support);CTE2
;1067;;龙境III:菌土重来;Dragon Odyssey:Return of the Fungal Earth;DO3
the-culinary-journey;1068;;美食的旅途;The Culinary Journey;TCJ
;1069;;缥缈方舟;;PMFZ
;1070;;东方匠魂传;Touhou Tinker Fantasy;TTF
;1071;;亵渎;;MYBL
techopolis-3;1072;;Techopolis 3;;
sky1234;1074;;sky1234;;
star-technology;1075;;Star Technology;;
;1076;;Hyper World;;HW
;1077;;妖月的魔法金属整合包;yaoyaoDream's ManaMetalMod Modpack;YMM
techcraft-severwinter;1078;;科技工艺:严冬;TechCraft: SevereWinter;TCSW
randblock2-winterize;1079;;随机空岛 2:银装素裹;RandBlock2:Winterize;
create-the-brass-concerto;1080;;机械动力:黄铜协奏曲;Create: The Brass Concerto;CBC
explorers-eve;1081;;Explorer's Eve;;
all-the-magic-arcana;1082;;All the Magic :Arcana;;ATMAr
;1083;;创想新世界;Inspired Creations: New World;ICNW
farming-crossing-5-made-to-order;1084;;农业森友会 5;Farming Crossing 5: Made to Order!;
;1085;;Ben10 Craft;;BEN10
;1086;;渺小的世界;Small world;
mierno;1087;;迷厄路;Mierno;
;1088;;最后那些天;Those last day;
;1089;;泠月狐基础整合;LunarFox;
roguelike-adventures-and-dungeons-3;1090;;冒险与地牢3;Roguelike Adventures and Dungeons 3;RAD3
;1091;;羽舲界;Hanekmio Instance;HMI
;1092;;玄理2:洗剪版;EnigTech 2 - Dishwasher's Cut;ET2DC
wheelchair-gregtech;1093;;Wheelchair Gregtech;;
egged-up;1094;;Egged Up;;
;1095;;香草纪元:食旅纪行;VanillaEra: FaresChron;
;1096;;愚者;The Fool;TF
;1097;;时间之书2:星梦指南;BookofTime II Stardream Compass;BKSC
;1098;;群峦之上:无垠伊土;EndlessFirma;EnF
;1099;;血月低语:暗夜惊魂;Bloodmoon's Whisper;BW
;1100;;MC饭;Delicious in Minecraft;DIM
;1101;;美味的甜点总是有毒;;
delight-desert-chef;1102;;乐事:沙漠厨师;Delight: Desert Chef;DDC
;1103;;异界随行诗;Otherworld Traveling Poems;
;1104;;无边荒漠;;
;1105;;空中厕所2;PoopSky2;
fear-nightfall;1106;;Fear Nightfall;;
homestead-cozy;1107;;Homestead - A Cozy Survival Experience;;HD
madness-of-the-skies;1108;;Madness of the skies;;
;1109;;魔之逆鳞;Nemesis of Demons;NOD
emc-and-fe-plus2;1110;;EMC & FE ++;;E&F
;1111;;云之谷;CloudValley;
bm-exosphere;1112;;BM: Exosphere;;BME
;1113;;锻造大师:侠之器;Master Blacksmith:Chivalrous blade;MB:CB
;1114;;自然;Nature;NA
;1115;;农场物语;FarmingTales;FT
optimaxcraft;1116;;OptiMaxCraft;;OMC
;1117;;决战黎明;Decisive Battle at Dawn;DBAD
floaty-creatoria;1118;;天工浮生;Floaty Creatoria;
;1119;;简约生电优化;Simplicity Survival Optimization;SSO
;1120;;星辰之下;Under the Stars;UtS
madness-of-the-skies-2;1121;;Madness of the Skies 2;;
raspberry-flavoured;1122;;Raspberry Flavoured;;RF
skillet-man;1123;;平底锅侠;Skillet Man;
;1124;;格雷科技:新征程;GregTech Odyssey;GTO
;1125;;无中生无尽;Ex Nihilo Ad Avaritia;ENAA
;1126;;Undertale;;
create-creative-and-beyond;1127;;机械动力:创造与超越;Create Creative and Beyond;CCB
create-delight-remake;1128;;机械动力:齿轮盛宴;Create:Delight Remake;CDR
ftb-skies-2;1129;;FTB Skies 2;;
;1130;;星辰时代:科技魔法;Stellar Era: Technomagic;SETM
aircrashed-engineer;1131;;坠机工程师;Aircrashed Engineer;ACE
project-tfcreate;1132;;Project: TFCreate;;
;1133;;落幕曲;;
;1134;;超大陆:勇气;Supercontinent:Pluck;SUPL
create-chronicles-the-endventure;1135;;Create Chronicles: The Endventure;;
life-in-the-village-4;1136;;Life in the village 4;;LITV4
real-island-2-modular-craft;1137;;真实空岛2:模块化之旅;Real Island 2 - Modular Craft;RI2
;1138;;奥术咖啡馆;;
unyielding;1139;;Unyielding;;
;1140;;剑与王国;;SNK
the-fungal-infection-coop;1141;;The Fungal Infection | Co-Op Base Defense;;
mc-eternal-2;1142;;永恒的MC 2;MC Eternal 2;MCE2
solo-leveling-reawakening;1143;;Solo Leveling - Reawakening;;
create-ultimate-selection;1144;;Create: Ultimate Selection - QUESTS NOW!;;
;1145;;元素觉醒;Elemental Awakening;
endless-horror-the-ultimate-horror-modpack;1146;;Endless Horror - The ULTIMATE Horror Experience;;
;1147;;锻造之旅;;
picky;1148;;Picky;;
resource-factory;1149;;资源工厂;Resource Factory;
stacia-2-expert;1150;;Stacia 2 Expert;;S2E
;1151;;田园乐事;;
;1152;;世界之旅;Journey of the world;JOTW
;1153;;清新优化;Fresh optimization;FRO
;1154;;遗世之匠;Touhou:The Forgotten Swordsmith;
;1155;;千禧:蝉鸣之时;;
skyrealm-descent;1157;;Skyrealm: Descent;;
;1158;;远征:以太利亚;Expedition: Ætheria;
;1159;;快速优化;Rapid Optimization;RO
liminal-industries;1160;;阈限工业;Liminal Industries;
;1161;;史诗冒险;;EA
;1162;;根自日常;ROOTINE;
blue-archive-x-mineraft;1163;;我的世界x蔚蓝档案;BlueArchive x Minecraft;BACraft
;1164;;Fresh & Smooth;;
;1165;;机动重启工程:风之旅;The Caravan of Winds;TCW
;1166;;Furry Fandom Club Boss Rush Official Offline;;FFCBR
immersive-ultra;1167;;极致沉浸;Immersive Ultra;
;1168;;Polymania;;
survivearchive;1169;;SurviveArchive - A Blue Archive modpack;;
beloong;1170;;化龍;BeLoong;
;1171;;畅游自然;;SiF
;1172;;退休工艺;Retirement Craft;RmC
;1173;;随机枪械乱斗;NimShade's Random Gunfight;NRG
;1174;;雪狐的基础整合;SF Integrated Modpack;SFI
;1175;;硬核惊变;Hardcore Surprise;HS
society-sunlit-valley;1176;;Society: Sunlit Valley;;
ciscos-fantasy-medieval-rpg-dragonfyre;1177;;Cisco's Fantasy Medieval RPG [Dragonfyre];;
all-the-mods-10-sky;1178;;All the Mods 10: To the Sky;;ATM10S
;1179;;谐伴匠途;;
the-cursed-wanderer-reborn;1180;;咒游诸界:重生;The Cursed Wanderer: Reborn;TCWR
;1181;;Turbo Vulkan;;TuVk
uls-zsurvival-craft;1182;;Z-Liminex;;
;1183;;MITE 重生 3;MITE Reborn 3;MR
reclamation-reclaim-the-world;1184;;Reclamation - Reclaim the World!;;
;1185;;寰宇地球;Earthworld;EW
pokehaan-craft-2;1186;;Pokehaan Craft 2;;
project-nope-zone;1187;;Project Nope Zone;;PNZ
all-the-simple-zero;1188;;一切简易;All the Simple;ATS
;1189;;匠战妖域;Tinkers' Warfare Demon Realm;twdr
;1190;;空想生存;;
;1191;;东方起源鉴;Touhou Origins Annals;TOA
realm-of-destiny;1192;;生命之境;Realm of Destiny;ROD
;1193;;奇妙的世界;Special World;
;1194;;毁灭;DESTRUCTION;DES
pvzw;1195;;植物大战僵尸:世界;Plants VS Zombies : World;PVZW
;1196;;农场物语Forge版;FarmingTales Forge;FT-WT
new-century-adventure;1197;;新世纪之险;New Century Adventure;NCA
repalmon;1198;;Re帕鲁梦;;RPLM
structural-ingenuity;1199;;结构型奇思妙想;Structural Ingenuity;
@@ -4007,6 +4007,17 @@
"size": 27971214
}
}
},
"software-renderer-loader": {
"name": "org.glavo:llvmpipe-loader:1.0",
"downloads": {
"artifact": {
"path": "org/glavo/llvmpipe-loader/1.0/llvmpipe-loader-1.0.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/llvmpipe-loader/1.0/llvmpipe-loader-1.0.jar",
"sha1": "ff255415e5c4b2a18970da0a8e552b557ca013ae",
"size": 12964773
}
}
}
},
"windows-x86": {
@@ -17,6 +17,10 @@
*/
package org.jackhuang.hmcl;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public final class JavaFXLauncher {
private JavaFXLauncher() {
@@ -27,9 +31,21 @@ public final class JavaFXLauncher {
static {
// init JavaFX Toolkit
try {
javafx.application.Platform.startup(() -> {
// Java 9 or Latter
final MethodHandle startup =
MethodHandles.publicLookup().findStatic(
javafx.application.Platform.class, "startup", MethodType.methodType(void.class, Runnable.class));
startup.invokeExact((Runnable) () -> {
});
started = true;
} catch (NoSuchMethodException e) {
// Java 8
try {
Class.forName("javafx.embed.swing.JFXPanel").getDeclaredConstructor().newInstance();
started = true;
} catch (Throwable e0) {
e0.printStackTrace();
}
} catch (IllegalStateException e) {
started = true;
} catch (Throwable e) {
@@ -23,6 +23,7 @@ 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.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.Platform;
@@ -30,7 +31,6 @@ import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
@@ -45,7 +45,7 @@ public class GameCrashWindowTest {
ManagedProcess process = new ManagedProcess(null, Arrays.asList("commands", "2"));
String logs = Files.readString(new File("../HMCLCore/src/test/resources/logs/too_old_java.txt").toPath());
String logs = FileUtils.readText(new File("../HMCLCore/src/test/resources/logs/too_old_java.txt"));
CountDownLatch latch = new CountDownLatch(1);
FXUtils.runInFX(() -> {
-7
View File
@@ -1,7 +0,0 @@
plugins {
id("java-library")
}
tasks.withType<JavaCompile> {
options.release.set(8)
}
@@ -1,86 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.SwingUtils;
/**
* @author Glavo
*/
public final class Main {
private static final int MINIMUM_JAVA_VERSION = 11;
private Main() {
}
static int findFirstNotNumber(String str, int start) {
if (start >= str.length())
return -1;
char ch = str.charAt(start);
if (ch < '0' || ch > '9')
return -1;
for (int i = start + 1; i < str.length(); i++) {
ch = str.charAt(i);
if (ch < '0' || ch > '9')
return i;
}
return str.length();
}
static int getJavaFeatureVersion(String javaVersion) {
if (javaVersion == null)
return -1;
try {
int end = findFirstNotNumber(javaVersion, 0);
if (end < 0)
return -1; // No valid version number found
int major = Integer.parseInt(javaVersion.substring(0, end));
if (major > 1)
return major;
if (major < 1)
return -1; // Invalid major version
// Java 1.x versions
int start = end + 1;
end = findFirstNotNumber(javaVersion, start);
if (end < 0)
return -1; // No valid minor version found
return Integer.parseInt(javaVersion.substring(start, end));
} catch (NumberFormatException e) {
return -1; // version number is too long
}
}
public static void main(String[] args) throws Throwable {
if (getJavaFeatureVersion(System.getProperty("java.version")) >= MINIMUM_JAVA_VERSION) {
EntryPoint.main(args);
} else {
String errorMessage = BootProperties.getResourceBundle().getString("boot.unsupported_java_version");
System.err.println(errorMessage);
SwingUtils.showErrorDialog(errorMessage);
System.exit(1);
}
}
}
@@ -1,19 +0,0 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
#
boot.unsupported_java_version=HMCL requires Java 11 or later to run, but still supports launching games with Java 8. Please install the latest version of Java and try opening HMCL again.\nYou can keep your old version of Java. HMCL can detect and manage multiple Java installations, and will automatically select the appropriate Java version for your game.
@@ -1,19 +0,0 @@
#
# Hello Minecraft! Launcher
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
#
boot.unsupported_java_version=HMCL 需要 Java 11 或更高版本才能运行,但依然支持使用 Java 8 启动游戏。请安装最新版本的 Java 再尝试启动 HMCL。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
@@ -1,48 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public final class MainTest {
@Test
public void testGetJavaFeatureVersion() {
assertEquals(6, Main.getJavaFeatureVersion("1.6.0"));
assertEquals(6, Main.getJavaFeatureVersion("1.6.0_45"));
assertEquals(7, Main.getJavaFeatureVersion("1.7.0"));
assertEquals(7, Main.getJavaFeatureVersion("1.7.0_80"));
assertEquals(8, Main.getJavaFeatureVersion("1.8"));
assertEquals(8, Main.getJavaFeatureVersion("1.8u321"));
assertEquals(8, Main.getJavaFeatureVersion("1.8.0_321"));
assertEquals(11, Main.getJavaFeatureVersion("11"));
assertEquals(11, Main.getJavaFeatureVersion("11.0.26"));
assertEquals(21, Main.getJavaFeatureVersion("21"));
assertEquals(26, Main.getJavaFeatureVersion("26-ea"));
assertEquals(-1, Main.getJavaFeatureVersion(null));
assertEquals(-1, Main.getJavaFeatureVersion(""));
assertEquals(-1, Main.getJavaFeatureVersion("0"));
assertEquals(-1, Main.getJavaFeatureVersion("0.8"));
assertEquals(-1, Main.getJavaFeatureVersion("abc"));
assertEquals(-1, Main.getJavaFeatureVersion("1.abc"));
assertEquals(-1, Main.getJavaFeatureVersion(".1"));
assertEquals(-1, Main.getJavaFeatureVersion("1111111111111111111111"));
}
}
-9
View File
@@ -2,15 +2,6 @@ plugins {
`java-library`
}
tasks.withType<JavaCompile> {
sourceCompatibility = "11"
targetCompatibility = "11"
}
tasks.compileJava {
options.compilerArgs.add("--add-exports=jdk.attach/sun.tools.attach=ALL-UNNAMED")
}
dependencies {
api(libs.kala.compress.zip)
api(libs.kala.compress.tar)
@@ -25,7 +25,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.io.HttpRequest;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
@@ -96,7 +96,7 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
}
try {
new FileDownloadTask(downloadProvider.get().injectURLWithCandidates(latest.downloadUrl), artifactLocation,
new FileDownloadTask(downloadProvider.get().injectURLWithCandidates(latest.downloadUrl), artifactLocation.toFile(),
Optional.ofNullable(latest.checksums.get("sha256"))
.map(checksum -> new IntegrityCheck("SHA-256", checksum))
.orElse(null))
@@ -110,9 +110,9 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
private AuthlibInjectorVersionInfo getLatestArtifactInfo() throws IOException {
IOException exception = null;
for (URI url : downloadProvider.get().injectURLWithCandidates(LATEST_BUILD_URL)) {
for (URL url : downloadProvider.get().injectURLWithCandidates(LATEST_BUILD_URL)) {
try {
return HttpRequest.GET(url.toString()).getJson(AuthlibInjectorVersionInfo.class);
return HttpRequest.GET(url.toExternalForm()).getJson(AuthlibInjectorVersionInfo.class);
} catch (IOException | JsonParseException e) {
if (exception == null) {
exception = new IOException("Failed to fetch authlib-injector artifact info");
@@ -17,11 +17,13 @@
*/
package org.jackhuang.hmcl.auth.authlibinjector;
import static org.jackhuang.hmcl.util.io.NetworkUtils.toURL;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilProvider;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.net.URI;
import java.net.URL;
import java.util.UUID;
public class AuthlibInjectorProvider implements YggdrasilProvider {
@@ -33,33 +35,33 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
}
@Override
public URI getAuthenticationURL() throws AuthenticationException {
return URI.create(apiRoot + "authserver/authenticate");
public URL getAuthenticationURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/authenticate");
}
@Override
public URI getRefreshmentURL() throws AuthenticationException {
return URI.create(apiRoot + "authserver/refresh");
public URL getRefreshmentURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/refresh");
}
@Override
public URI getValidationURL() throws AuthenticationException {
return URI.create(apiRoot + "authserver/validate");
public URL getValidationURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/validate");
}
@Override
public URI getInvalidationURL() throws AuthenticationException {
return URI.create(apiRoot + "authserver/invalidate");
public URL getInvalidationURL() throws AuthenticationException {
return toURL(apiRoot + "authserver/invalidate");
}
@Override
public URI getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
return URI.create(apiRoot + "api/user/profile/" + UUIDTypeAdapter.fromUUID(uuid) + "/skin");
public URL getSkinUploadURL(UUID uuid) throws UnsupportedOperationException {
return toURL(apiRoot + "api/user/profile/" + UUIDTypeAdapter.fromUUID(uuid) + "/skin");
}
@Override
public URI getProfilePropertiesURL(UUID uuid) throws AuthenticationException {
return URI.create(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
public URL getProfilePropertiesURL(UUID uuid) throws AuthenticationException {
return toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
}
@Override
Loaded 100 of 227 files, more files were not shown because too many files have changed in this diff. Show more