Compare commits

..
Author SHA1 Message Date
Glavo b4ac42d789 Add AGENTS.md (#5996) 2026-04-25 12:56:45 +08:00
Glavo 32aafb11a3 增强对 x86-64 转译支持状态的检测 (#5993) 2026-04-24 21:25:08 +08:00
Glavo 36dab70b7a 修复 CurseForge 源中内容不显示 Minecraft 26.x 版本的问题 (#5994) 2026-04-24 20:44:06 +08:00
Glavo ebf253b1a7 当鼠标未悬停在滚动条上时使滚动条半透明 (#5991) 2026-04-23 21:35:27 +08:00
辞庐 539dcca5ca 优化工具栏“全选”功能 (#5867) 2026-04-23 21:24:47 +08:00
neveler 659b6c2a70 优化文档中的版本声明周期示例 (#5920) 2026-04-22 22:08:39 +08:00
Glavo 3dcc7ab311 支持通过环境变量设置 HMCL 配置文件夹 (#5987) 2026-04-22 21:21:46 +08:00
辞庐 e0ccb58272 fix: 更新时不保存窗口位置 (#5819) 2026-04-22 21:12:10 +08:00
XirrenandGlavo d4ec8ebec8 对于jar路径,将感叹号视为非法字符 (#5976)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-22 21:05:20 +08:00
ENC_Euphony f3b21e5099 fix: place datapack in resource packs folder (#5988)
fix #5970
2026-04-22 20:49:05 +08:00
ENC_Euphony 202fd38180 fix: close Files.list stream when reading world archive root (#5986) 2026-04-21 20:36:19 +08:00
Damon LuandGlavo f5768f2f95 在 macOS 下随明暗模式设置 NSAppearance (#5778)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-20 23:21:36 +08:00
3gf8jv4dv 2d377d06de fix(l10n): revise Version List options on Settings > Downloads page (#5974) 2026-04-20 22:13:24 +08:00
辞庐 42d1581ba0 fix: 可输入的 combobox 内容没有与不可输入的对齐 (#5635) 2026-04-20 22:11:56 +08:00
3gf8jv4dv 995a23c2bc fix: restore tip when mod loader version list is empty (#5978) 2026-04-20 22:08:31 +08:00
Calboot 8969f55a1f [Bugfix] 使用 tomlj 代替 toml4j 以适配 TOML 标准 (#5863)
Resolves #5544
2026-04-20 21:35:47 +08:00
neveler 312a285792 HTMLRenderer 支持嵌套列表 (#5912) 2026-04-20 21:31:09 +08:00
Glavo d85b052195 更新 .gitignore 文件以忽略 codex 相关文件 (#5979) 2026-04-20 21:24:07 +08:00
GlavoandCopilot cd0d3d1b97 修复添加默认 JVM 参数时可能覆盖通过外部文件传递的参数的问题 (#5966)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-20 20:58:13 +08:00
Glavo 6ccc7df998 Fix #5862: 修复 CurseForge Addon Logo 中不包含 thumbnailUrl 时无法加载图标的问题 (#5971) 2026-04-19 22:20:04 +08:00
Glavo 73e5909a69 使用 jwebp 代替 TwelveMonkeys 加载 WebP 图像 (#5968) 2026-04-19 21:06:23 +08:00
辞庐 ade487277d 优化滚动条样式 (#5784) 2026-04-18 22:00:21 +08:00
3gf8jv4dv e755d17977 fix(l10n): revise Download Source options on Settings > Downloads page (#5644) 2026-04-18 21:57:37 +08:00
Damon Lu 292288de9e 改善启动脚本导出 (#5906)
Resolve #5200
2026-04-18 21:45:52 +08:00
Xirren 57f6dec462 整合包完整性检查忽略已升级模组 (#5918)
close #4812
2026-04-18 21:44:34 +08:00
ENC_EuphonyandGlavo 8e1a549c9d feat: modrinth loader tag aggregate display (#5964)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-18 21:38:20 +08:00
辞庐andgemini-code-assist[bot] 1d18eae377 fix: 下载页面部分模组 subtitle 换行 (#5637)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-17 22:20:08 +08:00
辞庐 6ef04d2fa5 对禁止取消的操作禁用取消按钮 (#5948) 2026-04-17 22:12:54 +08:00
ENC_Euphony 1778e9d916 fix: select logs outside the screen was not deselected correctly (#5955)
Fix #5884
2026-04-17 22:08:23 +08:00
Glavo dbb63e9faa 移除 ComponentSublist 内部元素的缩进 (#5956) 2026-04-17 21:24:15 +08:00
Glavo 6b1c3182eb Bump version root to 3.14 (#5961) 2026-04-17 10:42:57 +08:00
辞庐 82460ebbcb fix: 下载页面标签显示未进行 shouldDisplayCategory 判断 (#5946) 2026-04-12 21:50:40 +08:00
辞庐 3e8a9bf1c4 清理 DecoratorSkin 冗余 separator (#5947) 2026-04-12 21:50:26 +08:00
辞庐andGlavo a2db925e86 优化 MenuSeparator 颜色 (#5944)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-12 21:44:42 +08:00
辞庐 fe70cc3daf fix: 模组名字过长导致下载页面布局异常 (#5943) 2026-04-12 21:40:05 +08:00
gwdsirandGlavo 5991112cd8 .gitignore 增加 /.gemini/ (#5927)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-12 21:37:19 +08:00
辞庐 084d91c62a 增大自动安装界面加载器名称版本号间距 (#5945) 2026-04-12 21:03:01 +08:00
辞庐 4963f7a5f0 上传微软账户皮肤时校验大小 (#5949) 2026-04-12 20:57:03 +08:00
Glavo f42d5219c2 支持更多 Vulkan 渲染器 (#5940) 2026-04-12 20:41:39 +08:00
Glavo 101a9b5d5e 禁止在 LogWindow 中显示横向滚动条 (#5935) 2026-04-11 00:43:35 +08:00
Glavo 7cd79a4e2a Update versions.txt to 26.1.2 (#5934) 2026-04-10 22:49:40 +08:00
Glavoand3gf8jv4dv 30c3acfb23 增强对 Vulkan 的支持 (#5930)
1. 优化渲染器选项描述;
2. 添加 Vulkan 相关的渲染器选项。

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-04-10 22:19:51 +08:00
Glavo 31f04db555 支持将新格式快照版本号翻译为文言 (#5931) 2026-04-09 00:16:57 +08:00
Glavo 7d41f7bdd3 Add support for Minecraft 26.2-snapshot-1+ to unofficial supported platforms (#5928) 2026-04-08 01:48:50 +08:00
辞庐 8e6ac947ed fix: 安装整合包部分任务未正确归类 (#5922) 2026-04-06 20:38:07 +08:00
辞庐 20e321f502 优化原理图管理&世界管理输入校验 (#5923) 2026-04-06 20:36:49 +08:00
辞庐 00b323e507 优化 Java 下载弹窗 Spinner (#5924) 2026-04-06 20:35:43 +08:00
辞庐 333b593c8a 优化添加认证服务器 http 警告颜色 (#5925) 2026-04-06 20:31:31 +08:00
Glavo 665f6df142 Add IDEA copyright configuration files (#5919) 2026-04-06 13:12:07 +08:00
辞庐 9e16130537 fix: Alex 模型皮肤 3D 预览手臂背后有一列侧面的异常像素 (#5688) 2026-04-05 23:16:40 +08:00
Glavo 5c98d6734d 修复多次打开微软登录弹窗后报错的问题 (#5911) 2026-04-05 23:13:25 +08:00
辞庐 c3801a3445 优化下载页面依赖项展示 (#5872) 2026-04-05 23:10:53 +08:00
辞庐andGlavo 1d137a3a88 优化 ComboBox 展开图标 (#5917)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-04-05 22:57:37 +08:00
辞庐 2e8b9ae3ca fix: 长按左侧关闭返回按钮可以拖动窗口 (#5915) 2026-04-05 22:57:00 +08:00
Calboot fb750e8717 [Bugfix] 修复导入整合包界面箭头颜色错误的问题 (#5914)
Fixes #5913
2026-04-05 22:15:11 +08:00
辞庐 f9ae0902d8 fix: 安装模组未进行文件名存在检查 (#5879) 2026-04-05 20:33:08 +08:00
GlavoandBurning_TNT 8d69cbc02f Fix #5907: 修复无法为 MC 26.1.1 从官方源安装 NeoForge 的问题 (#5910)
Co-authored-by: Burning_TNT <pangyl08@163.com>
2026-04-04 22:03:54 +08:00
辞庐 90c2372c92 fix: JFXRippler 可能无法覆盖整个控件 (#5609) 2026-04-04 21:44:21 +08:00
Glavo 84c4943de5 搜索 Java 时缓存 Java 信息 (#4716) 2026-04-04 21:41:20 +08:00
3gf8jv4dv 057943b6c0 fix(i18n): clarify terms for specific world detail pages (#5486) 2026-04-04 21:22:41 +08:00
Calboot e943cb0aec [Bugfix] 修复 HMCL 在 1.20.1 neoforge 环境下将部分 forge 模组识别为 MixinExtras 的问题 (#5888)
Fixes #5329
2026-04-04 20:57:09 +08:00
Wulian233 70290e205b 优化 JFXComboBox 样式 添加 hover 背景与手型指针 (#5905) 2026-04-04 20:15:53 +08:00
Glavo a73b8c7172 Add Gemini Code Assist configuration file (#5903) 2026-04-03 23:19:08 +08:00
3gf8jv4dv a5942c8e91 chore: add LWJGL Unsafe Agent to Settings > About page (#5896) 2026-04-03 21:28:42 +08:00
Glavo dc6a97520b fix: ensure main class is not null for game instance (#5902) 2026-04-03 21:27:54 +08:00
8be3f04cc8 feat: 拖拽版本 json 安装实例 (#5730)
resolves #5459

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-03 21:23:14 +08:00
Calboot 8d85b1537d [Bugfix] 修复 HMCL 下载游戏内容时出现空游戏版本条目的问题 (#5899)
Fixes #5894
2026-04-03 20:58:24 +08:00
3gf8jv4dv b16f2a6ce6 chore: update HelloNBT's link on Settings > About page (#5895) 2026-04-03 20:45:39 +08:00
Glavo e19f175e29 优化 Java 下载 (#5859)
1. 清理代码;
2. 先将文件下载至临时文件夹,下载完毕后再移动至目标目录。
2026-04-03 20:44:10 +08:00
Glavo f3a5b2a99d 支持 26w14a (#5892) 2026-04-02 00:02:08 +08:00
neveler eac2740ea8 优化关于页组件间距使整体风格更加统一协调 (#5861) 2026-03-30 21:17:27 +08:00
a06da53ec9 修复清理游戏资源 / 库操作在 FXThread 运行的问题 (#5301)
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-30 21:04:08 +08:00
Xirren 765ea24d22 优化手动更新URL (#5880) 2026-03-30 20:40:50 +08:00
Glavo bd24ab37f5 Bump XZ for Java to 1.12 (#5883) 2026-03-30 20:40:32 +08:00
Glavo c689272c42 Bump lwjgl-unsafe-agent to 2.0 (#5882) 2026-03-30 20:27:11 +08:00
952cc5dd37 [Feature] 允许设置游戏内容默认下载源 (#5781)
Resolves #5594

Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-03-29 22:07:20 +08:00
辞庐andGlavo 356df5d630 更新 amdgpu.ids 文件 (#5316)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-29 21:58:43 +08:00
Damon Lu 0e433c5f4b 启动时检测 Wine 环境并进行警告 (#5518) 2026-03-29 21:56:44 +08:00
Glavo a28509de85 支持加载 SVG 图片 (#5484) 2026-03-29 21:29:25 +08:00
3gf8jv4dv e45601ecda chore: unify version type and release channel in english locale (#5548) 2026-03-29 21:02:54 +08:00
辞庐andGlavo 5bbf5cbfc8 启动时对会丢失游戏数据的情况添加 5 秒确认 (#5738)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-29 21:00:44 +08:00
afb98c20ce 优化 update.note.dev 英语换行 (#5876)
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-29 20:52:31 +08:00
辞庐 6b2bc4606b fix: JFXDialog 弹窗过程中模糊 (#5877)
resolves #5073
2026-03-29 20:43:58 +08:00
mineDiamondandGlavo 6b57cec576 feat: 为自定义颜色选择对话框添加显式的确认和取消按钮 (#5775)
close #5767

Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-29 20:43:26 +08:00
Glavo 83111b83f2 更新 launch.advice.lwjgl_3_4_1 文言文译文 (#5874) 2026-03-29 02:44:29 +08:00
Glavo 420e337c8c Update LWJGL advice message (#5873) 2026-03-29 02:18:20 +08:00
Glavoand3gf8jv4dv 028e43c959 通过 Java Agent 优化 LWJGL 3.4.1 性能 (#5870)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-03-29 01:35:07 +08:00
ef01acca6d feat: 下载页面折叠快照 Minecraft 版本 (#5601)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-27 22:58:49 +08:00
110d5f5630 更新更新渠道选择选项 (#5703)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-27 21:41:54 +08:00
neveler 94497a073d 种子地图、要塞地图、下届要塞地图和末地城地图支持 26.1 (#5860) 2026-03-27 21:06:36 +08:00
mineDiamond ff540a95ee refactor: 将 Datapack 重命名为 DataPack (#5785) 2026-03-26 21:26:18 +08:00
辞庐 ff6a32cdc3 fix: SettingsMap 缺少 toString 方法 (#5857) 2026-03-26 21:19:06 +08:00
Glavo 969644635a Bump HelloNBT to 0.3.0 (#5855) 2026-03-25 18:12:40 +08:00
Glavo a053736e95 Bump twelvemonkeys to 3.13.1 (#5854) 2026-03-25 18:07:58 +08:00
Glavo 879f82c462 Bump kala-compress to 1.27.1-3 (#5851) 2026-03-25 17:45:09 +08:00
Glavo e2d8d9f93f 更新文言文翻译 (#5845) 2026-03-24 21:39:53 +08:00
Glavo da4ed04697 Update default Java version to JAVA_25 (#5841) 2026-03-24 20:35:02 +08:00
Glavo 5d44df6b40 Fix #5822: 修复 JFXTextField 导致 GPU 占用率异常高的问题 (#5839) 2026-03-24 00:11:20 +08:00
辞庐andGlavo 114503e0bf 为游戏崩溃界面导出和启动器日志导出添加加载指示器 (#5694)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-23 22:03:16 +08:00
Calboot 8bd88aa8d8 Inject candidates for CF pack icon download (#5837) 2026-03-23 21:55:51 +08:00
Glavo cc7d1e1aae Add Minecraft 26.1 snapshot/pre/rc versions (#5833) 2026-03-23 21:54:14 +08:00
Damon Lu 56d4effda0 将“不适用于当前系统的选项”的系统支持信息移到 subtitle (#5813) 2026-03-23 21:46:13 +08:00
neveler 7c2dbe3dae 简化 JFXDialog 圆角 (#5836) 2026-03-23 21:45:23 +08:00
Calboot 99469077d2 [Enhancement] CurseForge整合包也下载图标 & 防止图标重复 (#5786) 2026-03-23 21:44:09 +08:00
neveler ec930843da JFXCustomColorPickerDialog 忽略 Alpha 通道 (#5830)
resolve #5824
2026-03-23 21:18:07 +08:00
neveler 969d314802 为 dialog 添加圆角 (#5832) 2026-03-23 21:10:54 +08:00
Glavo 3b11541b0a 更新内存状态栏 (#5829) 2026-03-23 20:33:57 +08:00
Glavo c58d5a3a81 Extract clamp methods to MathUtils (#5826) 2026-03-22 21:27:56 +08:00
Glavo cf9b4aacb2 优化 checkstyle 在缺失版权声明时的报错 (#5825) 2026-03-22 21:22:55 +08:00
Glavo e430db0cd6 使用 HelloNBT 替代 OpenNBT (#5667) 2026-03-21 22:57:52 +08:00
辞庐andGlavo 1b31e96629 feat(LogWindow): 在游戏进程结束后禁用“结束游戏进程”与“导出游戏运行栈”按钮 (#5660)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-21 21:56:51 +08:00
辞庐 5c19239ddd 优化 tag 展示逻辑 (#5702)
resolves #5677
2026-03-21 21:38:34 +08:00
Glavo c181c7825b 当 CurseForge API Key 缺失时隐藏下载源切换选项 (#5802) 2026-03-21 21:31:11 +08:00
辞庐andGlavo 80d2fefa1f 使用 checkstyle 检查文件版权声明 (#5687)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-21 21:14:05 +08:00
Glavo 8701030934 Bump version root to 3.13 (#5811) 2026-03-20 22:18:57 +08:00
183 changed files with 6034 additions and 2222 deletions

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
code_review:
pull_request_opened:
code_review: false
+7 -1
View File
@@ -64,4 +64,10 @@ language-subtag-registry
# test
/hmcl.json
/.hmcl.json
/.hmcl/
/.hmcl/
# AI
/.gemini/
!/.gemini/config.yaml
.codex
/.gradle-*/
+6
View File
@@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Hello Minecraft! Launcher&#10;Copyright (C) &amp;#36;today.year huangyuhui &lt;huanghongxun2008@126.com&gt; and contributors&#10;&#10;This program is free software: you can redistribute it and/or modify&#10;it under the terms of the GNU General Public License as published by&#10;the Free Software Foundation, either version 3 of the License, or&#10;(at your option) any later version.&#10;&#10;This program is distributed in the hope that it will be useful,&#10;but WITHOUT ANY WARRANTY; without even the implied warranty of&#10;MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&#10;GNU General Public License for more details.&#10;&#10;You should have received a copy of the GNU General Public License&#10;along with this program. If not, see &lt;https://www.gnu.org/licenses/&gt;." />
<option name="myName" value="HMCL" />
</copyright>
</component>
+8
View File
@@ -0,0 +1,8 @@
<component name="CopyrightManager">
<settings default="HMCL">
<LanguageOptions name="JAVA">
<option name="fileTypeOverride" value="3" />
<option name="addBlankAfter" value="false" />
</LanguageOptions>
</settings>
</component>
+27
View File
@@ -0,0 +1,27 @@
# Java Code Style Requirements
These rules apply to all Java code written or modified in this repository.
## Nullability
- Annotate every class with JetBrains Annotations `@NotNullByDefault`.
- Any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
- Nullability must never be implicit.
## Immutability
- Immutable arrays and collections must be explicitly annotated with JetBrains Annotations `@Unmodifiable` or `@UnmodifiableView` as appropriate.
- For arrays, use type-use syntax such as `String @Unmodifiable []`.
## Documentation
- Every class, field, and method must have documentation.
- Documentation must use `///` Markdown-style Javadoc comments.
- Keep documentation accurate and specific to the actual behavior, constraints, and side effects.
- Add concise implementation comments inside complex logic whenever they materially improve readability or explain non-obvious behavior.
## Gradle
- When invoking Gradle in this repository, always set `GRADLE_USER_HOME` to the workspace-local `.gradle-user-home` directory.
- Prefer commands such as `./gradlew -g .gradle-user-home ...` or the equivalent environment-variable-based configuration.
- When running Gradle `test` tasks, use a higher timeout of ten minutes.
+5 -2
View File
@@ -56,7 +56,8 @@ dependencies {
implementation(project(":HMCLCore"))
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.jwebp)
implementation(libs.fxsvgimage)
implementation(libs.java.info)
implementation(libs.monet.fx)
implementation(libs.nayuki.qrcodegen)
@@ -66,6 +67,7 @@ dependencies {
}
embedResources(libs.authlib.injector)
embedResources(libs.lwjgl.unsafe.agent)
}
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
@@ -155,6 +157,7 @@ val hmclProperties = buildList {
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
add("hmcl.lwjgl-unsafe-agent.version" to libs.lwjgl.unsafe.agent.get().version!!)
}
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
@@ -192,7 +195,7 @@ tasks.shadowJar {
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
listOf(
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*",
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
).forEach { exclude("com/sun/jna/$it/**") }
@@ -23,21 +23,13 @@ import com.jfoenix.controls.events.JFXDialogEvent;
import com.jfoenix.converters.DialogTransitionConverter;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.animation.*;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.css.*;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
@@ -49,7 +41,6 @@ import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
@@ -155,7 +146,6 @@ public class JFXDialog extends StackPane {
});
contentHolder = new StackPane();
contentHolder.setBackground(new Background(new BackgroundFill(Color.WHITE, new CornerRadii(2), null)));
JFXDepthManager.setDepth(contentHolder, 4);
contentHolder.setPickOnBounds(false);
// ensure stackpane is never resized beyond it's preferred size
@@ -381,6 +371,14 @@ public class JFXDialog extends StackPane {
setCycleDuration(Duration.seconds(0.4));
setDelay(Duration.ZERO);
}
@Override
protected void starting() {
}
@Override
protected void stopping() {
}
}
/***************************************************************************
@@ -231,10 +231,11 @@ public class JFXRippler extends StackPane {
*
* @return the ripple radius size
*/
protected double computeRippleRadius() {
double width2 = control.getLayoutBounds().getWidth() * control.getLayoutBounds().getWidth();
double height2 = control.getLayoutBounds().getHeight() * control.getLayoutBounds().getHeight();
return Math.min(Math.sqrt(width2 + height2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
protected double computeRippleRadius(double centerX, double centerY) {
double dx = Math.max(centerX, control.getLayoutBounds().getWidth() - centerX);
double dy = Math.max(centerY, control.getLayoutBounds().getHeight() - centerY);
return (Math.sqrt(dx * dx + dy * dy) / 0.9) + 2;
}
protected void setOverLayBounds(Rectangle overlay) {
@@ -473,7 +474,7 @@ public class JFXRippler extends StackPane {
super(centerX,
centerY,
getRipplerRadius() == Region.USE_COMPUTED_SIZE ?
computeRippleRadius() : getRipplerRadius(), null);
computeRippleRadius(centerX, centerY) : getRipplerRadius(), null);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
@@ -518,7 +519,7 @@ public class JFXRippler extends StackPane {
new KeyValue(opacityProperty(),
1,
RIPPLE_INTERPOLATOR)
), new KeyFrame(Duration.millis(900), inKeyValues));
), new KeyFrame(Duration.millis(600), inKeyValues));
setScaleX(0);
setScaleY(0);
@@ -0,0 +1,277 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.adapters.skins.ComboBoxListViewSkin;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
protected final ObjectProperty<Paint> promptTextFill;
private boolean invalid = true;
private final StackPane customPane;
private final StackPane line = new StackPane();
private final StackPane focusedLine = new StackPane();
private final Text promptText = new Text();
private final double initScale = 0.05;
private final Scale scale;
private final Timeline linesAnimation;
private ParallelTransition transition;
private CachedTransition promptTextUpTransition;
private CachedTransition promptTextDownTransition;
private CachedTransition promptTextColorTransition;
private final Scale promptTextScale;
private Paint oldPromptTextFill;
private final BooleanBinding usePromptText;
public JFXComboBoxListViewSkin(JFXComboBox<T> comboBox) {
super(comboBox);
this.scale = new Scale(this.initScale, 1.0F);
this.linesAnimation = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.scale.xProperty(), this.initScale, Interpolator.EASE_BOTH), new KeyValue(this.focusedLine.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1.0F), new KeyValue(this.focusedLine.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(160.0F), new KeyValue(this.scale.xProperty(), 1, Interpolator.EASE_BOTH)));
this.promptTextScale = new Scale(1.0F, 1.0F, 0.0F, 0.0F);
this.promptTextFill = new SimpleObjectProperty(Color.valueOf("#B2B2B2"));
this.usePromptText = Bindings.createBooleanBinding(this::usePromptText, ((JFXComboBox) this.getSkinnable()).valueProperty(), this.getSkinnable().promptTextProperty());
this.getArrowButton().setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
this.promptText.textProperty().bind(comboBox.promptTextProperty());
this.promptText.fillProperty().bind(this.promptTextFill);
this.promptText.getStyleClass().addAll("text", "prompt-text");
this.promptText.getTransforms().add(this.promptTextScale);
if (!comboBox.isLabelFloat()) {
this.promptText.visibleProperty().bind(this.usePromptText);
}
this.customPane = new StackPane();
this.customPane.setMouseTransparent(true);
this.customPane.getStyleClass().add("combo-box-button-container");
this.customPane.backgroundProperty().bindBidirectional(this.getSkinnable().backgroundProperty());
this.customPane.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
this.customPane.getChildren().add(this.promptText);
this.getChildren().add(0, this.customPane);
StackPane.setAlignment(this.promptText, Pos.CENTER_LEFT);
this.line.getStyleClass().add("input-line");
this.focusedLine.getStyleClass().add("input-focused-line");
this.getChildren().add(this.line);
this.getChildren().add(this.focusedLine);
this.line.setPrefHeight(1.0F);
this.line.setTranslateY(1.0F);
this.line.setManaged(false);
this.line.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
if (this.getSkinnable().isDisabled()) {
this.line.setBorder(new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(1.0F))));
this.line.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
}
this.focusedLine.setPrefHeight(2.0F);
this.focusedLine.setTranslateY(0.0F);
this.focusedLine.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
this.focusedLine.setOpacity(0.0F);
this.focusedLine.getTransforms().add(this.scale);
this.focusedLine.setManaged(false);
if (comboBox.isEditable()) {
comboBox.getEditor().setStyle("-fx-background-color:TRANSPARENT;-fx-padding: 4 8 4 8");
comboBox.getEditor().promptTextProperty().unbind();
comboBox.getEditor().setPromptText(null);
comboBox.getEditor().textProperty().addListener((o, oldVal, newVal) -> {
this.usePromptText.invalidate();
comboBox.setValue(this.getConverter().fromString(newVal));
});
}
comboBox.labelFloatProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
this.promptText.visibleProperty().unbind();
JFXUtilities.runInFX(() -> this.createFloatingAnimation());
} else {
this.promptText.visibleProperty().bind(this.usePromptText);
}
this.createFocusTransition();
});
comboBox.focusColorProperty().addListener((o, oldVal, newVal) -> {
if (newVal != null) {
this.focusedLine.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, newVal, Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(160.0F));
}
protected void starting() {
super.starting();
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
}
};
this.transition = null;
}
}
});
comboBox.unFocusColorProperty().addListener((o, oldVal, newVal) -> {
if (newVal != null) {
this.line.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
}
});
comboBox.disabledProperty().addListener((o, oldVal, newVal) -> {
this.line.setBorder(newVal ? new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(this.line.getHeight()))) : Border.EMPTY);
this.line.setBackground(new Background(new BackgroundFill(newVal ? Color.TRANSPARENT : ((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
});
comboBox.focusedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
this.focus();
} else {
this.unFocus();
}
});
comboBox.valueProperty().addListener((o, oldVal, newVal) -> {
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
this.animateFloatingLabel(newVal != null && !newVal.toString().isEmpty());
}
});
}
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
this.customPane.resizeRelocate(x, y, w, h);
if (this.invalid) {
this.invalid = false;
if (!this.getSkinnable().isEditable()) {
Text javaPromptText = (Text) super.getDisplayNode().lookup(".text");
if (javaPromptText != null) {
this.promptTextFill.set(javaPromptText.getFill());
}
}
this.createFloatingAnimation();
if (((ComboBoxBase) this.getSkinnable()).getValue() != null) {
this.animateFloatingLabel(true);
}
}
this.focusedLine.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.focusedLine.prefHeight(-1.0F));
this.line.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.line.prefHeight(-1.0F));
this.scale.setPivotX(w / (double) 2.0F);
}
private void createFloatingAnimation() {
this.promptTextUpTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), -this.customPane.getHeight() + 6.05, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 0.85, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 0.85, Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(240.0F));
}
};
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, ((JFXComboBox) this.getSkinnable()).getFocusColor(), Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(160.0F));
}
protected void starting() {
super.starting();
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
}
};
this.promptTextDownTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 1, Interpolator.EASE_BOTH)))) {
{
this.setDelay(Duration.millis(0.0F));
this.setCycleDuration(Duration.millis(240.0F));
}
};
this.promptTextDownTransition.setOnFinished((finish) -> {
this.promptText.setTranslateY(0.0F);
this.promptTextScale.setX(1.0F);
this.promptTextScale.setY(1.0F);
});
}
private void focus() {
if (this.transition == null) {
this.createFocusTransition();
}
this.transition.play();
}
private void animateFloatingLabel(boolean up) {
if (this.promptText == null) {
Platform.runLater(() -> this.animateFloatingLabel(up));
} else {
if (this.transition != null) {
this.transition.stop();
this.transition.getChildren().remove(this.promptTextUpTransition);
this.transition.getChildren().remove(this.promptTextColorTransition);
this.transition = null;
}
if (up && this.promptText.getTranslateY() == (double) 0.0F) {
this.promptTextDownTransition.stop();
this.promptTextUpTransition.play();
if (this.getSkinnable().isFocused()) {
this.promptTextColorTransition.play();
}
} else if (!up) {
this.promptTextUpTransition.stop();
if (this.getSkinnable().isFocused()) {
this.promptTextFill.set(this.oldPromptTextFill);
}
this.promptTextDownTransition.play();
}
}
}
private void createFocusTransition() {
this.transition = new ParallelTransition();
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
this.transition.getChildren().add(this.promptTextUpTransition);
this.transition.getChildren().add(this.promptTextColorTransition);
}
this.transition.getChildren().add(this.linesAnimation);
}
private void unFocus() {
if (this.transition != null) {
this.transition.stop();
}
this.scale.setX(this.initScale);
this.focusedLine.setOpacity(0.0F);
if (((JFXComboBox) this.getSkinnable()).isLabelFloat() && this.oldPromptTextFill != null) {
this.promptTextFill.set(this.oldPromptTextFill);
if (this.usePromptText()) {
this.promptTextDownTransition.play();
}
}
}
private boolean usePromptText() {
Object txt = ((JFXComboBox) this.getSkinnable()).getValue();
String promptTxt = this.getSkinnable().getPromptText();
return (txt == null || txt.toString().isEmpty()) && promptTxt != null && !promptTxt.isEmpty() && !this.promptTextFill.get().equals(Color.TRANSPARENT);
}
}
@@ -40,12 +40,16 @@ import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Duration;
import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Shadi Shaheen
*/
@@ -78,7 +82,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
// create JFX Decorator
pickerDecorator = new JFXDecorator(dialog, this, false, false, false);
pickerDecorator.setOnCloseButtonAction(this::updateColor);
pickerDecorator.setOnCloseButtonAction(this::close);
pickerDecorator.setPickOnBounds(false);
customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
StyleSheets.init(customScene);
@@ -153,6 +157,22 @@ public class JFXCustomColorPickerDialog extends StackPane {
paraTransition.play();
});
container.getChildren().add(tabs);
HBox actionsHBox = new HBox();
actionsHBox.getStyleClass().add("jfx-color-dialog-actions");
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.setOnAction(event -> updateColor());
acceptButton.getStyleClass().add("jfx-color-dialog-accept");
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(event -> close());
cancelButton.getStyleClass().add("jfx-color-dialog-cancel");
actionsHBox.getChildren().addAll(acceptButton, cancelButton);
container.getChildren().add(actionsHBox);
initRun = () -> {
// change tabs labels font color according to the selected color
pane.backgroundProperty().addListener((o, oldVal, newVal) -> {
@@ -179,7 +199,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
(int) (newColor.getRed() * 255),
(int) (newColor.getGreen() * 255),
(int) (newColor.getBlue() * 255));
String rgb = String.format("rgba(%d, %d, %d, 1)",
String rgb = String.format("rgb(%d, %d, %d)",
(int) (newColor.getRed() * 255),
(int) (newColor.getGreen() * 255),
(int) (newColor.getBlue() * 255));
@@ -232,7 +252,13 @@ public class JFXCustomColorPickerDialog extends StackPane {
hexField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
return pane.getBackground().getFills().get(0).getFill();
}, pane.backgroundProperty()));
acceptButton.textFillProperty().bind(Bindings.createObjectBinding(() -> {
Color fill = (Color) pane.getBackground().getFills().get(0).getFill();
return ColorScheme.newBuilder(Themes.getColorScheme())
.setPrimaryColorSeed(fill)
.build()
.getPrimary();
}, pane.backgroundProperty()));
((Pane) pickerDecorator.lookup(".jfx-decorator-buttons-container")).backgroundProperty()
.bind(Bindings.createObjectBinding(() -> {
@@ -262,9 +288,6 @@ public class JFXCustomColorPickerDialog extends StackPane {
});
};
container.getChildren().add(tabs);
this.getChildren().add(container);
this.setPadding(new Insets(0));
@@ -294,7 +317,11 @@ public class JFXCustomColorPickerDialog extends StackPane {
if (!systemChange) {
userChange = true;
try {
curvedColorPicker.setColor(Color.valueOf(colorWebString));
Color color = Color.valueOf(colorWebString);
if (color.getOpacity() != 1.0) {
color = Color.color(color.getRed(), color.getGreen(), color.getBlue());
}
curvedColorPicker.setColor(color);
} catch (IllegalArgumentException ignored) {
// if color is not valid then do nothing
}
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.JOptionPane;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -49,6 +50,8 @@ public final class EntryPoint {
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
checkWine();
setupJavaFXVMOptions();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
@@ -220,6 +223,20 @@ public final class EntryPoint {
}
}
private static void checkWine() {
if (OperatingSystem.isRunningUnderWine()) {
SwingUtils.initLookAndFeel();
LOG.warning("HMCL is running under Wine or its distributions!");
int result = JOptionPane.showOptionDialog(null, i18n("fatal.wine_warning"), i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, null, null);
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
exit(1);
}
}
}
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
@@ -17,35 +17,37 @@
*/
package org.jackhuang.hmcl;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableBooleanValue;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
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;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.Lang;
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.CommandBuilder;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.*;
import java.io.File;
import java.io.IOException;
@@ -65,8 +67,8 @@ import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class Launcher extends Application {
public static final CookieManager COOKIE_MANAGER = new CookieManager();
@@ -111,7 +113,7 @@ public final class Launcher extends Application {
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& ConfigHolder.isNewlyCreated()
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
if (!confirmWithCountdown(AlertType.WARNING, i18n("fatal.mac_app_translocation"), 5))
return;
} else {
checkConfigInTempDir();
@@ -137,6 +139,9 @@ public final class Launcher extends Application {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
Themes.applyNativeDarkMode(primaryStage);
UpdateChecker.init();
primaryStage.show();
@@ -182,6 +187,27 @@ public final class Launcher extends Application {
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
}
private static boolean confirmWithCountdown(Alert.AlertType alertType, String contentText, int seconds) {
Alert alert = new Alert(alertType, contentText, ButtonType.YES, ButtonType.NO);
Button okButton = (Button) alert.getDialogPane().lookupButton(ButtonType.YES);
okButton.setDisable(true);
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
for (int i = 0; i < seconds; i++) {
keyFrames[i] = new KeyFrame(Duration.seconds(i),
new KeyValue(okButton.textProperty(), i18n("button.ok.countdown", seconds - i)));
}
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
new KeyValue(okButton.textProperty(), i18n("button.ok")),
new KeyValue(okButton.disableProperty(), false));
Timeline timeline = new Timeline(keyFrames);
alert.setOnShown(e -> timeline.play());
alert.setOnCloseRequest(e -> timeline.stop());
return alert.showAndWait().orElse(null) == ButtonType.YES;
}
private static boolean isConfigInTempDir() {
String configPath = ConfigHolder.configLocation().toString();
@@ -221,7 +247,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) {
&& !confirmWithCountdown(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), 5)) {
EntryPoint.exit(0);
}
}
@@ -24,7 +24,6 @@ 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;
/**
@@ -49,6 +48,7 @@ public final class Metadata {
public static final String ABOUT_URL = PUBLISH_URL + "/about";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
public static final String DOCS_URL = "https://docs.hmcl.net";
public static final String CONTACT_URL = DOCS_URL + "/help.html";
@@ -59,34 +59,38 @@ public final class Metadata {
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path CURRENT_DIRECTORY = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_GLOBAL_DIRECTORY;
public static final Path HMCL_CURRENT_DIRECTORY;
public static final Path DEPENDENCIES_DIRECTORY;
static {
String hmclHome = System.getProperty("hmcl.home");
if (hmclHome == null) {
String hmclHome = System.getProperty("hmcl.home", System.getenv("HMCL_USER_HOME"));
if (StringUtils.isBlank(hmclHome)) {
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
String xdgData = System.getenv("XDG_DATA_HOME");
if (StringUtils.isNotBlank(xdgData)) {
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Path.of(xdgData, "hmcl").toAbsolutePath().normalize();
} else {
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Path.of(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
}
} else {
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
} else {
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Path.of(hmclHome).toAbsolutePath().normalize();
}
String hmclCurrentDir = System.getProperty("hmcl.dir");
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
String hmclCurrentDir = System.getProperty("hmcl.dir", System.getenv("HMCL_LOCAL_HOME"));
HMCL_CURRENT_DIRECTORY = StringUtils.isNotBlank(hmclCurrentDir)
? Path.of(hmclCurrentDir).toAbsolutePath().normalize()
: CURRENT_DIRECTORY.resolve(".hmcl");
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
String hmclDependencies = System.getProperty("hmcl.dependencies.dir", System.getenv("HMCL_DEPENDENCIES_DIR"));
DEPENDENCIES_DIRECTORY = StringUtils.isNotBlank(hmclDependencies)
? Path.of(hmclDependencies).toAbsolutePath().normalize()
: HMCL_CURRENT_DIRECTORY.resolve("dependencies");
}
public static boolean isStable() {
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* 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
* 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.countly;
import org.jackhuang.hmcl.Metadata;
@@ -21,12 +21,16 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.NativePatcher;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;
@@ -150,4 +154,60 @@ public final class HMCLGameLauncher extends DefaultLauncher {
generateOptionsTxt();
super.makeLaunchScript(scriptFile);
}
@Override
protected void appendJvmArgs(CommandBuilder result) {
super.appendJvmArgs(result);
if (config().getAllowAutoAgent()
&& !options.isNoGeneratedJVMArgs()
&& !options.isNoGeneratedOptimizingJVMArgs()
&& NativePatcher.needPatchMemoryUtil(version, options.getJava().getParsedVersion())) {
LOG.info("Attempting to patch game with lwjgl-unsafe-agent");
try {
result.add("-javaagent:" + extractLwjglUnsafeAgent());
} catch (Exception e) {
LOG.warning("Failed to extract lwjgl-unsafe-agent", e);
}
}
}
private Path extractLwjglUnsafeAgent() throws IOException {
String agentVersion = JarUtils.getAttribute("hmcl.lwjgl-unsafe-agent.version", null);
if (agentVersion == null) {
throw new IOException("Missing hmcl.lwjgl-unsafe-agent.version attribute");
}
Library library = new Library(new Artifact("org.glavo", "lwjgl-unsafe-agent", agentVersion));
String fileName = library.getArtifact().getFileName();
Path agentPath = repository.getLibraryFile(version, library).toAbsolutePath().normalize();
if (agentPath.toString().contains("=")) {
throw new IOException("Invalid library path: " + agentPath);
}
byte[] bytes;
try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/" + fileName)) {
if (input == null) {
throw new IOException("/assets/" + fileName + " not found");
}
bytes = input.readAllBytes();
}
if (Files.isRegularFile(agentPath)) {
try {
if (Files.size(agentPath) == bytes.length) {
return agentPath;
}
} catch (IOException e) {
LOG.warning("Failed to check size of " + agentPath, e);
}
}
Files.createDirectories(agentPath.getParent());
FileUtils.saveSafely(agentPath, output -> output.write(bytes));
return agentPath;
}
}
@@ -310,7 +310,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
Optional<Path> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try {
return FXUtils.loadImage(iconFile.get());
return FXUtils.loadImage(iconFile.get(), 64, 64, true, true);
} catch (Exception e) {
LOG.warning("Failed to load version icon of " + id, e);
}
@@ -430,6 +430,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setGraphicsBackend(vs.getGraphicsBackend())
.setRenderer(vs.getRenderer())
.setEnableDebugLogOutput(vs.isEnableDebugLogOutput())
.setUseNativeGLFW(vs.isUseNativeGLFW())
@@ -514,7 +515,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
return false;
return FileUtils.isNameValid(id);
return FileUtils.isNameValidForJar(id);
}
/**
@@ -65,16 +65,18 @@ import java.util.stream.Collectors;
import static javafx.application.Platform.runLater;
import static javafx.application.Platform.setImplicitExit;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
public final class LauncherHelper {
private static final String LWJGL_3_4_1_TIP = "lwjgl3.4.1-ffm";
private final Profile profile;
private Account account;
private final String selectedVersion;
@@ -170,33 +172,55 @@ public final class LauncherHelper {
}
}),
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
return null;
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
return null;
}
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|| renderer.mesaDriverName() == null)
return null;
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
if (lib == null)
return null;
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
return null;
}
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(agent));
} else {
javaAgents.add(agent);
return null;
}
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(agent));
} else {
javaAgents.add(agent);
return null;
}
})
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
.thenComposeAsync(() -> {
if (config().getAllowAutoAgent()
|| setting.isNoJVMArgs()
|| setting.isNoOptimizingJVMArgs()
|| Boolean.TRUE.equals(config().getShownTips().get(LWJGL_3_4_1_TIP))
|| !NativePatcher.needPatchMemoryUtil(version.get(), javaVersionRef.get().getParsedVersion())) {
return Task.completed(null);
} else {
CompletableFuture<Void> future = new CompletableFuture<>();
runInFX(() -> {
Controllers.confirm(i18n("launch.advice.lwjgl_3_4_1"), i18n("launch.advice.lwjgl_3_4_1.title"), MessageType.QUESTION, () -> {
config().getShownTips().put(LWJGL_3_4_1_TIP, true);
config().setAllowAutoAgent(true);
future.complete(null);
}, () -> {
config().getShownTips().put(LWJGL_3_4_1_TIP, true);
future.complete(null);
});
});
return Task.fromCompletableFuture(future);
}
})
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
@@ -207,6 +231,7 @@ public final class LauncherHelper {
if (quickPlayOption != null) {
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
}
LaunchOptions launchOptions = launchOptionsBuilder.create();
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
@@ -599,7 +624,7 @@ public final class LauncherHelper {
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& isCompatibleWithX86Java()) {
&& Platform.isSupportedTranslationX86_64()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
break;
@@ -31,7 +31,6 @@ public final class Log {
private final String log;
private Log4jLevel level;
private boolean selected = false;
public Log(String log) {
this.log = log;
@@ -57,14 +56,6 @@ public final class Log {
return level;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public String toString() {
return log;
@@ -51,7 +51,10 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf;
@@ -167,7 +170,7 @@ public final class ModpackHelper {
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
public static boolean isExternalGameNameConflicts(String name) {
@@ -211,16 +214,16 @@ public final class ModpackHelper {
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
@@ -228,7 +231,7 @@ public final class ModpackHelper {
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
default:
throw new UnsupportedModpackException();
}
@@ -163,6 +163,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
}
@Override
public void close() {
if (!future.isDone())
future.completeExceptionally(new AuthenticationException("OAuth server is closing"));
stop();
}
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
@@ -99,7 +99,7 @@ public final class HMCLJavaRepository implements JavaRepository {
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
}
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
private static void getAllJava(List<Path> list, Platform platform, Path platformRoot) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(platformRoot)) {
for (Path file : stream) {
try {
@@ -117,8 +117,7 @@ public final class HMCLJavaRepository implements JavaRepository {
}
if (Files.isDirectory(javaDir)) {
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
list.add(executable);
}
}
} catch (Throwable e) {
@@ -131,28 +130,23 @@ public final class HMCLJavaRepository implements JavaRepository {
}
@Override
public Collection<JavaRuntime> getAllJava(Platform platform) {
public Collection<Path> getAllJava(Platform platform) {
Path platformRoot = getPlatformRoot(platform);
if (!Files.isDirectory(platformRoot))
return Collections.emptyList();
ArrayList<JavaRuntime> list = new ArrayList<>();
getAllJava(list, platform, platformRoot, true);
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
platformRoot = root.resolve(platform.getOperatingSystem().getMojangName() + "-" + platform.getArchitecture().getCheckedName());
if (Files.isDirectory(platformRoot))
getAllJava(list, platform, platformRoot, false);
}
ArrayList<Path> list = new ArrayList<>();
getAllJava(list, platform, platformRoot);
return list;
}
@Override
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
Path javaDir = getJavaDir(platform, gameJavaVersion);
Path tempDir = getPlatformRoot(platform).resolve(".tmp").resolve(javaDir.getFileName());
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
return new MojangJavaDownloadTask(downloadProvider, javaDir, tempDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
@@ -165,16 +159,16 @@ public final class HMCLJavaRepository implements JavaRepository {
JavaInfo info;
if (JavaManager.isCompatible(platform))
info = JavaInfoUtils.fromExecutable(executable, false);
info = JavaInfoUtils.fromExecutable(executable);
else
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
info = new JavaInfo(platform, result.download().version().name(), null);
Map<String, Object> update = new LinkedHashMap<>();
update.put("provider", "mojang");
update.put("component", gameJavaVersion.component());
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles.getFiles().forEach((path, file) -> {
result.remoteFiles().getFiles().forEach((path, file) -> {
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
if (downloadInfo != null) {
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.java;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
@@ -27,7 +28,6 @@ import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
@@ -39,35 +39,10 @@ public final class JavaInfoUtils {
private JavaInfoUtils() {
}
private static Path tryFindReleaseFile(Path executable) {
Path parent = executable.getParent();
if (parent != null && parent.getFileName() != null && parent.getFileName().toString().equals("bin")) {
Path javaHome = parent.getParent();
if (javaHome != null && javaHome.getFileName() != null) {
Path releaseFile = javaHome.resolve("release");
String javaHomeName = javaHome.getFileName().toString();
if ((javaHomeName.contains("jre") || javaHomeName.contains("jdk") || javaHomeName.contains("openj9"))
&& Files.isRegularFile(releaseFile)) {
return releaseFile;
}
}
}
return null;
}
public static @NotNull JavaInfo fromExecutable(Path executable, boolean tryFindReleaseFile) throws IOException {
public static @NotNull JavaInfo fromExecutable(Path executable) throws IOException {
assert executable.isAbsolute();
Path releaseFile;
if (tryFindReleaseFile && (releaseFile = tryFindReleaseFile(executable)) != null) {
try {
return JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException ignored) {
}
}
Path thisPath = JarUtils.thisJarPath();
if (thisPath == null) {
throw new IOException("Failed to find current HMCL location");
}
@@ -94,7 +69,6 @@ public final class JavaInfoUtils {
? architecture
: Architecture.SYSTEM_ARCH);
return new JavaInfo(platform, result.javaVersion, result.javaVendor);
} catch (IOException e) {
throw e;
@@ -103,14 +77,9 @@ public final class JavaInfoUtils {
}
}
private static final class Result {
@SerializedName("os.name")
public String osName;
@SerializedName("os.arch")
public String osArch;
@SerializedName("java.version")
public String javaVersion;
@SerializedName("java.vendor")
public String javaVendor;
@JsonSerializable
private record Result(@SerializedName("os.name") String osName, @SerializedName("os.arch") String osArch,
@SerializedName("java.version") String javaVersion,
@SerializedName("java.vendor") String javaVendor) {
}
}
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.java;
import com.google.gson.*;
import com.google.gson.stream.JsonWriter;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.Metadata;
@@ -30,7 +32,9 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.windows.WinReg;
@@ -40,9 +44,14 @@ import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -55,6 +64,16 @@ public final class JavaManager {
private JavaManager() {
}
private static final String[] KNOWN_VENDOR_DIRECTORIES = {
"Java",
"BellSoft",
"AdoptOpenJDK",
"Zulu",
"Microsoft",
"Eclipse Foundation",
"Semeru"
};
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"));
@@ -103,16 +122,27 @@ public final class JavaManager {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
// Windows x86-64 platform is compatible with x86 programs
return architecture == Architecture.X86;
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86;
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
// Since Windows 10 Build 21277, Windows Arm64 has been compatible with x86-64 programs via translation
if (architecture == Architecture.X86_64 && Platform.isSupportedTranslationX86_64())
return true;
// Windows Arm64 is compatible with x86 programs via translation
if (architecture == Architecture.X86)
return true;
return false;
}
break;
case LINUX:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
break;
case MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
// macOS Arm64 compatible with x86-64 programs via Rosetta 2.
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
return architecture == Architecture.X86_64;
break;
}
@@ -160,12 +190,12 @@ public final class JavaManager {
return javaRuntime;
}
JavaInfo info = JavaInfoUtils.fromExecutable(executable, true);
JavaInfo info = JavaInfoUtils.fromExecutable(executable);
return JavaRuntime.of(executable, info, false);
}
public static void refresh() {
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
Task.supplyAsync(() -> searchPotentialJavaExecutables(false)).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (result != null) {
LATCH.await();
allJava = result;
@@ -326,7 +356,7 @@ public final class JavaManager {
}
public static void initialize() {
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables(true);
JavaManager.allJava = allJava;
LATCH.countDown();
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
@@ -334,56 +364,56 @@ public final class JavaManager {
// search java
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables() {
Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM);
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables(boolean useCache) {
Searcher searcher = new Searcher(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("javaCache.json"));
if (useCache)
searcher.loadCache();
searcher.searchAllJavaInRepository(Platform.SYSTEM_PLATFORM);
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86_64);
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
if (Platform.isSupportedTranslationX86_64())
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86_64);
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
}
break;
case MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searchAllJavaInRepository(javaRuntimes, Platform.MACOS_X86_64);
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
searcher.searchAllJavaInRepository(Platform.MACOS_X86_64);
break;
}
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(ARM)", "C:\\Program Files (ARM)");
}
searcher.searchJavaInProgramFiles("ProgramFiles", "C:\\Program Files");
searcher.searchJavaInProgramFiles("ProgramFiles(x86)", "C:\\Program Files (x86)");
break;
case LINUX:
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/java")); // Oracle RPMs
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib32/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib64/jvm")); // General locations
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
searcher.searchAllJavaInDirectory(Path.of("/usr/java")); // Oracle RPMs
searcher.searchAllJavaInDirectory(Path.of("/usr/lib/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of("/usr/lib32/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of("/usr/lib64/jvm")); // General locations
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
break;
case MACOS:
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines"));
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
searcher.searchJavaInMacJavaVirtualMachines(Path.of("/Library/Java/JavaVirtualMachines"));
searcher.searchJavaInMacJavaVirtualMachines(Path.of(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
searcher.tryAddJavaExecutable(Path.of("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
searcher.tryAddJavaExecutable(Path.of("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
// Homebrew
tryAddJavaExecutable(javaRuntimes, Paths.get("/opt/homebrew/opt/java/bin/java"));
searchAllJavaInDirectory(javaRuntimes, Paths.get("/opt/homebrew/Cellar/openjdk"));
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Paths.get("/opt/homebrew/Cellar"), "openjdk@*")) {
searcher.tryAddJavaExecutable(Path.of("/opt/homebrew/opt/java/bin/java"));
searcher.searchAllJavaInDirectory(Path.of("/opt/homebrew/Cellar/openjdk"));
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Path.of("/opt/homebrew/Cellar"), "openjdk@*")) {
for (Path dir : dirs) {
searchAllJavaInDirectory(javaRuntimes, dir);
searcher.searchAllJavaInDirectory(dir);
}
} catch (IOException e) {
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
@@ -397,16 +427,16 @@ public final class JavaManager {
// Search Minecraft bundled runtimes
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH.isX86()) {
FileUtils.tryGetPath(System.getenv("localappdata"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime")
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), ".minecraft/runtime"), false);
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), ".minecraft/runtime"), false);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
}
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
searcher.searchAllOfficialJava(CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
// Search in PATH.
if (System.getenv("PATH") != null) {
@@ -420,7 +450,7 @@ public final class JavaManager {
}
try {
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
searcher.tryAddJavaExecutable(Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
@@ -430,16 +460,17 @@ public final class JavaManager {
String[] paths = System.getenv("HMCL_JRES").split(File.pathSeparator);
for (String path : paths) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(path));
searcher.tryAddJavaHome(Path.of(path));
} catch (InvalidPathException ignored) {
}
}
}
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), ".jdks"));
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
searcher.tryAddJavaExecutable(Path.of(javaPath));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path: " + javaPath);
}
@@ -447,233 +478,406 @@ public final class JavaManager {
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
if (currentJava != null
&& !javaRuntimes.containsKey(currentJava.getBinary())
&& !searcher.javaRuntimes.containsKey(currentJava.getBinary())
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
javaRuntimes.put(currentJava.getBinary(), currentJava);
searcher.addResult(currentJava.getBinary(), currentJava);
}
LOG.trace(javaRuntimes.values().stream().sorted()
searcher.saveCache();
LOG.trace(searcher.javaRuntimes.values().stream().sorted()
.map(it -> String.format(" - %s %s (%s, %s): %s",
it.isJDK() ? "JDK" : "JRE",
it.getVersion(),
it.getPlatform().getArchitecture().getDisplayName(),
Lang.requireNonNullElse(it.getVendor(), "Unknown"),
it.getBinary()))
.collect(Collectors.joining("\n", "Finished Java lookup, found " + javaRuntimes.size() + "\n", "")));
return javaRuntimes;
.collect(Collectors.joining("\n", "Finished Java lookup, found " + searcher.javaRuntimes.size() + "\n", "")));
return searcher.javaRuntimes;
}
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
Path executable = getExecutable(javaHome);
if (!Files.isRegularFile(executable)) {
return;
private static final class Searcher {
private final Path cacheFile;
final Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
private final LinkedHashMap<Path, JavaInfoCache> caches = new LinkedHashMap<>();
private final Set<Path> failed = new HashSet<>();
private boolean needRefreshCache = false;
Searcher(Path cacheFile) {
this.cacheFile = cacheFile;
}
try {
executable = executable.toRealPath();
} catch (IOException e) {
LOG.warning("Failed to resolve path " + executable, e);
return;
private static final Pattern CACHE_VERSION_PATTERN = Pattern.compile("(?<major>\\d+)(?:\\.(?<minor>\\d+))?");
private static final int CACHE_MAJOR_VERSION = 0;
private static final int CACHE_MINOR_VERSION = 0;
private record JavaInfoCache(String key, JavaInfo info) {
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
void loadCache() {
if (Files.notExists(cacheFile))
return;
JavaInfo info = null;
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
try {
info = JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException e) {
LOG.warning("Failed to read release file " + releaseFile, e);
JsonObject jsonFile = JsonUtils.fromJsonFile(cacheFile, JsonObject.class);
JsonElement fileVersion = jsonFile.get("version");
Matcher matcher;
if (fileVersion instanceof JsonPrimitive version
&& (matcher = CACHE_VERSION_PATTERN.matcher(version.getAsString())).matches()) {
int major = Integer.parseInt(matcher.group("major"));
String minorString = matcher.group("minor");
int minor = minorString != null ? Integer.parseInt(minorString) : 0;
if (major != CACHE_MAJOR_VERSION || minor < CACHE_MINOR_VERSION)
throw new IOException("Unsupported cache file, version: %s".formatted(version.getAsString()));
} else
throw new IOException("Invalid version JSON: " + fileVersion);
JsonArray cachesArray = jsonFile.getAsJsonArray("caches");
for (JsonElement element : cachesArray) {
try {
var obj = (JsonObject) element;
Path realPath = Path.of(obj.getAsJsonPrimitive("path").getAsString()).toRealPath();
String key = obj.getAsJsonPrimitive("key").getAsString();
OperatingSystem osName = OperatingSystem.parseOSName(obj.getAsJsonPrimitive("os.name").getAsString());
Architecture osArch = Architecture.parseArchName(obj.getAsJsonPrimitive("os.arch").getAsString());
String javaVersion = obj.getAsJsonPrimitive("java.version").getAsString();
JavaInfo.Builder infoBuilder = JavaInfo.newBuilder(Platform.getPlatform(osName, osArch), javaVersion);
if (obj.get("java.vendor") instanceof JsonPrimitive vendor)
infoBuilder.setVendor(vendor.getAsString());
caches.put(realPath, new JavaInfoCache(key, infoBuilder.build()));
} catch (Exception e) {
LOG.warning("Invalid cache: " + element);
needRefreshCache = true;
}
}
} catch (Exception ex) {
LOG.warning("Failed to load cache file: " + cacheFile);
needRefreshCache = true;
}
}
if (info == null) {
void saveCache() {
if (!needRefreshCache)
return;
needRefreshCache = false;
try {
info = JavaInfoUtils.fromExecutable(executable, false);
FileUtils.saveSafely(cacheFile, output -> {
try (var writer = new JsonWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
writer.beginObject();
writer.name("version").value("%d.%d".formatted(CACHE_MAJOR_VERSION, CACHE_MINOR_VERSION));
writer.name("caches");
writer.beginArray();
for (Map.Entry<Path, JavaInfoCache> entry : caches.entrySet()) {
Path path = entry.getKey();
JavaInfoCache cache = entry.getValue();
JavaInfo info = cache.info();
writer.beginObject();
writer.name("path").value(path.toString());
writer.name("key").value(cache.key());
writer.name("os.name").value(info.getPlatform().os().getCheckedName());
writer.name("os.arch").value(info.getPlatform().arch().getCheckedName());
writer.name("java.version").value(info.getVersion());
if (info.getVendor() != null)
writer.name("java.vendor").value(info.getVendor());
writer.endObject();
}
writer.endArray();
writer.endObject();
}
});
} catch (Exception e) {
LOG.warning("Failed to save cache file: " + cacheFile);
}
}
private static @Nullable String createCacheKey(Path realPath) {
Path binDir = realPath.getParent();
if (binDir == null || !FileUtils.getName(binDir).equals("bin"))
return null;
if (Files.isRegularFile(realPath.resolveSibling("ikvm.properties")))
return null;
Path javaHome = binDir.getParent();
if (javaHome == null)
return null;
Path libDir = javaHome.resolve("lib");
if (!Files.isDirectory(libDir))
return null;
BasicFileAttributes launcherAttributes;
String releaseHash = null;
BasicFileAttributes coreLibsAttributes = null;
try {
launcherAttributes = Files.readAttributes(realPath, BasicFileAttributes.class);
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
releaseHash = DigestUtils.digestToString("SHA-1", releaseFile);
} else {
Path coreLibsFile = libDir.resolve("rt.jar");
if (!Files.isRegularFile(coreLibsFile)) {
coreLibsFile = javaHome.resolve("jre/lib/rt.jar");
if (!Files.isRegularFile(coreLibsFile))
return null;
coreLibsAttributes = Files.readAttributes(coreLibsFile, BasicFileAttributes.class);
}
}
} catch (Exception e) {
LOG.warning("Failed to create cache key for " + realPath, e);
return null;
}
StringJoiner joiner = new StringJoiner(",");
joiner.add("sz:" + launcherAttributes.size());
joiner.add("lm:" + launcherAttributes.lastModifiedTime().toMillis());
if (releaseHash != null)
joiner.add(releaseHash);
if (coreLibsAttributes != null) {
joiner.add("rsz:" + coreLibsAttributes.size());
joiner.add("rlm:" + coreLibsAttributes.lastModifiedTime().toMillis());
}
return joiner.toString();
}
void addResult(Path realPath, JavaRuntime javaRuntime) {
javaRuntimes.put(realPath, javaRuntime);
}
void tryAddJavaHome(Path javaHome) {
tryAddJavaExecutable(getExecutable(javaHome));
}
void tryAddJavaExecutable(Path executable) {
tryAddJavaExecutable(executable, false);
}
void tryAddJavaExecutable(Path executable, boolean isManaged) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
return;
}
if (javaRuntimes.containsKey(executable)
|| failed.contains(executable)
|| ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
String cacheKey = createCacheKey(executable);
if (cacheKey != null) {
JavaInfoCache cache = caches.get(executable);
if (cache != null) {
if (isCompatible(cache.info().getPlatform()) && cacheKey.equals(cache.key())) {
javaRuntimes.put(executable, JavaRuntime.of(executable, cache.info(), isManaged));
return;
} else {
caches.remove(executable);
needRefreshCache = true;
}
}
} else if (caches.remove(executable) != null) {
needRefreshCache = true;
}
JavaInfo info;
try {
info = JavaInfoUtils.fromExecutable(executable);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
failed.add(executable);
return;
}
if (cacheKey != null) {
caches.put(executable, new JavaInfoCache(cacheKey, info));
needRefreshCache = true;
}
javaRuntimes.put(executable, JavaRuntime.of(executable, info, isManaged));
}
void tryAddJavaInComponentDir(String platform, Path component, boolean verify) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return;
Path dir = component.resolve(platform).resolve(component.getFileName());
if (verify) {
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
}
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return;
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(macPath);
return;
} else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
tryAddJavaHome(dir);
}
void searchAllJavaInRepository(Platform platform) {
for (Path java : REPOSITORY.getAllJava(platform)) {
tryAddJavaExecutable(java, true);
}
for (Path java : LOCAL_REPOSITORY.getAllJava(platform)) {
tryAddJavaExecutable(java, true);
}
if (platform.os() == OperatingSystem.MACOS) {
// In the past, we used 'osx' as the checked name for macOS
Path platformRoot = REPOSITORY.getPlatformRoot(platform).resolveSibling("osx-" + platform.getArchitecture().getCheckedName());
searchAllJavaInDirectory(platformRoot);
}
}
if (info != null && isCompatible(info.getPlatform()))
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
void searchAllOfficialJava(Path directory, boolean verify) {
if (!Files.isDirectory(directory))
return;
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
private static void tryAddJavaExecutable(Map<Path, JavaRuntime> javaRuntimes, Path executable) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
return;
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
if (javaPlatform != null) {
searchAllOfficialJava(directory, javaPlatform, verify);
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (Platform.isSupportedTranslationX86_64()) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
}
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64()) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
}
}
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
try {
info = JavaInfoUtils.fromExecutable(executable, true);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
}
if (info != null && isCompatible(info.getPlatform())) {
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
}
private static void tryAddJavaInComponentDir(Map<Path, JavaRuntime> javaRuntimes, String platform, Path component, boolean verify) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return;
Path dir = component.resolve(platform).resolve(component.getFileName());
if (verify) {
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
void searchAllOfficialJava(Path directory, String platform, boolean verify) {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
tryAddJavaInComponentDir(platform, component, verify);
}
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return;
LOG.warning("Failed to list java-runtime directory " + directory, e);
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(javaRuntimes, macPath);
void searchAllJavaInDirectory(Path directory) {
if (!Files.isDirectory(directory)) {
return;
} else
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
}
}
tryAddJavaHome(javaRuntimes, dir);
}
private static void searchAllJavaInRepository(Map<Path, JavaRuntime> javaRuntimes, Platform platform) {
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) {
if (!Files.isDirectory(directory))
return;
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
if (javaPlatform != null) {
searchAllOfficialJava(javaRuntimes, directory, javaPlatform, verify);
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(subDir);
}
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, String platform, boolean verify) {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
tryAddJavaInComponentDir(javaRuntimes, platform, component, verify);
void searchJavaInProgramFiles(String env, String defaultValue) {
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
Path path;
try {
path = Path.of(programFiles);
} catch (InvalidPathException ignored) {
return;
}
} catch (IOException e) {
LOG.warning("Failed to list java-runtime directory " + directory, e);
}
}
private static void searchAllJavaInDirectory(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(javaRuntimes, subDir);
for (String vendor : KNOWN_VENDOR_DIRECTORIES) {
searchAllJavaInDirectory(path.resolve(vendor));
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
private static void searchJavaInProgramFiles(Map<Path, JavaRuntime> javaRuntimes, String env, String defaultValue) {
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
Path path;
try {
path = Paths.get(programFiles);
} catch (InvalidPathException ignored) {
return;
}
for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) {
searchAllJavaInDirectory(javaRuntimes, path.resolve(vendor));
}
}
private static void searchJavaInMacJavaVirtualMachines(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(javaRuntimes, subDir.resolve("Contents/Home"));
void searchJavaInMacJavaVirtualMachines(Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(subDir.resolve("Contents/Home"));
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
// ==== Windows Registry Support ====
void queryJavaInRegistryKey(WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
Object home = reg.queryValue(hkey, java, "JavaHome");
if (home instanceof String) {
try {
tryAddJavaHome(javaRuntimes, Paths.get((String) home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
if (reg.queryValue(hkey, java, "JavaHome") instanceof String home) {
try {
tryAddJavaHome(Path.of(home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
}
}
@@ -567,6 +567,21 @@ public final class Config extends ObservableSetting {
this.versionListSource.set(versionListSource);
}
@SerializedName("defaultAddonSource")
private final StringProperty defaultAddonSource = new SimpleStringProperty("modrinth");
public StringProperty defaultAddonSourceProperty() {
return defaultAddonSource;
}
public String getDefaultAddonSource() {
return defaultAddonSource.get();
}
public void setDefaultAddonSource(String defaultAddonSource) {
this.defaultAddonSource.set(defaultAddonSource);
}
@SerializedName("hasProxy")
private final BooleanProperty hasProxy = new SimpleBooleanProperty();
@@ -689,6 +704,21 @@ public final class Config extends ObservableSetting {
this.disableAutoGameOptions.set(disableAutoGameOptions);
}
@SerializedName("allowAutoAgent")
private final BooleanProperty allowAutoAgent = new SimpleBooleanProperty(false);
public BooleanProperty allowAutoAgentProperty() {
return allowAutoAgent;
}
public boolean getAllowAutoAgent() {
return allowAutoAgent.get();
}
public void setAllowAutoAgent(boolean allowAutoAgent) {
this.allowAutoAgent.set(allowAutoAgent);
}
// Accounts
@SerializedName("authlibInjectorServers")
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* 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
* 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.setting;
public final class SambaException extends RuntimeException {
@@ -603,6 +603,20 @@ public final class VersionSetting implements Cloneable, Observable {
processPriorityProperty.set(processPriority);
}
private final ObjectProperty<GraphicsAPI> graphicsBackend = new SimpleObjectProperty<>(this, "graphicsBackend", GraphicsAPI.DEFAULT);
public ObjectProperty<GraphicsAPI> graphicsBackendProperty() {
return graphicsBackend;
}
public GraphicsAPI getGraphicsBackend() {
return graphicsBackendProperty().get();
}
public void setGraphicsBackend(GraphicsAPI api) {
graphicsBackendProperty().set(api);
}
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
public Renderer getRenderer() {
@@ -820,8 +834,9 @@ public final class VersionSetting implements Cloneable, Observable {
}
obj.addProperty("java", java);
obj.addProperty("graphicsBackend", src.getGraphicsBackend().name());
obj.addProperty("renderer", src.getRenderer().name());
if (src.getRenderer() == Renderer.LLVMPIPE)
if (src.getRenderer() == Renderer.OpenGL.LLVMPIPE)
obj.addProperty("useSoftwareRenderer", true);
return obj;
@@ -894,16 +909,19 @@ public final class VersionSetting implements Cloneable, Observable {
}
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
.map(Renderer::of).orElseGet(() -> {
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
return useSoftwareRenderer ? Renderer.OpenGL.LLVMPIPE : Renderer.DEFAULT;
}));
vs.setGraphicsBackend(Optional.ofNullable(obj.get("graphicsBackend")).map(JsonElement::getAsString)
.flatMap(name -> {
try {
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
return Optional.of(GraphicsAPI.valueOf(name.toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
}).orElseGet(() -> {
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
}));
}).orElseGet(() -> vs.getRenderer() instanceof Renderer.Driver renderer ? renderer.api() : GraphicsAPI.DEFAULT));
return vs;
}
@@ -36,8 +36,9 @@ import org.glavo.monetfx.beans.property.ColorSchemeProperty;
import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty;
import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.ui.MacOSNativeUtils;
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OSVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -233,6 +234,11 @@ public final class Themes {
}
});
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) {
MacOSNativeUtils.setAppearance(darkModeProperty().get());
ChangeListener<Boolean> listener = FXUtils.onWeakChange(Themes.darkModeProperty(), MacOSNativeUtils::setAppearance);
stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener);
}
}
@@ -105,9 +105,14 @@ public final class Controllers {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
Path modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
FXUtils.applyDragListener(gameListPage, file -> ModpackHelper.isFileModpackByExtension(file) || "json".equalsIgnoreCase(FileUtils.getNameWithoutExtension(file)), files -> {
Path file = files.get(0);
if (ModpackHelper.isFileModpackByExtension(file)) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file), i18n("install.modpack"));
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
Versions.installFromJson(Profiles.getSelectedProfile(), file);
}
});
return gameListPage;
});
@@ -208,6 +213,21 @@ public final class Controllers {
return decorator;
}
public static void saveWindowStates() {
if (stageX != null) {
config().setX(stageX.get() / SCREEN.getBounds().getWidth());
}
if (stageY != null) {
config().setY(stageY.get() / SCREEN.getBounds().getHeight());
}
if (stageHeight != null) {
config().setHeight(stageHeight.get());
}
if (stageWidth != null) {
config().setWidth(stageWidth.get());
}
}
public static void onApplicationStop() {
stageSizeChangeListener = null;
if (stageX != null) {
@@ -370,7 +390,7 @@ public final class Controllers {
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.macos_arm64"), null, MessageType.INFO, continueAction);
continueAction.run();
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.windows_arm64"), null, MessageType.INFO, continueAction);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX &&
@@ -235,8 +235,8 @@ public final class FXUtils {
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
public static final List<String> IMAGE_EXTENSIONS = Lang.immutableListOf(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "apng"
public static final List<String> IMAGE_EXTENSIONS = List.of(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "svg", "apng"
);
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
@@ -273,10 +274,10 @@ public class GameCrashWindow extends Stage {
logWindow.show();
}
private void exportGameCrashInfo() {
private CompletableFuture<Path> exportGameCrashInfo() {
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
CompletableFuture.supplyAsync(() ->
return CompletableFuture.supplyAsync(() ->
logs.stream().map(Log::getLog).collect(Collectors.joining("\n")))
.thenComposeAsync(logs -> {
long processStartTime = managedProcess.getProcess().info()
@@ -301,20 +302,7 @@ public class GameCrashWindow extends Stage {
return false;
}
});
})
.handleAsync((result, exception) -> {
if (exception == null) {
FXUtils.showFileInExplorer(logFile);
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.success", logFile), i18n("message.success"), MessageDialogPane.MessageType.SUCCESS).ok(null).build();
DialogUtils.show(stackPane, dialog);
} else {
LOG.warning("Failed to export game crash info", exception);
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build();
DialogUtils.show(stackPane, dialog);
}
return null;
}, Schedulers.javafx());
}).thenApply(ignored -> logFile);
}
private final class View extends VBox {
@@ -444,8 +432,35 @@ public class GameCrashWindow extends Stage {
HBox toolBar = new HBox();
VBox.setMargin(toolBar, new Insets(0, 0, 4, 0));
{
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
SpinnerPane exportButtonPane = new SpinnerPane();
exportButtonPane.getStyleClass().add("small-spinner-pane");
JFXButton exportButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportButtonPane.setContent(exportButton);
exportButton.setOnAction(e -> {
exportButtonPane.showSpinner();
exportGameCrashInfo().whenCompleteAsync((result, exception) -> {
exportButtonPane.hideSpinner();
if (exception == null) {
FXUtils.showFileInExplorer(result);
var dialog = new MessageDialogPane.Builder(
i18n("settings.launcher.launcher_log.export.success", result),
i18n("message.success"),
MessageDialogPane.MessageType.SUCCESS
).ok(null).build();
DialogUtils.show(stackPane, dialog);
} else {
LOG.warning("Failed to export game crash info", exception);
var dialog = new MessageDialogPane.Builder(
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
i18n("message.error"),
MessageDialogPane.MessageType.ERROR
).ok(null).build();
DialogUtils.show(stackPane, dialog);
}
}, Schedulers.javafx());
});
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
logButton.setOnAction(e -> showLogWindow());
@@ -457,7 +472,7 @@ public class GameCrashWindow extends Stage {
toolBar.setPadding(new Insets(8));
toolBar.setSpacing(8);
toolBar.getStyleClass().add("jfx-tool-bar");
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton);
toolBar.getChildren().setAll(exportButtonPane, logButton, helpButton);
}
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
@@ -37,6 +37,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
* @author Glavo
*/
public final class HTMLRenderer {
private static final String INDENT = " ";
private static URI resolveLink(Node linkNode) {
String href = linkNode.absUrl("href");
if (href.isEmpty())
@@ -57,6 +58,7 @@ public final class HTMLRenderer {
private boolean underline;
private boolean strike;
private boolean highlight;
private int indentLevel;
private String headerLevel;
private Node hyperlink;
@@ -218,7 +220,7 @@ public final class HTMLRenderer {
appendImage(node);
break;
case "li":
appendText("\n \u2022 ");
appendText("\n" + INDENT.repeat(indentLevel) + " \u2022 ");
break;
case "dt":
appendText(" ");
@@ -237,11 +239,14 @@ public final class HTMLRenderer {
}
if (node.childNodeSize() > 0) {
boolean isLiNode = "li".equals(name);
if (isLiNode) indentLevel++;
pushNode(node);
for (Node childNode : node.childNodes()) {
appendNode(childNode);
}
popNode();
if (isLiNode) indentLevel--;
}
switch (name) {
@@ -30,15 +30,14 @@ import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
@@ -169,6 +168,7 @@ public final class LogWindow extends Stage {
LogWindowImpl() {
getStyleClass().add("log-window");
listView.getStyleClass().add("no-horizontal-scrollbar");
listView.getProperties().put("no-smooth-scrolling", true);
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
@@ -241,6 +241,10 @@ public final class LogWindow extends Stage {
});
}
private ManagedProcess getGameProcess() {
return gameProcess;
}
@Override
protected Skin<?> createDefaultSkin() {
return new LogWindowSkin(this);
@@ -255,9 +259,6 @@ public final class LogWindow extends Stage {
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final Set<ListCell<Log>> selected = new HashSet<>();
private final JFXSnackbar snackbar = new JFXSnackbar();
LogWindowSkin(LogWindowImpl control) {
@@ -303,6 +304,7 @@ public final class LogWindow extends Stage {
{
ListView<Log> listView = control.listView;
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
listView.getItems().addListener((InvalidationListener) observable -> {
if (!listView.getItems().isEmpty() && control.autoScroll.get())
listView.scrollTo(listView.getItems().size() - 1);
@@ -312,7 +314,6 @@ public final class LogWindow extends Stage {
+ "\"; -fx-font-size: " + config().getFontSize() + "px;");
listView.setCellFactory(x -> new ListCell<>() {
{
x.setSelectionModel(new NoneMultipleSelectionModel<>());
getStyleClass().add("log-window-list-cell");
Region clippedContainer = (Region) listView.lookup(".clipped-container");
if (clippedContainer != null) {
@@ -322,32 +323,6 @@ public final class LogWindow extends Stage {
setPadding(new Insets(2));
setWrapText(true);
setGraphic(null);
setOnMouseClicked(event -> {
if (event.getButton() != MouseButton.PRIMARY)
return;
if (!event.isControlDown()) {
for (ListCell<Log> logListCell : selected) {
if (logListCell != this) {
logListCell.pseudoClassStateChanged(SELECTED, false);
if (logListCell.getItem() != null) {
logListCell.getItem().setSelected(false);
}
}
}
selected.clear();
}
selected.add(this);
pseudoClassStateChanged(SELECTED, true);
if (getItem() != null) {
getItem().setSelected(true);
}
event.consume();
});
}
@Override
@@ -361,7 +336,6 @@ public final class LogWindow extends Stage {
pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO);
pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG);
pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE);
pseudoClassStateChanged(SELECTED, !empty && item.isSelected());
if (empty) {
setText(null);
@@ -373,10 +347,13 @@ public final class LogWindow extends Stage {
listView.setOnKeyPressed(event -> {
if (event.isControlDown() && event.getCode() == KeyCode.C) {
if (listView.getSelectionModel().isEmpty())
return;
StringBuilder stringBuilder = new StringBuilder();
for (Log item : listView.getItems()) {
if (item != null && item.isSelected()) {
for (Log item : listView.getSelectionModel().getSelectedItems()) {
if (item != null) {
if (item.getLog() != null)
stringBuilder.append(item.getLog());
stringBuilder.append('\n');
@@ -425,6 +402,13 @@ public final class LogWindow extends Stage {
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
control.getGameProcess().getProcess()
.onExit()
.thenRunAsync(() -> {
terminateButton.setDisable(true);
exportDumpButton.setDisable(true);
}, Schedulers.javafx());
vbox.getChildren().add(bottom);
}
}
@@ -0,0 +1,101 @@
/*
* Hello Minecraft! Launcher
* 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
* 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 com.sun.jna.Pointer;
import org.jackhuang.hmcl.util.platform.macos.ObjectiveCRuntime;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class MacOSNativeUtils {
private static final Pointer nsApp = init();
private static @Nullable Pointer init() {
if (ObjectiveCRuntime.INSTANCE == null) {
return null;
}
try {
var objc = ObjectiveCRuntime.INSTANCE;
Pointer nsApplication = objc.objc_getClass("NSApplication");
if (!isNull(nsApplication)) {
Pointer sharedSel = objc.sel_registerName("sharedApplication");
if (!isNull(sharedSel))
return objc.objc_msgSend(nsApplication, sharedSel);
}
} catch (Throwable e) {
LOG.warning("Failed to initialize macOS appearance support", e);
}
return null;
}
public static boolean isSupported() {
return nsApp != null;
}
private static boolean isNull(Pointer pointer) {
return pointer == null || Pointer.nativeValue(pointer) == 0;
}
public static void setAppearance(boolean dark) {
setAppearance(dark, false);
}
public static void setAppearance(boolean dark, boolean highContrast) {
if (nsApp == null) return;
try {
var objc = ObjectiveCRuntime.INSTANCE;
Pointer nsAppearance = objc.objc_getClass("NSAppearance");
if (isNull(nsAppearance))
return;
Pointer namedSel = objc.sel_registerName("appearanceNamed:");
Pointer nsString = objc.objc_getClass("NSString");
if (isNull(nsString)) return;
Pointer sel = objc.sel_registerName("stringWithUTF8String:");
String appearanceName;
if (highContrast) {
appearanceName = dark ? "NSAppearanceNameAccessibilityHighContrastDarkAqua" : "NSAppearanceNameAccessibilityHighContrastAqua";
} else {
appearanceName = dark ? "NSAppearanceNameDarkAqua" : "NSAppearanceNameAqua";
}
Pointer appearanceNamePtr = objc.objc_msgSend(nsString, sel, appearanceName);
if (isNull(appearanceNamePtr)) return;
Pointer appearance = objc.objc_msgSend(nsAppearance, namedSel, appearanceNamePtr);
if (isNull(appearance)) return;
Pointer setSel = objc.sel_registerName("setAppearance:");
objc.objc_msgSend(nsApp, setSel, appearance);
} catch (Throwable t) {
LOG.warning("Failed to set macOS appearance", t);
}
}
private MacOSNativeUtils() {
}
}
@@ -0,0 +1,228 @@
/*
* Hello Minecraft! Launcher
* 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
* 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.application.Platform;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.MathUtils;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
import java.lang.ref.WeakReference;
/// @author Glavo
public final class MemoryStatusBar extends Control {
private static final String DEFAULT_STYLE_CLASS = "memory-status-bar";
public MemoryStatusBar() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
setPrefWidth(200);
}
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
/// The memory status of the system.
///
/// The property will be automatically updated.
///
/// The property has no bean, because it may share between multiple MemoryStatusBar instances.
public ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
return memoryStatus;
}
public PhysicalMemoryStatus getMemoryStatus() {
return memoryStatus.get();
}
private final DoubleProperty memoryAllocated = new SimpleDoubleProperty();
public DoubleProperty memoryAllocatedProperty() {
return memoryAllocated;
}
public double getMemoryAllocated() {
return memoryAllocated.get();
}
public void setMemoryAllocated(double memoryAllocated) {
this.memoryAllocated.set(memoryAllocated);
}
@Override
protected SkinBase<MemoryStatusBar> createDefaultSkin() {
return new Skin(this);
}
private static final class Skin extends SkinBase<MemoryStatusBar> {
private static final int HEIGHT = 4;
private final StackPane track;
private final Rectangle memoryUsed;
private final Rectangle memoryAllocate;
private double contentWidth = -1;
Skin(MemoryStatusBar control) {
super(control);
track = new StackPane();
track.getStyleClass().add("track");
track.setPadding(new Insets(HEIGHT, 0, 0, 0));
memoryUsed = new Rectangle();
memoryUsed.setManaged(false);
memoryUsed.setArcWidth(HEIGHT);
memoryUsed.setArcHeight(HEIGHT);
memoryUsed.getStyleClass().add("memory-used");
memoryAllocate = new Rectangle();
memoryAllocate.setManaged(false);
memoryAllocate.setArcWidth(HEIGHT);
memoryAllocate.setArcHeight(HEIGHT);
memoryAllocate.getStyleClass().add("memory-allocate");
this.getChildren().setAll(track, memoryAllocate, memoryUsed);
registerInvalidationListener(control.memoryStatusProperty(), it -> updateBar());
registerInvalidationListener(control.memoryAllocatedProperty(), it -> updateBar());
}
private void updateBar() {
if (contentWidth <= 0) {
// not yet layout
return;
}
PhysicalMemoryStatus status = getSkinnable().getMemoryStatus();
double total = status.getTotal();
double used = status.getUsed();
double allocated = getSkinnable().getMemoryAllocated();
if (total <= 0) {
memoryUsed.setWidth(contentWidth);
memoryAllocate.setWidth(contentWidth);
return;
}
double usedWidth = MathUtils.clamp(contentWidth * (used / total), 0, contentWidth);
double allocatedWidth = MathUtils.clamp(contentWidth * (allocated / total) + usedWidth, 0, contentWidth);
memoryUsed.setWidth(usedWidth);
memoryAllocate.setWidth(allocatedWidth);
}
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
track.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
memoryUsed.relocate(contentX, contentY);
memoryAllocate.relocate(contentX, contentY);
this.contentWidth = contentWidth;
memoryUsed.setHeight(contentHeight);
memoryAllocate.setHeight(contentHeight);
updateBar();
}
@Override
public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return leftInset + track.prefWidth(height) + rightInset;
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return topInset + HEIGHT + bottomInset;
}
@Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
}
private static final class UpdateMemoryStatus extends Thread {
private static final int UPDATE_INTERVAL = 3000;
@FXThread
private static WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyCache;
@FXThread
static ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
if (memoryStatusPropertyCache != null) {
var property = memoryStatusPropertyCache.get();
if (property != null) {
return property;
}
}
ObjectProperty<PhysicalMemoryStatus> property = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
memoryStatusPropertyCache = new WeakReference<>(property);
new UpdateMemoryStatus(memoryStatusPropertyCache).start();
return property;
}
private final WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef;
UpdateMemoryStatus(WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef) {
this.memoryStatusPropertyRef = memoryStatusPropertyRef;
setName("UpdateMemoryStatus");
setDaemon(true);
setPriority(Thread.MIN_PRIORITY);
}
@Override
public void run() {
while (true) {
PhysicalMemoryStatus status = SystemInfo.getPhysicalMemoryStatus();
var memoryStatusProperty = memoryStatusPropertyRef.get();
if (memoryStatusProperty == null)
return;
if (Controllers.isStopped())
return;
Platform.runLater(() -> memoryStatusProperty.set(status));
try {
//noinspection BusyWait
Thread.sleep(UPDATE_INTERVAL);
} catch (InterruptedException e) {
return;
}
}
}
}
}
@@ -164,6 +164,9 @@ public class AccountListItem extends RadioButton {
if (skinImg.isError()) {
throw new InvalidSkinException("Failed to read skin image", skinImg.getException());
}
if (skinImg.getWidth() != 64 || (skinImg.getHeight() != 32 && skinImg.getHeight() != 64)) {
throw new InvalidSkinException("Invalid skin size");
}
NormalizedSkin skin = new NormalizedSkin(skinImg);
String model = skin.isSlim() ? "slim" : "";
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
@@ -115,7 +115,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
GridPane.setRowIndex(lblServerName, 1);
lblServerWarning = new Label(i18n("account.injector.http"));
lblServerWarning.setStyle("-fx-text-fill: red;");
lblServerWarning.setStyle("-fx-text-fill: -monet-error;");
GridPane.setColumnIndex(lblServerWarning, 0);
GridPane.setRowIndex(lblServerWarning, 2);
lblServerWarning.managedProperty().bind(lblServerWarning.visibleProperty());
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -28,6 +30,7 @@ import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -43,8 +46,6 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
private boolean expanded = false;
ComponentSublistWrapper(ComponentSublist sublist) {
boolean noPadding = !sublist.hasComponentPadding();
this.getStyleClass().add("options-sublist-wrapper");
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
@@ -104,12 +105,26 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
if (container == null) {
this.container = new VBox();
if (!noPadding) {
container.setPadding(new Insets(8, 16, 10, 16));
}
this.container.getStyleClass().add("container");
FXUtils.setLimitHeight(container, 0);
FXUtils.setOverflowHidden(container);
Rectangle rectangle = FXUtils.setOverflowHidden(container);
rectangle.getStyleClass().add("overflow-hidden");
var last = PseudoClass.getPseudoClass("last");
InvalidationListener updateArc = o -> {
if (ComponentSublistWrapper.this.getPseudoClassStates().contains(last)) {
rectangle.setArcHeight(4);
rectangle.setArcWidth(4);
} else {
rectangle.setArcHeight(0);
rectangle.setArcWidth(0);
}
};
updateArc.invalidated(null);
ComponentSublistWrapper.this.getPseudoClassStates().addListener(updateArc);
container.getChildren().setAll(sublist);
ComponentSublistWrapper.this.getChildren().add(container);
@@ -120,8 +135,7 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
}
Platform.runLater(() -> {
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
double contentHeight = expanded ? (sublist.prefHeight(sublist.getWidth()) + (noPadding ? 4 : 8 + 10)) : 0;
double contentHeight = expanded ? sublist.prefHeight(sublist.getWidth()) : 0;
double targetRotate = expanded ? -180 : 0;
if (AnimationUtils.isAnimationEnabled()) {
@@ -17,8 +17,14 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.utils.JFXNodeUtils;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.beans.value.ChangeListener;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
@@ -26,7 +32,11 @@ import javafx.scene.control.ScrollBar;
import javafx.scene.control.Skin;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.util.Lang;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.util.MathUtils;
// Referenced in root.css
@SuppressWarnings("unused")
@@ -36,6 +46,10 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
private Rectangle track = new Rectangle();
private Rectangle thumb = new Rectangle();
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"})
private ChangeListener<Boolean> thumbHoverListener;
private Animation thumbHoverAnimation;
public FloatScrollBarSkin(final ScrollBar scrollBar) {
this.scrollBar = scrollBar;
scrollBar.setPrefHeight(1e-18);
@@ -45,8 +59,8 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
Point2D dragStart;
double preDragThumbPos;
NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
final NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
final NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
{
// Children are added unmanaged because for some reason the height of the bar keeps changing
@@ -63,7 +77,6 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
setup();
thumb.setOnMousePressed(me -> {
if (me.isSynthesized()) {
// touch-screen events handled by Scroll handler
@@ -75,7 +88,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
*/
if (getSkinnable().getMax() > getSkinnable().getMin()) {
dragStart = thumb.localToParent(me.getX(), me.getY());
double clampedValue = Lang.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax());
double clampedValue = MathUtils.clamp(getSkinnable().getValue(), getSkinnable().getMin(), getSkinnable().getMax());
preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin());
me.consume();
}
@@ -107,7 +120,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
getSkinnable().requestFocus();
double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin();
if (!Double.isNaN(newValue)) {
getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax()));
getSkinnable().setValue(MathUtils.clamp(newValue, getSkinnable().getMin(), getSkinnable().getMax()));
}
}
@@ -128,14 +141,18 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
track.widthProperty().unbind();
track.heightProperty().unbind();
double offset = 4;
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
track.relocate(0, -8);
track.widthProperty().bind(scrollBar.widthProperty());
track.setHeight(8);
track.relocate(offset, -6.0);
NumberBinding trackWidth = Bindings.max(0, Bindings.subtract(scrollBar.widthProperty(), offset * 2));
track.widthProperty().bind(trackWidth);
track.setHeight(6.0);
} else {
track.relocate(-8, 0);
track.setWidth(8);
track.heightProperty().bind(scrollBar.heightProperty());
track.relocate(-6.0, offset);
track.setWidth(6.0);
NumberBinding trackHeight = Bindings.max(0, Bindings.subtract(scrollBar.heightProperty(), offset * 2));
track.heightProperty().bind(trackHeight);
}
thumb.xProperty().unbind();
@@ -144,15 +161,37 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
thumb.heightProperty().unbind();
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
thumb.relocate(0, -8);
thumb.widthProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
thumb.setHeight(8);
thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position));
thumb.relocate(0, -6.0);
thumb.setHeight(6.0);
NumberBinding trackWidth = Bindings.max(0, Bindings.subtract(scrollBar.widthProperty(), offset * 2));
thumb.widthProperty().bind(Bindings.min(trackWidth,
Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(trackWidth))));
thumb.xProperty().bind(
Bindings.add(offset,
Bindings.multiply(
Bindings.max(0, Bindings.subtract(trackWidth, thumb.widthProperty())),
position)
)
);
} else {
thumb.relocate(-8, 0);
thumb.setWidth(8);
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
thumb.relocate(-6.0, 0);
thumb.setWidth(6.0);
NumberBinding trackHeight = Bindings.max(0, Bindings.subtract(scrollBar.heightProperty(), offset * 2));
thumb.heightProperty().bind(Bindings.min(trackHeight,
Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(trackHeight))));
thumb.yProperty().bind(
Bindings.add(offset,
Bindings.multiply(
Bindings.max(0, Bindings.subtract(trackHeight, thumb.heightProperty())),
position)
)
);
}
}
@@ -162,7 +201,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
return Double.MAX_VALUE;
}
return 8;
return 6.0;
}
@Override
@@ -171,13 +210,46 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
return Double.MAX_VALUE;
}
return 8;
return 6.0;
}
};
this.thumbHoverListener = FXUtils.onWeakChangeAndOperate(thumb.hoverProperty(), newValue -> {
if (thumbHoverAnimation != null) {
thumbHoverAnimation.stop();
thumbHoverAnimation = null;
}
double targetOpacity = newValue ? 1.0 : 0.5;
double currentOpacity = thumb.getOpacity();
double opacityAdjustment = targetOpacity - currentOpacity;
if (Math.abs(opacityAdjustment) < 0.001) {
if (opacityAdjustment != 0)
thumb.setOpacity(targetOpacity);
return;
}
if (AnimationUtils.isAnimationEnabled() && JFXNodeUtils.isTreeVisible(thumb)) {
thumbHoverAnimation = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(thumb.opacityProperty(), currentOpacity)),
new KeyFrame(Motion.SHORT2, new KeyValue(thumb.opacityProperty(), targetOpacity))
);
thumbHoverAnimation.play();
} else {
thumb.setOpacity(targetOpacity);
}
});
}
@Override
public void dispose() {
if (thumbHoverAnimation != null) {
thumbHoverAnimation.stop();
thumbHoverAnimation = null;
}
thumbHoverListener = null;
scrollBar = null;
group = null;
}
@@ -30,9 +30,6 @@ import org.jackhuang.hmcl.ui.SVG;
public class LineButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-button";
private static final int IDX_TRAILING_TEXT = IDX_TRAILING;
private static final int IDX_TRAILING_ICON = IDX_TRAILING + 1;
public static LineButton createNavigationButton() {
var button = new LineButton();
button.setTrailingIcon(SVG.ARROW_FORWARD);
@@ -50,7 +47,14 @@ public class LineButton extends LineButtonBase {
public LineButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
container.setMouseTransparent(true);
}
protected int getTrailingTextIndex() {
return IDX_TRAILING;
}
protected int getTrailingIconIndex() {
return getTrailingTextIndex() + 1;
}
private ObjectProperty<EventHandler<ActionEvent>> onAction;
@@ -110,12 +114,13 @@ public class LineButton extends LineButtonBase {
trailingTextLabel = new Label();
trailingTextLabel.getStyleClass().add("trailing-label");
trailingTextLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
trailingTextLabel.setMouseTransparent(true);
}
trailingTextLabel.setText(message);
setNode(IDX_TRAILING_TEXT, trailingTextLabel);
setNode(getTrailingTextIndex(), trailingTextLabel);
} else if (trailingTextLabel != null) {
trailingTextLabel.setText("");
setNode(IDX_TRAILING_TEXT, null);
setNode(getTrailingTextIndex(), null);
}
}
};
@@ -149,7 +154,7 @@ public class LineButton extends LineButtonBase {
@Override
protected void invalidated() {
setNode(IDX_TRAILING_ICON, get());
setNode(getTrailingIconIndex(), get());
}
};
@@ -171,6 +176,7 @@ public class LineButton extends LineButtonBase {
public void setTrailingIcon(SVG rightIcon, double size) {
Node rightIconNode = rightIcon.createIcon(size);
rightIconNode.getStyleClass().add("trailing-icon");
rightIconNode.setMouseTransparent(true);
setTrailingIcon(rightIconNode);
}
}
@@ -64,6 +64,7 @@ public abstract class LineComponent extends StackPane implements NoPaddingCompon
this.container = new HBox(SPACING);
container.getStyleClass().add("line-component-container");
container.setAlignment(Pos.CENTER_LEFT);
container.setPickOnBounds(true);
this.titleLabel = new Label();
titleLabel.getStyleClass().add("title-label");
@@ -28,6 +28,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.*;
import javafx.scene.layout.StackPane;
@@ -43,12 +44,14 @@ import java.util.function.Function;
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
/// @author Glavo
public final class LineSelectButton<T> extends LineButton {
public class LineSelectButton<T> extends LineButton {
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
private JFXPopup popup;
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private ObservableList<Node> popupItems; // keep a reference
public LineSelectButton() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
@@ -87,7 +90,7 @@ public final class LineSelectButton<T> extends LineButton {
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
Bindings.bindContent(popupMenu.getContent(), popupItems = MappedObservableList.create(itemsProperty(), item -> {
VBox vbox = new VBox();
var itemTitleLabel = new Label();
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.geometry.Insets;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.theme.Themes;
public class MenuSeparator extends StackPane {
@@ -28,7 +28,7 @@ public class MenuSeparator extends StackPane {
Rectangle rect = new Rectangle();
rect.widthProperty().bind(widthProperty().add(-14));
rect.setHeight(1);
rect.setFill(Color.GRAY);
rect.fillProperty().bind(Themes.colorSchemeProperty().getOutlineVariant());
maxHeightProperty().set(10);
setPadding(new Insets(3));
getChildren().setAll(rect);
@@ -55,7 +55,8 @@ public final class MultiFileItem<T> extends VBox {
@SuppressWarnings("unchecked")
public MultiFileItem() {
setPadding(new Insets(0, 0, 10, 0));
this.getStyleClass().add("multi-file-item");
setSpacing(8);
group.selectedToggleProperty().addListener((a, b, newValue) -> {
@@ -81,12 +81,11 @@ public class TaskExecutorDialogPane extends BorderPane {
setCancel(cancel);
btnCancel.setDisable(onCancel.getCancellationAction() == null);
btnCancel.setOnAction(e -> {
if (onCancel.getCancellationAction() != null) {
if (executor != null)
executor.cancel();
onCancel.getCancellationAction().accept(this);
}
if (executor != null)
executor.cancel();
onCancel.getCancellationAction().accept(this);
});
speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> {
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
@@ -146,6 +147,10 @@ public class TwoLineListItem extends VBox {
subtitleProperty().set(subtitle);
}
public HBox getFirstLine() {
return firstLine;
}
public Label getTitleLabel() {
return lblTitle;
}
@@ -167,12 +172,15 @@ public class TwoLineListItem extends VBox {
Bindings.bindContent(tagsBox.getChildren(), tags);
var scrollPane = new ScrollPane(tagsBox);
scrollPane.setMinSize(0, 0);
HBox.setHgrow(scrollPane, Priority.ALWAYS);
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
FXUtils.onChangeAndOperate(tagsBox.heightProperty(), height -> FXUtils.setLimitHeight(scrollPane, height.doubleValue()));
scrollPane.setPrefWidth(50);
firstLine.getChildren().setAll(lblTitle, scrollPane);
tags.addListener((InvalidationListener) ignored -> scrollPane.requestLayout());
}
return tags;
}
@@ -50,7 +50,7 @@ import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.MathUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
@@ -218,7 +218,7 @@ public class DecoratorController {
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
case PAINT:
Paint paint = config().getBackgroundPaint();
double opacity = Lang.clamp(0, config().getBackgroundImageOpacity(), 100) / 100.;
double opacity = MathUtils.clamp(config().getBackgroundImageOpacity(), 0, 100) / 100.;
if (paint instanceof Color || paint == null) {
Color color = (Color) paint;
if (color == null)
@@ -274,6 +274,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (canBack) {
JFXButton backNavButton = new JFXButton();
skinnable.forbidDraggingWindow(backNavButton);
backNavButton.setFocusTraversable(false);
backNavButton.setGraphic(SVG.ARROW_BACK.createIcon(Themes.titleFillProperty()));
backNavButton.getStyleClass().add("jfx-decorator-button");
@@ -285,6 +286,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (canClose) {
JFXButton closeNavButton = new JFXButton();
skinnable.forbidDraggingWindow(closeNavButton);
closeNavButton.setFocusTraversable(false);
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
closeNavButton.getStyleClass().add("jfx-decorator-button");
@@ -349,13 +351,9 @@ public class DecoratorSkin extends SkinBase<Decorator> {
refreshNavButton.setGraphic(SVG.REFRESH.createIcon(Themes.titleFillProperty()));
refreshNavButton.getStyleClass().add("jfx-decorator-button");
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
skinnable.forbidDraggingWindow(refreshNavButton);
Rectangle separator = new Rectangle();
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
separator.heightProperty().bind(navBar.heightProperty());
separator.setFill(Color.GRAY);
navRight.getChildren().setAll(refreshNavButton, separator);
navRight.getChildren().setAll(refreshNavButton);
navBar.setRight(navRight);
}
}
@@ -54,13 +54,18 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage {
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
@@ -134,6 +139,19 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
Set<String> existingFiles;
try (var list = Files.list(runDirectory.resolve(subdirectoryName))) {
existingFiles = list.map(Path::getFileName)
.map(Path::toString)
.collect(Collectors.toSet());
} catch (IOException e) {
LOG.warning("Failed to list files in " + runDirectory.resolve(subdirectoryName), e);
existingFiles = Set.of();
}
Set<String> finalExistingFiles = existingFiles;
Controllers.prompt(i18n("archive.file.name"), (result, handler) -> {
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
@@ -153,7 +171,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
}
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
handler.resolve();
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValidForJar), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
}
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* 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
* 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.download;
import com.jfoenix.controls.JFXButton;
@@ -97,8 +97,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
graphic.setMouseTransparent(true);
graphic.setLeft(new TwoLineListItem(i18n("modpack.choose." + type), i18n("modpack.choose." + type + ".detail")));
SVGPath arrow = new SVGPath();
arrow.setContent(SVG.ARROW_FORWARD.getPath());
SVGPath arrow = SVG.ARROW_FORWARD.createIcon();
BorderPane.setAlignment(arrow, Pos.CENTER);
graphic.setRight(arrow);
@@ -423,7 +423,11 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
if (status == Status.LOADING)
transitionPane.setContent(spinner, ContainerAnimations.FADE);
else if (status == Status.SUCCESS)
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
if (control.versions.isEmpty()) {
transitionPane.setContent(emptyPane, ContainerAnimations.FADE);
} else {
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
}
else // if (status == Status.FAILED)
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
});
@@ -17,11 +17,21 @@
*/
package org.jackhuang.hmcl.ui.image;
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import org.girod.javafx.svgimage.LoaderParameters;
import org.girod.javafx.svgimage.SVGImage;
import org.girod.javafx.svgimage.SVGLoader;
import org.girod.javafx.svgimage.ScaleQuality;
import org.glavo.webp.WebPImage;
import org.glavo.webp.WebPImageLoadOptions;
import org.glavo.webp.javafx.WebPFXImage;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.image.apng.Png;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888BitmapSequence;
@@ -30,16 +40,13 @@ import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl;
import org.jackhuang.hmcl.util.SwingFXUtils;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -61,16 +68,48 @@ public final class ImageUtils {
};
public static final ImageLoader WEBP = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
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();
var options = new WebPImageLoadOptions(requestedWidth, requestedHeight, preserveRatio, smooth);
return new WebPFXImage(WebPImage.read(input, options));
};
public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);
LoaderParameters parameters = new LoaderParameters();
parameters.autoStartAnimations = false;
SVGImage image;
if (Platform.isFxApplicationThread()) {
image = SVGLoader.load(content, parameters);
} else {
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
image = CompletableFuture.supplyAsync(
() -> SVGLoader.load(content, parameters),
Schedulers.javafx()
).get();
}
if (image == null)
throw new IOException("Failed to load SVG image");
var snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return image.toImage(snapshotParameters);
}
double scaleX = requestedWidth / image.getScaledWidth();
double scaleY = requestedHeight / image.getScaledHeight();
if (preserveRatio || scaleX == scaleY) {
double scale = Math.min(scaleX, scaleY);
return image.scale(scale).toImage(snapshotParameters);
} else {
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
}
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
};
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
@@ -136,11 +175,13 @@ public final class ImageUtils {
public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
"webp", WEBP,
"svg", SVG,
"apng", APNG
);
public static final Map<String, ImageLoader> CONTENT_TYPE_TO_LOADER = Map.of(
"image/webp", WEBP,
"image/svg+xml", SVG,
"image/apng", APNG
);
@@ -165,6 +206,14 @@ public final class ImageUtils {
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
}
private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);
// This is currently a simple check, more complex checks can be considered in the future
public static boolean isSVG(byte[] headerBuffer) {
return headerBuffer.length > SVG_HEADER.length
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
}
private static final byte[] PNG_HEADER = {
(byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47,
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
@@ -232,6 +281,8 @@ public final class ImageUtils {
return WEBP;
if (isApng(headerBuffer))
return APNG;
if (isSVG(headerBuffer))
return SVG;
return null;
}
@@ -406,6 +457,19 @@ public final class ImageUtils {
return new AnimationImageImpl(width, height, framePixels, durations, cycleCount);
}
private static int[] rgbaToArgb(ByteBuffer rgba) {
int pixelCount = rgba.remaining() / 4;
int[] argb = new int[pixelCount];
for (int i = 0; i < pixelCount; i++) {
int r = rgba.get() & 0xFF;
int g = rgba.get() & 0xFF;
int b = rgba.get() & 0xFF;
int a = rgba.get() & 0xFF;
argb[i] = (a << 24) | (r << 16) | (g << 8) | b;
}
return argb;
}
private ImageUtils() {
}
}
@@ -18,10 +18,8 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.*;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.theme.Themes;
@@ -30,6 +28,7 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
@@ -38,11 +37,18 @@ import java.io.InputStream;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class AboutPage extends StackPane {
public final class AboutPage extends SpinnerPane {
private final WeakListenerHolder holder = new WeakListenerHolder();
public AboutPage() {
VBox content = new VBox();
content.getStyleClass().add("spinner-pane-content");
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
setContent(scrollPane);
ComponentList about = new ComponentList();
{
var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
@@ -84,27 +90,16 @@ public final class AboutPage extends StackPane {
legal.getContent().setAll(copyright, claim, openSource);
}
VBox content = new VBox(16);
content.setPadding(new Insets(10));
content.getChildren().setAll(
ComponentList.createComponentListTitle(i18n("about")),
about,
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
thanks,
ComponentList.createComponentListTitle(i18n("about.dependency")),
deps,
ComponentList.createComponentListTitle(i18n("about.legal")),
legal
);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
private static Image loadImage(String url) {
@@ -85,7 +85,13 @@ public class DownloadSettingsPage extends StackPane {
downloadSourcePane.setItems(DownloadProviders.DIRECT_PROVIDERS.keySet());
downloadSourcePane.valueProperty().bindBidirectional(config().downloadTypeProperty());
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane);
var defaultAddonSourcePane = new LineSelectButton<String>();
defaultAddonSourcePane.setTitle(i18n("settings.launcher.default_addon_source"));
defaultAddonSourcePane.setConverter(key -> I18n.i18n("mods." + key));
defaultAddonSourcePane.setItems("modrinth", "curseforge");
defaultAddonSourcePane.valueProperty().bindBidirectional(config().defaultAddonSourceProperty());
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane, defaultAddonSourcePane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.main;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.theme.Themes;
@@ -37,9 +36,7 @@ public class FeedbackPage extends SpinnerPane {
public FeedbackPage() {
VBox content = new VBox();
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
content.getStyleClass().add("spinner-pane-content");
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
@@ -84,8 +81,5 @@ public class FeedbackPage extends SpinnerPane {
ComponentList.createComponentListTitle(i18n("contact.feedback")),
feedback
);
this.setContent(content);
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.annotations.SerializedName;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
@@ -42,9 +41,7 @@ public class HelpPage extends SpinnerPane {
public HelpPage() {
content = new VBox();
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
content.getStyleClass().add("spinner-pane-content");
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
@@ -47,6 +47,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Result;
@@ -233,7 +234,6 @@ public final class JavaDownloadDialog extends StackPane {
downloadButton.setOnAction(e -> onDownload());
downloadButton.getStyleClass().add("dialog-accept");
downloadButton.disableProperty().bind(Bindings.isNull(remoteVersionBox.getSelectionModel().selectedItemProperty()));
downloadButtonPane.getChildren().setAll(downloadButton);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
@@ -300,6 +300,11 @@ public final class JavaDownloadDialog extends StackPane {
remoteVersionBox.getSelectionModel().selectFirst();
});
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.getStyleClass().add("small-spinner-pane");
spinnerPane.setContent(downloadButton);
downloadButtonPane.getChildren().setAll(spinnerPane);
Consumer<DiscoJavaVersionList> updateListStatus = list -> {
remoteVersionBox.setItems(null);
packageTypeBox.getItems().clear();
@@ -308,11 +313,11 @@ public final class JavaDownloadDialog extends StackPane {
warningLabel.setText(null);
if (list == null || (list.versions != null && list.versions.isEmpty()))
downloadButtonPane.getChildren().setAll(downloadButton);
spinnerPane.hideSpinner();
else if (list.status == DiscoJavaVersionList.Status.LOADING)
downloadButtonPane.getChildren().setAll(new JFXSpinner());
spinnerPane.showSpinner();
else {
downloadButtonPane.getChildren().setAll(downloadButton);
spinnerPane.hideSpinner();
if (list.status == DiscoJavaVersionList.Status.SUCCESS) {
packageTypeBox.getItems().setAll(list.versions.keySet());
@@ -54,6 +54,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
@@ -98,7 +99,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
if (mainPage == null) {
MainPage mainPage = new MainPage();
FXUtils.applyDragListener(mainPage,
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file),
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file) || "json".equalsIgnoreCase(FileUtils.getExtension(file)),
modpacks -> {
Path file = modpacks.get(0);
if (ModpackHelper.isFileModpackByExtension(file)) {
@@ -113,6 +114,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
Versions.installFromJson(Profiles.getSelectedProfile(), file);
}
});
@@ -18,24 +18,24 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -46,6 +46,7 @@ import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.AprilFools;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
@@ -61,20 +62,16 @@ import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread;
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 final class SettingsPage extends ScrollPane {
@SuppressWarnings("FieldCanBeLocal")
private final ToggleGroup updateChannelGroup;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener updateListener;
@@ -121,82 +118,59 @@ public final class SettingsPage extends ScrollPane {
settingsPane.getContent().add(sponsorPane);
}
ObjectProperty<UpdateChannel> updateChannel;
{
ComponentSublist updatePane = new ComponentSublist();
JFXButton updateButton = new JFXButton();
updateButton.setGraphic(SVG.UPDATE.createIcon(20));
updateButton.setOnAction(e -> onUpdate());
updateButton.setPadding(Insets.EMPTY);
FXUtils.installFastTooltip(updateButton, i18n("update.tooltip"));
var updatePane = new LineSelectButton<UpdateChannel>() {
{
getStyleClass().add("update-pane");
setNode(IDX_TRAILING, updateButton);
}
@Override
protected int getTrailingTextIndex() {
return LineComponent.IDX_TRAILING + 1;
}
};
updateChannel = updatePane.valueProperty();
updatePane.setTitle(i18n("update"));
updatePane.setHasSubtitle(true);
updatePane.setValue(UpdateChannel.getChannel());
final Label lblUpdate;
final Label lblUpdateSub;
{
VBox headerLeft = new VBox();
updatePane.setConverter(channel -> i18n("update.channel." + channel.channelName));
updatePane.setItems(List.of(UpdateChannel.STABLE, UpdateChannel.DEVELOPMENT));
updatePane.setDescriptionConverter(channel -> i18n("update.note." + channel.channelName));
lblUpdate = new Label(i18n("update"));
lblUpdate.getStyleClass().add("title-label");
lblUpdateSub = new Label();
lblUpdateSub.getStyleClass().add("subtitle-label");
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
updatePane.setHeaderLeft(headerLeft);
}
final StringProperty lblUpdateSubProperty = updatePane.subtitleProperty();
{
JFXButton btnUpdate = FXUtils.newToggleButton4(SVG.UPDATE, 20);
btnUpdate.setOnAction(e -> onUpdate());
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
updateListener = any -> {
btnUpdate.setVisible(UpdateChecker.isOutdated());
boolean outdated = UpdateChecker.isOutdated();
updateButton.setVisible(outdated);
updateButton.setManaged(outdated);
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
if (UpdateChecker.isOutdated()) {
lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSub.setText(i18n("update.checking"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
lblUpdateSubProperty.set(i18n("update.checking"));
} else {
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
lblUpdateSubProperty.set(i18n("update.latest"));
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
updatePane.setHeaderRight(btnUpdate);
}
{
VBox content = new VBox(12);
content.setPadding(new Insets(8, 0, 0, 0));
updateChannelGroup = new ToggleGroup();
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
chkUpdateStable.setUserData(UpdateChannel.STABLE);
chkUpdateStable.setToggleGroup(updateChannelGroup);
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateDev.setToggleGroup(updateChannelGroup);
Label noteWrapper = new Label(i18n("update.note"));
VBox.setMargin(noteWrapper, new Insets(8, 0, 0, 0));
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
updatePane.getContent().add(content);
}
settingsPane.getContent().add(updatePane);
}
@@ -206,8 +180,6 @@ public final class SettingsPage extends ScrollPane {
previewPane.setSubtitle(i18n("update.preview.subtitle"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
updateChannel.set(UpdateChannel.getChannel());
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
@@ -288,6 +260,15 @@ public final class SettingsPage extends ScrollPane {
settingsPane.getContent().add(disableAutoGameOptionsPane);
}
{
LineToggleButton allowAutoAgentPane = new LineToggleButton();
allowAutoAgentPane.setTitle(i18n("settings.launcher.allow_auto_agent"));
allowAutoAgentPane.setSubtitle(i18n("settings.launcher.allow_auto_agent.subtitle"));
allowAutoAgentPane.selectedProperty().bindBidirectional(config().allowAutoAgentProperty());
settingsPane.getContent().add(allowAutoAgentPane);
}
{
BorderPane debugPane = new BorderPane();
@@ -301,12 +282,31 @@ public final class SettingsPage extends ScrollPane {
if (LOG.getLogFile() == null)
openLogFolderButton.setDisable(true);
SpinnerPane exportLogPane = new SpinnerPane();
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
logButton.setOnAction(e -> onExportLogs());
exportLogPane.setContent(logButton);
logButton.setOnAction(e -> {
exportLogPane.showSpinner();
onExportLogs().whenCompleteAsync((result, exception) -> {
exportLogPane.hideSpinner();
if (exception == null) {
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
FXUtils.showFileInExplorer(result);
} else {
LOG.warning("Failed to export logs", exception);
Controllers.dialog(
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
null,
MessageType.ERROR
);
}
}, Schedulers.javafx());
});
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, logButton);
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
@@ -378,86 +378,79 @@ public final class SettingsPage extends ScrollPane {
}
}
private void onExportLogs() {
thread(() -> {
private CompletableFuture<Path> onExportLogs() {
return CompletableFuture.supplyAsync(Lang.wrap(() -> {
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);
Path outputFile;
try {
if (recentLogFiles.isEmpty()) {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
if (recentLogFiles.isEmpty()) {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
LOG.info("Exporting latest logs to " + outputFile);
try (OutputStream output = Files.newOutputStream(outputFile)) {
LOG.exportLogs(output);
}
} else {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
LOG.info("Exporting latest logs to " + outputFile);
try (OutputStream output = Files.newOutputStream(outputFile)) {
LOG.exportLogs(output);
}
} else {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
LOG.info("Exporting latest logs to " + outputFile);
LOG.info("Exporting latest logs to " + outputFile);
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
try (var os = Files.newOutputStream(outputFile);
var zos = new ZipOutputStream(os)) {
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
try (var os = Files.newOutputStream(outputFile);
var zos = new ZipOutputStream(os)) {
Set<String> entryNames = new HashSet<>();
Set<String> entryNames = new HashSet<>();
for (Path path : recentLogFiles) {
String fileName = FileUtils.getName(path);
String extension = StringUtils.substringAfterLast(fileName, '.');
for (Path path : recentLogFiles) {
String fileName = FileUtils.getName(path);
String extension = StringUtils.substringAfterLast(fileName, '.');
if ("gz".equals(extension) || "xz".equals(extension)) {
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed,
// then copy the compressed file content as-is into a new entry in the zip file.
InputStream input = null;
try {
input = Files.newInputStream(path);
input = "gz".equals(extension)
? new GZIPInputStream(input)
: new XZInputStream(input);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
IOUtils.closeQuietly(input, ex);
input = null;
}
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
continue;
}
// Copy the log file content as-is into a new entry in the zip file.
if ("gz".equals(extension) || "xz".equals(extension)) {
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed.
// ensure the input file and the current zip entry are closed,
// then copy the compressed file content as-is into a new entry in the zip file.
InputStream input;
InputStream input = null;
try {
input = Files.newInputStream(path);
input = "gz".equals(extension)
? new GZIPInputStream(input)
: new XZInputStream(input);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
continue;
IOUtils.closeQuietly(input, ex);
input = null;
}
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
continue;
}
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
LOG.exportLogs(zos);
zos.closeEntry();
// Copy the log file content as-is into a new entry in the zip file.
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed.
InputStream input;
try {
input = Files.newInputStream(path);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
continue;
}
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
}
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
LOG.exportLogs(zos);
zos.closeEntry();
}
} catch (IOException e) {
LOG.warning("Failed to export logs", e);
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
return;
}
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile)));
FXUtils.showFileInExplorer(outputFile);
});
return outputFile;
}), Schedulers.io());
}
private void onSponsor() {
@@ -22,8 +22,14 @@ import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.skin.TreeViewSkin;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.tag.Tag;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -33,6 +39,7 @@ import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Path;
@@ -56,8 +63,9 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
this.state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("nbt.title", file.toString())));
this.file = file;
this.type = NBTFileType.ofFile(file);
//noinspection DataFlowIssue
this.type = NBTFileType.ofFile(file);
if (type == null) {
throw new IOException("Unknown type of file " + file);
}
@@ -85,14 +93,33 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
actions.getChildren().setAll(saveButton, cancelButton);
Task.supplyAsync(() -> type.readAsTree(file))
Task.supplyAsync(() -> type.read(file))
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
setLoading(false);
NBTTreeView view = new NBTTreeView(result);
NBTTreeItem root = new NBTTreeItem(result, FileUtils.getName(file));
var view = new TreeView<>(root) {
@Override
protected Skin<?> createDefaultSkin() {
return new TreeViewSkin<>(this) {
{
FXUtils.smoothScrolling(getVirtualFlow());
}
};
}
};
view.setCellFactory(ignored -> new NBTTreeCell());
view.addEventHandler(TreeItem.<NBTElement>branchExpandedEvent(), event -> {
TreeItem<NBTElement> item = event.getTreeItem();
if (item.getValue() instanceof Tag && item.getChildren().size() == 1)
item.getChildren().get(0).setExpanded(true);
});
root.setExpanded(true);
BorderPane.setMargin(view, new Insets(10));
onEscPressed(view, cancelButton::fire);
root.setCenter(view);
this.root.setCenter(view);
} else {
LOG.warning("Fail to open nbt file", exception);
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(exception), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire);
@@ -17,148 +17,34 @@
*/
package org.jackhuang.hmcl.ui.nbt;
import com.github.steveice10.opennbt.NBTIO;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import kala.compress.utils.BoundedInputStream;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.chunk.ChunkRegion;
import org.glavo.nbt.io.NBTCodec;
import org.glavo.nbt.tag.Tag;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
/**
* @author Glavo
*/
/// @author Glavo
public enum NBTFileType {
COMPRESSED("dat", "dat_old") {
@Override
public Tag read(Path file) throws IOException {
try (BufferedInputStream fileInputStream = new BufferedInputStream(Files.newInputStream(file))) {
fileInputStream.mark(3);
byte[] header = new byte[3];
if (fileInputStream.read(header) < 3) {
throw new IOException("File is too small");
}
fileInputStream.reset();
InputStream input;
if (Arrays.equals(header, new byte[]{0x1f, (byte) 0x8b, 0x08})) {
input = new GZIPInputStream(fileInputStream);
} else {
input = fileInputStream;
}
Tag tag = NBTIO.readTag(input);
if (!(tag instanceof CompoundTag))
throw new IOException("Unexpected tag: " + tag);
return tag;
}
return NBTCodec.of().readTag(file);
}
},
ANVIL("mca") {
@Override
public Tag read(Path file) throws IOException {
return REGION.read(file);
}
@Override
public NBTTreeView.Item readAsTree(Path file) throws IOException {
return REGION.readAsTree(file);
public NBTElement read(Path file) throws IOException {
return NBTCodec.of().readRegion(file);
}
},
REGION("mcr") {
@Override
public Tag read(Path file) throws IOException {
try (RandomAccessFile r = new RandomAccessFile(file.toFile(), "r")) {
ListTag tag = new ListTag(file.getFileName().toString(), CompoundTag.class);
if (r.length() == 0) {
return tag;
}
byte[] header = new byte[4096];
byte[] buffer = new byte[1 * 1024 * 1024]; // The maximum size of each chunk is 1MiB
Inflater inflater = new Inflater();
r.readFully(header);
for (int i = 0; i < 4096; i += 4) {
int offset = ((header[i] & 0xff) << 16) + ((header[i + 1] & 0xff) << 8) + (header[i + 2] & 0xff);
int length = header[i + 3] & 0xff;
if (offset == 0 || length == 0) {
continue;
}
r.seek(offset * 4096L);
r.readFully(buffer, 0, length * 4096);
int chunkLength = ((buffer[0] & 0xff) << 24) + ((buffer[1] & 0xff) << 16) + ((buffer[2] & 0xff) << 8) + (buffer[3] & 0xff);
InputStream input = new ByteArrayInputStream(buffer);
input.skip(5);
input = new BoundedInputStream(input, chunkLength - 1);
switch (buffer[4]) {
case 0x01:
// GZip
input = new GZIPInputStream(input);
break;
case 0x02:
// Zlib
inflater.reset();
input = new InflaterInputStream(input, inflater);
break;
case 0x03:
// Uncompressed
break;
default:
throw new IOException("Unsupported compression method: " + Integer.toHexString(buffer[4] & 0xff));
}
try (InputStream in = input) {
Tag chunk = NBTIO.readTag(in);
if (!(chunk instanceof CompoundTag))
throw new IOException("Unexpected tag: " + chunk);
tag.add(chunk);
}
}
return tag;
}
}
@Override
public NBTTreeView.Item readAsTree(Path file) throws IOException {
NBTTreeView.Item item = new NBTTreeView.Item(read(file));
for (Tag tag : ((ListTag) item.getValue())) {
CompoundTag chunk = (CompoundTag) tag;
NBTTreeView.Item tree = NBTTreeView.buildTree(chunk);
Tag xPos = chunk.get("xPos");
Tag zPos = chunk.get("zPos");
if (xPos instanceof IntTag && zPos instanceof IntTag) {
tree.setText(String.format("Chunk: %d %d", xPos.getValue(), zPos.getValue()));
} else {
tree.setText("Chunk: Unknown");
}
item.getChildren().add(tree);
}
return item;
public ChunkRegion read(Path file) throws IOException {
return NBTCodec.of().readRegion(file);
}
};
@@ -168,7 +54,7 @@ public enum NBTFileType {
return NBTFileType.ofFile(file) != null;
}
public static NBTFileType ofFile(Path file) {
public static @Nullable NBTFileType ofFile(Path file) {
String ext = FileUtils.getExtension(file);
for (NBTFileType type : types) {
for (String extension : type.extensions) {
@@ -186,11 +72,6 @@ public enum NBTFileType {
this.extensions = extensions;
}
public abstract Tag read(Path file) throws IOException;
public abstract NBTElement read(Path file) throws IOException;
public NBTTreeView.Item readAsTree(Path file) throws IOException {
NBTTreeView.Item root = NBTTreeView.buildTree(read(file));
root.setName(file.getFileName().toString());
return root;
}
}
@@ -1,79 +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.ui.nbt;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* @author Glavo
*/
public enum NBTTagType {
BYTE, SHORT, INT, LONG, FLOAT, DOUBLE,
BYTE_ARRAY, INT_ARRAY, LONG_ARRAY,
STRING,
LIST, COMPOUND;
private static final Map<String, NBTTagType> lookupTable = new HashMap<>();
static {
for (NBTTagType type : values()) {
lookupTable.put(type.getTagClassName(), type);
}
}
public static NBTTagType typeOf(Tag tag) {
NBTTagType type = lookupTable.get(tag.getClass().getSimpleName());
if (type == null) {
throw new IllegalArgumentException("Unknown tag: " + type);
}
return type;
}
private final String iconUrl;
private final String tagClassName;
NBTTagType() {
String tagName;
String className;
int idx = name().indexOf('_');
if (idx < 0) {
tagName = name().charAt(0) + name().substring(1).toLowerCase(Locale.ROOT);
className = tagName + "Tag";
} else {
tagName = name().charAt(0) + name().substring(1, idx + 1).toLowerCase(Locale.ROOT)
+ name().charAt(idx + 1) + name().substring(idx + 2).toLowerCase(Locale.ROOT);
className = tagName.substring(0, idx) + tagName.substring(idx + 1) + "Tag";
}
this.iconUrl = "/assets/img/nbt/TAG_" + tagName + ".png";
this.tagClassName = className;
}
public String getIconUrl() {
return iconUrl;
}
public String getTagClassName() {
return tagClassName;
}
}
@@ -0,0 +1,129 @@
/*
* Hello Minecraft! Launcher
* 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
* 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.nbt;
import javafx.scene.control.TreeCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.chunk.Chunk;
import org.glavo.nbt.chunk.ChunkRegion;
import org.glavo.nbt.tag.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class NBTTreeCell extends TreeCell<@Nullable NBTElement> {
private static @Nullable Image getIcon(NBTElement element) {
if (element instanceof Tag tag) {
TagType<?> type = tag.getType();
if (type == TagType.BYTE) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte.png");
} else if (type == TagType.SHORT) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Short.png");
} else if (type == TagType.INT) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int.png");
} else if (type == TagType.LONG) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long.png");
} else if (type == TagType.FLOAT) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Float.png");
} else if (type == TagType.DOUBLE) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Double.png");
} else if (type == TagType.STRING) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_String.png");
} else if (type == TagType.BYTE_ARRAY) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte_Array.png");
} else if (type == TagType.INT_ARRAY) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int_Array.png");
} else if (type == TagType.LONG_ARRAY) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long_Array.png");
} else if (type == TagType.LIST) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png");
} else if (type == TagType.COMPOUND) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png");
} else {
return null;
}
} else if (element instanceof ChunkRegion)
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png");
else if (element instanceof Chunk)
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png");
else
return null;
}
private final ImageView imageView;
public NBTTreeCell() {
imageView = new ImageView();
imageView.setFitHeight(16);
imageView.setFitWidth(16);
setGraphic(imageView);
}
private NBTTreeItem getNBTTreeItem() {
return (NBTTreeItem) getTreeItem();
}
@Override
public void updateItem(@Nullable NBTElement item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
imageView.setImage(null);
setText(null);
return;
}
imageView.setImage(getIcon(item));
String name = getNBTTreeItem().getOverrideName();
if (name == null) {
NBTElement value = getNBTTreeItem().getValue();
if (value instanceof Tag tag) {
if (tag.getParent() instanceof ListTag<?>) {
name = Integer.toString(tag.getIndex());
} else {
name = tag.getName();
}
} else if (value instanceof Chunk chunk) {
name = "Chunk (" + chunk.getLocalX() + ", " + chunk.getLocalZ() + ")";
} else {
name = "";
}
}
String text;
if (item instanceof ParentTag<?> parentTag) {
text = i18n("nbt.entries", parentTag.size());
} else if (item instanceof ValueTag<?> valueTag) {
text = valueTag.getAsString();
} else {
text = null;
}
if (text == null) {
setText(name);
} else {
setText(name + ": " + text);
}
}
}
@@ -0,0 +1,68 @@
/*
* Hello Minecraft! Launcher
* 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
* 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.nbt;
import javafx.collections.ObservableList;
import javafx.scene.control.TreeItem;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.NBTParent;
import org.glavo.nbt.chunk.Chunk;
import org.glavo.nbt.tag.*;
import org.jetbrains.annotations.Nullable;
/// @author Glavo
public final class NBTTreeItem extends TreeItem<NBTElement> {
private final @Nullable String overrideName;
public NBTTreeItem(NBTElement value) {
this(value, null);
}
public NBTTreeItem(NBTElement value, @Nullable String overrideName) {
super(value);
this.overrideName = overrideName;
}
@Override
public boolean isLeaf() {
return !(getValue() instanceof NBTParent<?> parent) || parent.isEmpty();
}
private boolean needInitChildren = true;
@Override
public ObservableList<TreeItem<NBTElement>> getChildren() {
ObservableList<TreeItem<NBTElement>> children = super.getChildren();
if (needInitChildren) {
needInitChildren = false;
if (getValue() instanceof Chunk chunk) {
if (chunk.getRootTag() != null) {
children.setAll(chunk.getRootTag().stream().map(NBTTreeItem::new).toList());
}
} else if (getValue() instanceof NBTParent<?> parent) {
children.setAll(parent.stream().map(NBTTreeItem::new).toList());
}
}
return children;
}
public @Nullable String getOverrideName() {
return overrideName;
}
}
@@ -1,184 +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.ui.nbt;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.skin.TreeViewSkin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Callback;
import org.jackhuang.hmcl.ui.FXUtils;
import java.lang.reflect.Array;
import java.util.EnumMap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Glavo
*/
public final class NBTTreeView extends TreeView<Tag> {
public NBTTreeView(NBTTreeView.Item tree) {
this.setRoot(tree);
if (tree != null) tree.setExpanded(true);
this.setCellFactory(cellFactory());
}
@Override
protected javafx.scene.control.Skin<?> createDefaultSkin() {
return new TreeViewSkin<Tag>(this) {
{
FXUtils.smoothScrolling(getVirtualFlow());
}
};
}
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
return view -> new TreeCell<>() {
private void setTagText(String text) {
String name = ((Item) getTreeItem()).getName();
if (name == null) {
setText(text);
} else if (text == null) {
setText(name);
} else {
setText(name + ": " + text);
}
}
private void setTagText(int nEntries) {
setTagText(i18n("nbt.entries", nEntries));
}
@Override
public void updateItem(Tag item, boolean empty) {
super.updateItem(item, empty);
ImageView imageView = (ImageView) this.getGraphic();
if (imageView == null) {
imageView = new ImageView();
this.setGraphic(imageView);
}
if (item == null) {
imageView.setImage(null);
setText(null);
return;
}
NBTTagType tagType = NBTTagType.typeOf(item);
imageView.setImage(icons.computeIfAbsent(tagType, type -> new Image(type.getIconUrl())));
imageView.setFitHeight(16);
imageView.setFitWidth(16);
if (((Item) getTreeItem()).getText() != null) {
setText(((Item) getTreeItem()).getText());
} else {
switch (tagType) {
case BYTE:
case SHORT:
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case STRING:
setTagText(item.getValue().toString());
break;
case BYTE_ARRAY:
case INT_ARRAY:
case LONG_ARRAY:
setTagText(Array.getLength(item.getValue()));
break;
case LIST:
setTagText(((ListTag) item).size());
break;
case COMPOUND:
setTagText(((CompoundTag) item).size());
break;
default:
setTagText(null);
}
}
}
};
}
public static Item buildTree(Tag tag) {
Item item = new Item(tag);
if (tag instanceof CompoundTag) {
for (Tag subTag : ((CompoundTag) tag)) {
item.getChildren().add(buildTree(subTag));
}
} else if (tag instanceof ListTag) {
int idx = 0;
for (Tag subTag : ((ListTag) tag)) {
Item subTree = buildTree(subTag);
subTree.setName(String.valueOf(idx++));
item.getChildren().add(subTree);
}
}
FXUtils.onChangeAndOperate(item.expandedProperty(), expanded -> {
if (expanded && item.getChildren().size() == 1) item.getChildren().get(0).setExpanded(true);
});
return item;
}
public CompoundTag getRootTag() {
return ((CompoundTag) getRoot().getValue());
}
public static class Item extends TreeItem<Tag> {
private String text;
private String name;
public Item() {
}
public Item(Tag value) {
super(value);
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name == null ? getValue().getName() : name;
}
}
}
@@ -0,0 +1,22 @@
/*
* Hello Minecraft! Launcher
* 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
* 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/>.
*/
@NotNullByDefault
package org.jackhuang.hmcl.ui.nbt;
import org.jetbrains.annotations.NotNullByDefault;
@@ -33,7 +33,7 @@ public class SkinCube extends MeshView {
public static float[] createTexCoords(float width, float height, float depth, float scaleX, float scaleY,
float startX, float startY, boolean isSlim) {
float x = (width + depth) * 2, y = height + depth, half_width = width / x * scaleX, half_depth = depth / x * scaleX,
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = isSlim ? half_depth : half_width,
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = half_width,
bottom_x = startX, middle_y = depth / y * scaleY + top_y, bottom_y = scaleY + top_y;
return new float[]{
top_x, top_y, // T0 ---
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* 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
* 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.versions;
import com.jfoenix.controls.JFXTextField;
@@ -8,6 +25,7 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.GraphicsAPI;
import org.jackhuang.hmcl.game.NativesDirectoryType;
import org.jackhuang.hmcl.game.Renderer;
import org.jackhuang.hmcl.setting.Profile;
@@ -19,6 +37,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.nio.file.FileSystems;
@@ -52,6 +71,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
private final ComponentSublist nativesDirSublist;
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
private final LineSelectButton<GraphicsAPI> graphicsBackendPane;
private final LineSelectButton<Renderer> rendererPane;
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
@@ -62,6 +82,10 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
versionId == null ? i18n("settings.advanced") : i18n("settings.advanced.title", versionId)
));
@Nullable GameVersionNumber gameVersion = versionId != null
? GameVersionNumber.asGameVersion(profile.getRepository().getGameVersion(versionId))
: null;
this.getStyleClass().add("gray-background");
ScrollPane scrollPane = new ScrollPane();
@@ -164,9 +188,25 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
new MultiFileItem.Option<>(i18n("settings.advanced.natives_directory.default"), NativesDirectoryType.VERSION_FOLDER),
nativesDirCustomOption
));
HintPane nativesDirHint = new HintPane(MessageDialogPane.MessageType.WARNING);
nativesDirHint.setText(i18n("settings.advanced.natives_directory.hint"));
nativesDirItem.getChildren().add(nativesDirHint);
graphicsBackendPane = new LineSelectButton<>();
graphicsBackendPane.setTitle(i18n("settings.advanced.graphics_backend"));
graphicsBackendPane.setConverter(backend -> i18n("settings.advanced.graphics_backend." + backend.name().toLowerCase(Locale.ROOT)));
graphicsBackendPane.setDescriptionConverter(backend -> switch (backend) {
case DEFAULT -> i18n("settings.advanced.graphics_backend.default.desc");
case OPENGL -> i18n("settings.advanced.graphics_backend.opengl.desc");
case VULKAN -> {
if (gameVersion == null)
yield i18n("settings.advanced.graphics_backend.vulkan.desc.global");
else if (gameVersion.compareTo("26.2-snapshot-2") < 0)
yield i18n("settings.advanced.graphics_backend.vulkan.desc.unsupported");
else
yield i18n("settings.advanced.graphics_backend.vulkan.desc");
}
default -> null;
});
graphicsBackendPane.setValue(GraphicsAPI.DEFAULT);
graphicsBackendPane.setItems(GraphicsAPI.values());
rendererPane = new LineSelectButton<>();
rendererPane.setTitle(i18n("settings.advanced.renderer"));
@@ -175,7 +215,23 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
String bundleKey = "settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT) + ".desc";
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
});
rendererPane.setItems(Renderer.values());
rendererPane.setValue(Renderer.DEFAULT);
FXUtils.onChangeAndOperate(graphicsBackendPane.valueProperty(), backend -> {
if (backend == null) { // unbind
return;
}
rendererPane.setItems(Renderer.getSupported(backend));
if (backend == GraphicsAPI.DEFAULT) {
rendererPane.setDisable(true);
rendererPane.setValue(Renderer.DEFAULT);
} else {
rendererPane.setDisable(false);
if (!(rendererPane.getValue() instanceof Renderer.Driver driver) || driver.api() != backend)
rendererPane.setValue(Renderer.DEFAULT);
}
});
noJVMArgsPane = new LineToggleButton();
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
@@ -195,12 +251,14 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
useNativeGLFWPane = new LineToggleButton();
useNativeGLFWPane.setTitle(i18n("settings.advanced.use_native_glfw"));
useNativeGLFWPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
useNativeOpenALPane = new LineToggleButton();
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
useNativeOpenALPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
workaroundPane.getContent().setAll(
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
nativesDirSublist, graphicsBackendPane, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
noJVMCheckPane, noNativesPatchPane
);
@@ -233,6 +291,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
FXUtils.bindString(txtPostExitCommand, versionSetting.postExitCommandProperty());
rendererPane.valueProperty().bindBidirectional(versionSetting.rendererProperty());
graphicsBackendPane.valueProperty().bindBidirectional(versionSetting.graphicsBackendProperty());
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
@@ -275,6 +334,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty());
rendererPane.valueProperty().unbindBidirectional(versionSetting.rendererProperty());
graphicsBackendPane.valueProperty().unbindBidirectional(versionSetting.graphicsBackendProperty());
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
@@ -22,7 +22,7 @@ import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.mod.DataPack;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -44,45 +44,45 @@ import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements WorldManagePage.WorldRefreshable {
public final class DataPackListPage extends ListPageBase<DataPackListPageSkin.DataPackInfoObject> implements WorldManagePage.WorldRefreshable {
private final World world;
private final Datapack datapack;
private final DataPack dataPack;
final BooleanProperty readOnly;
public DatapackListPage(WorldManagePage worldManagePage) {
public DataPackListPage(WorldManagePage worldManagePage) {
world = worldManagePage.getWorld();
datapack = new Datapack(world.getFile().resolve("datapacks"));
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
dataPack = new DataPack(world.getFile().resolve("datapacks"));
setItems(MappedObservableList.create(dataPack.getPacks(), DataPackListPageSkin.DataPackInfoObject::new));
readOnly = worldManagePage.readOnlyProperty();
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
this::installMultiDatapack, this::refresh);
this::installMultiDataPack, this::refresh);
refresh();
}
private void installMultiDatapack(List<Path> datapackPath) {
datapackPath.forEach(this::installSingleDatapack);
private void installMultiDataPack(List<Path> dataPackPath) {
dataPackPath.forEach(this::installSingleDataPack);
if (readOnly.get()) {
Controllers.showToast(i18n("datapack.reload.toast"));
}
}
private void installSingleDatapack(Path datapack) {
private void installSingleDataPack(Path dataPack) {
try {
this.datapack.installPack(datapack, world.getGameVersion());
this.dataPack.installPack(dataPack, world.getGameVersion());
} catch (IOException | IllegalArgumentException e) {
LOG.warning("Unable to parse datapack file " + datapack, e);
LOG.warning("Unable to parse datapack file " + dataPack, e);
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new DatapackListPageSkin(this);
return new DataPackListPageSkin(this);
}
public void refresh() {
setLoading(true);
Task.runAsync(datapack::loadFromDir)
Task.runAsync(dataPack::loadFromDir)
.withRunAsync(Schedulers.javafx(), () -> setLoading(false))
.start();
}
@@ -94,18 +94,18 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res != null) {
installMultiDatapack(res);
installMultiDataPack(res);
}
datapack.loadFromDir();
dataPack.loadFromDir();
}
void removeSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
void removeSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
selectedItems.stream()
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
.forEach(pack -> {
try {
datapack.deletePack(pack);
dataPack.deletePack(pack);
} catch (IOException e) {
// Fail to remove mods if the game is running or the datapack is absent.
LOG.warning("Failed to delete datapack \"" + pack.getId() + "\"", e);
@@ -113,23 +113,23 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
});
}
void enableSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
void enableSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
selectedItems.stream()
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
.forEach(pack -> pack.setActive(true));
}
void disableSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
void disableSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
selectedItems.stream()
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
.forEach(pack -> pack.setActive(false));
}
void openDataPackFolder() {
FXUtils.openFolder(datapack.getPath());
FXUtils.openFolder(dataPack.getPath());
}
@NotNull Predicate<DatapackListPageSkin.DatapackInfoObject> updateSearchPredicate(String queryString) {
@NotNull Predicate<DataPackListPageSkin.DataPackInfoObject> updateSearchPredicate(String queryString) {
if (queryString.isBlank()) {
return dataPack -> true;
}
@@ -28,6 +28,7 @@ import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -42,7 +43,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.mod.DataPack;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -67,7 +68,7 @@ import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
private final TransitionPane toolbarPane;
private final HBox searchBar;
@@ -75,8 +76,8 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
private final HBox selectingToolbar;
InvalidationListener updateBarByStateWeakListener;
private final JFXListView<DatapackInfoObject> listView;
private final FilteredList<DatapackInfoObject> filteredList;
private final JFXListView<DataPackInfoObject> listView;
private final FilteredList<DataPackInfoObject> filteredList;
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
private final BooleanProperty isSelecting = new SimpleBooleanProperty(false);
@@ -85,7 +86,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
private static final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1);
final Consumer<Integer> toggleSelect;
DatapackListPageSkin(DatapackListPage skinnable) {
DataPackListPageSkin(DataPackListPage skinnable) {
super(skinnable);
StackPane pane = new StackPane();
@@ -97,6 +98,14 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
listView = new JFXListView<>();
filteredList = new FilteredList<>(skinnable.getItems());
// reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
var selectAllButton = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
ListChangeListener<Object> listener = change -> {
selectAllButton.setDisable(!listView.getItems().isEmpty()
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
};
{
toolbarPane = new TransitionPane();
searchBar = new HBox();
@@ -123,12 +132,13 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
enableButton.disableProperty().bind(getSkinnable().readOnly);
disableButton.disableProperty().bind(getSkinnable().readOnly);
listView.getSelectionModel().getSelectedItems().addListener(listener);
selectingToolbar.getChildren().addAll(
removeButton,
enableButton,
disableButton,
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
selectAllButton,
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
listView.getSelectionModel().clearSelection())
);
@@ -181,9 +191,10 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
ComponentList.setVgrow(center, Priority.ALWAYS);
center.loadingProperty().bind(skinnable.loadingProperty());
listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().readOnly));
listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
this.listView.setItems(filteredList);
listView.getItems().addListener(listener);
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
@@ -216,13 +227,13 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
}
static class DatapackInfoObject extends RecursiveTreeObject<DatapackInfoObject> {
static class DataPackInfoObject extends RecursiveTreeObject<DataPackInfoObject> {
private final BooleanProperty activeProperty;
private final Datapack.Pack packInfo;
private final DataPack.Pack packInfo;
private SoftReference<CompletableFuture<Image>> iconCache;
DatapackInfoObject(Datapack.Pack packInfo) {
DataPackInfoObject(DataPack.Pack packInfo) {
this.packInfo = packInfo;
this.activeProperty = packInfo.activeProperty();
}
@@ -235,7 +246,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
return packInfo.getDescription().toString();
}
Datapack.Pack getPackInfo() {
DataPack.Pack getPackInfo() {
return packInfo;
}
@@ -270,7 +281,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
}
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DatapackInfoObject>> current) {
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DataPackInfoObject>> current) {
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
CompletableFuture<Image> imageFuture;
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
@@ -286,7 +297,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
imageFuture.thenAcceptAsync(image -> {
if (current != null) {
ObjectProperty<DatapackInfoObject> infoObjectProperty = current.get();
ObjectProperty<DataPackInfoObject> infoObjectProperty = current.get();
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
// The current ListCell has already switched to another object
return;
@@ -298,13 +309,13 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
}
private final class DatapackInfoListCell extends MDListCell<DatapackInfoObject> {
private final class DataPackInfoListCell extends MDListCell<DataPackInfoObject> {
final JFXCheckBox checkBox = new JFXCheckBox();
ImageContainer imageContainer = new ImageContainer(32);
final TwoLineListItem content = new TwoLineListItem();
BooleanProperty booleanProperty;
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
DataPackInfoListCell(JFXListView<DataPackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
super(listView);
HBox container = new HBox(8);
@@ -326,7 +337,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
@Override
protected void updateControl(DatapackInfoObject dataItem, boolean empty) {
protected void updateControl(DataPackInfoObject dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getTitle());
content.setSubtitle(dataItem.getSubtitle());
@@ -338,7 +349,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
}
public void handleSelect(DatapackInfoListCell cell, MouseEvent mouseEvent) {
public void handleSelect(DataPackInfoListCell cell, MouseEvent mouseEvent) {
if (cell.isEmpty()) {
mouseEvent.consume();
return;
@@ -531,7 +531,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
listView.setCellFactory(x -> new ListCell<>() {
private static final Insets PADDING = new Insets(9, 9, 0, 9);
@@ -578,7 +577,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
} else {
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(item.getSlug());
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : item.getTitle());
content.setSubtitle(item.getDescription());
String description = item.getDescription();
if (description != null) {
description = description.replaceAll("\\R", " ");
}
content.setSubtitle(description);
content.getTags().clear();
for (String category : item.getCategories()) {
if (getSkinnable().shouldDisplayCategory(category))
@@ -27,16 +27,8 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
@@ -54,11 +46,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@@ -66,14 +54,7 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -244,8 +225,10 @@ public class DownloadPage extends Control implements DecoratorPage {
content.setSubtitle(getSkinnable().addon.getDescription());
content.getSubtitleLabel().setWrapText(true);
getSkinnable().addon.getCategories().stream()
.filter(category -> getSkinnable().page.shouldDisplayCategory(category))
.map(category -> getSkinnable().page.getLocalizedCategory(category))
.forEach(content::addTag);
content.getFirstLine().setMinWidth(0);
descriptionPane.getChildren().add(content);
if (getSkinnable().mod != null) {
@@ -292,7 +275,7 @@ public class DownloadPage extends Control implements DecoratorPage {
Version game = repository.getResolvedPreservingPatchesVersion(control.version.getVersion());
String gameVersion = repository.getGameVersion(game).orElse(null);
if (gameVersion != null) {
if (gameVersion != null && control.versions.containsKey(gameVersion)) {
List<RemoteMod.Version> modVersions = control.versions.get(gameVersion);
if (modVersions != null && !modVersions.isEmpty()) {
Set<ModLoaderType> targetLoaders = LibraryAnalyzer.analyze(game, gameVersion).getModLoaders();
@@ -313,25 +296,55 @@ public class DownloadPage extends Control implements DecoratorPage {
}
}
for (String gameVersion : control.versions.keys().stream()
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
.toList()) {
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
if (versions == null || versions.isEmpty()) {
continue;
final class Versions {
static ComponentSublist createSublist(DownloadPage control, String title, List<String> target) {
var sublist = new ComponentSublist(() -> {
ArrayList<ModItem> items = new ArrayList<>();
for (String gv : target) {
List<RemoteMod.Version> versions = control.versions.get(gv);
if (versions != null) {
for (RemoteMod.Version v : versions) {
items.add(new ModItem(control.addon, v, control));
}
}
}
return items;
});
sublist.setTitle(title);
sublist.getStyleClass().add("no-padding");
return sublist;
}
var sublist = new ComponentSublist(() -> {
ArrayList<ModItem> items = new ArrayList<>(versions.size());
for (RemoteMod.Version v : versions) {
items.add(new ModItem(control.addon, v, control));
}
return items;
});
sublist.getStyleClass().add("no-padding");
sublist.setTitle("Minecraft " + gameVersion);
final List<String> releases = new ArrayList<>();
final List<String> snapshots = new ArrayList<>();
}
list.getContent().add(sublist);
TreeMap<GameVersionNumber, Versions> map = new TreeMap<>(Collections.reverseOrder());
for (String version : control.versions.keys()) {
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(version);
GameVersionNumber releaseOfSnapshot = GameVersionNumber.getReleaseOfSnapshot(gameVersion);
GameVersionNumber releaseVersion = Objects.requireNonNullElse(releaseOfSnapshot, gameVersion);
Versions lists = map.computeIfAbsent(releaseVersion, v -> new Versions());
if (releaseOfSnapshot == null) {
lists.releases.add(version);
} else {
lists.snapshots.add(version);
}
}
for (Map.Entry<GameVersionNumber, Versions> entry : map.entrySet()) {
GameVersionNumber gameVersion = entry.getKey();
Versions versions = entry.getValue();
if (!versions.releases.isEmpty())
list.getContent().add(Versions.createSublist(control, i18n("addon.download.title.release", gameVersion), versions.releases));
if (!versions.snapshots.isEmpty())
list.getContent().add(Versions.createSublist(control, i18n("addon.download.title.snapshot", gameVersion), versions.snapshots));
}
});
}
@@ -340,7 +353,7 @@ public class DownloadPage extends Control implements DecoratorPage {
}
}
private static final class DependencyModItem extends StackPane {
private static final class DependencyModItem extends LineButton {
public static final EnumMap<RemoteMod.DependencyType, String> I18N_KEY = new EnumMap<>(Lang.mapOf(
Pair.pair(RemoteMod.DependencyType.EMBEDDED, "mods.dependency.embedded"),
Pair.pair(RemoteMod.DependencyType.OPTIONAL, "mods.dependency.optional"),
@@ -356,16 +369,16 @@ public class DownloadPage extends Control implements DecoratorPage {
pane.setPadding(new Insets(0, 8, 0, 8));
pane.setAlignment(Pos.CENTER_LEFT);
TwoLineListItem content = new TwoLineListItem();
pane.setMouseTransparent(true);
HBox.setHgrow(content, Priority.ALWAYS);
var imageView = new ImageContainer(40);
pane.getChildren().setAll(imageView, content);
RipplerContainer container = new RipplerContainer(pane);
FXUtils.onClicked(container, () -> {
FXUtils.setLimitHeight(this, 60);
setOnAction((e) -> {
fireEvent(new DialogCloseEvent());
Controllers.navigate(new DownloadPage(page, addon, version, callback));
});
getChildren().setAll(container);
setNode(IDX_LEADING, pane);
if (addon != RemoteMod.BROKEN) {
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
@@ -482,6 +495,7 @@ public class DownloadPage extends Control implements DecoratorPage {
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setContent(dependenciesList);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import java.util.MissingResourceException;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -61,13 +62,30 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
repository = new Repository(type, curseForge, modrinth);
supportChinese.set(true);
downloadSources.get().setAll("mods.modrinth", "mods.curseforge");
if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else if (curseForge != null) {
downloadSource.set("mods.curseforge");
boolean supportedCurseForge = CurseForgeRemoteModRepository.isAvailable() && curseForge != null;
downloadSources.setAll("mods.modrinth");
if (supportedCurseForge) {
downloadSources.add("mods.curseforge");
}
if ("curseforge".equalsIgnoreCase(config().getDefaultAddonSource())) {
if (supportedCurseForge) {
downloadSource.set("mods.curseforge");
} else if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else {
throw new AssertionError("Should not be here.");
}
} else {
throw new AssertionError("Should not be here.");
if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else if (supportedCurseForge) {
downloadSource.set("mods.curseforge");
} else {
throw new AssertionError("Should not be here.");
}
}
}
@@ -54,7 +54,10 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -154,6 +157,18 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
);
// Toolbar Selecting
// reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
var selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
ListChangeListener<Object> listener = change -> {
selectAll.setDisable(!listView.getItems().isEmpty()
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
};
listView.getSelectionModel().getSelectedItems().addListener(listener);
listView.getItems().addListener(listener);
toolbarSelecting.getChildren().setAll(
createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
@@ -171,8 +186,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
.toList()
)
),
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
listView.getSelectionModel().selectAll()),
selectAll,
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
listView.getSelectionModel().clearSelection())
);
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* 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
* 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.versions;
import com.jfoenix.controls.JFXButton;
@@ -23,7 +23,10 @@ import com.jfoenix.controls.JFXListView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
@@ -157,20 +160,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
return;
Path parent = currentDirectory.path;
Controllers.dialog(new InputDialogPane(
Controllers.prompt(
i18n("schematics.create_directory.prompt"),
"",
(result, handler) -> {
if (StringUtils.isBlank(result)) {
handler.reject(i18n("schematics.create_directory.failed.empty_name"));
return;
}
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
handler.reject(i18n("schematics.create_directory.failed.invalid_name"));
return;
}
Path targetDir = parent.resolve(result);
if (Files.exists(targetDir)) {
handler.reject(i18n("schematics.create_directory.failed.already_exists"));
@@ -185,7 +177,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
LOG.warning("Failed to create directory: " + targetDir, e);
handler.reject(i18n("schematics.create_directory.failed", targetDir));
}
}));
}, "", new RequiredValidator(), new Validator(i18n("schematics.create_directory.failed.invalid_name"), FileUtils::isNameValid));
}
private DirItem loadAll(Path dir, @Nullable DirItem parent) {
@@ -30,8 +30,10 @@ import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventPriority;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
@@ -39,8 +41,10 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Supplier;
@@ -186,15 +190,34 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
private void clearLibraries() {
FileUtils.deleteDirectoryQuietly(getProfile().getRepository().getBaseDirectory().resolve("libraries"));
var libraries = getProfile().getRepository().getBaseDirectory().resolve("libraries");
Task.runAsync(Schedulers.io(), () -> {
FileUtils.deleteDirectoryQuietly(libraries);
}).whenComplete(Schedulers.javafx(), (exception) -> {
if (exception != null) {
Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
private void clearAssets() {
HMCLGameRepository baseDirectory = getProfile().getRepository();
FileUtils.deleteDirectoryQuietly(baseDirectory.getBaseDirectory().resolve("assets"));
if (version.get() != null) {
FileUtils.deleteDirectoryQuietly(baseDirectory.getRunDirectory(version.get().getVersion()).resolve("resources"));
}
Path assetsDir = getProfile().getRepository().getBaseDirectory().resolve("assets");
Profile.ProfileVersion currentVersion = version.get();
Path resourcesDir = currentVersion != null
? getProfile().getRepository().getRunDirectory(currentVersion.getVersion()).resolve("resources")
: null;
Task.runAsync(Schedulers.io(), () -> {
FileUtils.deleteDirectoryQuietly(assetsDir);
if (resourcesDir != null) {
FileUtils.deleteDirectoryQuietly(resourcesDir);
}
}).whenComplete(Schedulers.javafx(), (exception) -> {
if (exception != null) {
Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
private void clearJunkFiles() {
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
@@ -43,6 +42,7 @@ import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.MemoryStatusBar;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@@ -57,7 +57,6 @@ import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -115,8 +114,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final IntegerProperty maxMemory = new SimpleIntegerProperty();
private final BooleanProperty modpack = new SimpleBooleanProperty();
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
public VersionSettingsPage(boolean globalSetting) {
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
@@ -296,30 +293,14 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
lowerBoundPane.getChildren().setAll(label, slider, txtMaxMemory, new Label(i18n("settings.memory.unit.mib")));
}
StackPane progressBarPane = new StackPane();
progressBarPane.setAlignment(Pos.CENTER_LEFT);
VBox.setMargin(progressBarPane, new Insets(8, 0, 0, 16));
{
progressBarPane.setMinHeight(4);
progressBarPane.getStyleClass().add("memory-total");
StackPane usedMemory = new StackPane();
usedMemory.getStyleClass().add("memory-used");
usedMemory.maxWidthProperty().bind(Bindings.createDoubleBinding(() ->
progressBarPane.getWidth() *
(memoryStatus.get().getUsed() * 1.0 / memoryStatus.get().getTotal()), progressBarPane.widthProperty(),
memoryStatus));
StackPane allocateMemory = new StackPane();
allocateMemory.getStyleClass().add("memory-allocate");
allocateMemory.maxWidthProperty().bind(Bindings.createDoubleBinding(() ->
progressBarPane.getWidth() *
Math.min(1.0,
(double) (HMCLGameRepository.getAllocatedMemory(maxMemory.get() * 1024L * 1024L, memoryStatus.get().getAvailable(), chkAutoAllocate.isSelected())
+ memoryStatus.get().getUsed()) / memoryStatus.get().getTotal()), progressBarPane.widthProperty(),
maxMemory, memoryStatus, chkAutoAllocate.selectedProperty()));
progressBarPane.getChildren().setAll(allocateMemory, usedMemory);
}
MemoryStatusBar memoryStatusBar = new MemoryStatusBar();
VBox.setMargin(memoryStatusBar, new Insets(8, 0, 0, 16));
memoryStatusBar.memoryAllocatedProperty().bind(Bindings.createDoubleBinding(() ->
(double) HMCLGameRepository.getAllocatedMemory(
maxMemory.get() * 1024L * 1024L,
memoryStatusBar.getMemoryStatus().getAvailable(),
chkAutoAllocate.isSelected()),
memoryStatusBar.memoryStatusProperty(), maxMemory, chkAutoAllocate.selectedProperty()));
BorderPane digitalPane = new BorderPane();
VBox.setMargin(digitalPane, new Insets(0, 0, 0, 16));
@@ -327,26 +308,29 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
Label lblPhysicalMemory = new Label();
lblPhysicalMemory.getStyleClass().add("memory-label");
digitalPane.setLeft(lblPhysicalMemory);
lblPhysicalMemory.textProperty().bind(Bindings.createStringBinding(() ->
i18n("settings.memory.used_per_total",
GIGABYTES.convertFromBytes(memoryStatus.get().getUsed()),
GIGABYTES.convertFromBytes(memoryStatus.get().getTotal())), memoryStatus));
lblPhysicalMemory.textProperty().bind(Bindings.createStringBinding(() -> {
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
return i18n("settings.memory.used_per_total",
GIGABYTES.convertFromBytes(memoryStatus.getUsed()),
GIGABYTES.convertFromBytes(memoryStatus.getTotal()));
}, memoryStatusBar.memoryStatusProperty()));
Label lblAllocateMemory = new Label();
lblAllocateMemory.textProperty().bind(Bindings.createStringBinding(() -> {
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
long maxMemory = Lang.parseInt(this.maxMemory.get(), 0) * 1024L * 1024L;
return i18n(memoryStatus.get().hasAvailable() && maxMemory > memoryStatus.get().getAvailable()
return i18n(memoryStatus.hasAvailable() && maxMemory > memoryStatus.getAvailable()
? (chkAutoAllocate.isSelected() ? "settings.memory.allocate.auto.exceeded" : "settings.memory.allocate.manual.exceeded")
: (chkAutoAllocate.isSelected() ? "settings.memory.allocate.auto" : "settings.memory.allocate.manual"),
GIGABYTES.convertFromBytes(maxMemory),
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(maxMemory, memoryStatus.get().getAvailable(), chkAutoAllocate.isSelected())),
GIGABYTES.convertFromBytes(memoryStatus.get().getAvailable()));
}, memoryStatus, maxMemory, chkAutoAllocate.selectedProperty()));
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(maxMemory, memoryStatus.getAvailable(), chkAutoAllocate.isSelected())),
GIGABYTES.convertFromBytes(memoryStatus.getAvailable()));
}, memoryStatusBar.memoryStatusProperty(), maxMemory, chkAutoAllocate.selectedProperty()));
lblAllocateMemory.getStyleClass().add("memory-label");
digitalPane.setRight(lblAllocateMemory);
}
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, progressBarPane, digitalPane);
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, memoryStatusBar, digitalPane);
}
launcherVisibilityPane = new LineSelectButton<>();
@@ -367,7 +351,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
cboWindowsSize.setPrefWidth(150);
right.setLeft(cboWindowsSize);
cboWindowsSize.setEditable(true);
cboWindowsSize.setStyle("-fx-padding: 4 4 4 16");
cboWindowsSize.setPromptText("854x480");
cboWindowsSize.getItems().setAll(getSupportedResolutions());
@@ -472,7 +455,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
componentList.disableProperty().bind(enableSpecificSettings.not());
}
private void fireWorkingDirChanged() {
FXUtils.runInFX(() -> fireEvent(new VersionPage.WorkingDirChangedEvent()));
}
@@ -776,57 +759,4 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
return state.getReadOnlyProperty();
}
private static final class UpdateMemoryStatus extends Thread {
@FXThread
private static WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyCache;
@FXThread
static ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
if (memoryStatusPropertyCache != null) {
var property = memoryStatusPropertyCache.get();
if (property != null) {
return property;
}
}
ObjectProperty<PhysicalMemoryStatus> property = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
memoryStatusPropertyCache = new WeakReference<>(property);
new UpdateMemoryStatus(memoryStatusPropertyCache).start();
return property;
}
private final WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef;
UpdateMemoryStatus(WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef) {
this.memoryStatusPropertyRef = memoryStatusPropertyRef;
setName("UpdateMemoryStatus");
setDaemon(true);
setPriority(Thread.MIN_PRIORITY);
}
@Override
public void run() {
while (true) {
PhysicalMemoryStatus status = SystemInfo.getPhysicalMemoryStatus();
var memoryStatusProperty = memoryStatusPropertyRef.get();
if (memoryStatusProperty == null)
return;
if (Controllers.isStopped())
return;
Platform.runLater(() -> memoryStatusProperty.set(status));
try {
//noinspection BusyWait
Thread.sleep(1000);
} catch (InterruptedException e) {
return;
}
}
}
}
}
@@ -22,8 +22,11 @@ import javafx.application.Platform;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameDownloadTask;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.setting.*;
@@ -146,7 +149,8 @@ public final class Versions {
} else {
handler.reject(i18n("version.manage.rename.fail"));
}
}, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
}, version,
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)));
}
@@ -158,6 +162,43 @@ public final class Versions {
FXUtils.openFolder(profile.getRepository().getRunDirectory(version));
}
public static void installFromJson(Profile profile, Path file) {
Version version;
try {
version = profile.getRepository().readVersionJson(file);
} catch (Exception e) {
Controllers.dialog(i18n("install.new_game.malformed_json"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
return;
}
Controllers.prompt(i18n("version.manage.duplicate.prompt"), (result, handler) -> {
handler.resolve();
DefaultDependencyManager dependencyManager = profile.getDependency();
HMCLGameRepository repository = profile.getRepository();
Version newVersion = version.setId(result).setJar(result);
Controllers.taskDialog(
Task.allOf(new GameDownloadTask(dependencyManager, null, newVersion),
Task.allOf(
new GameAssetDownloadTask(dependencyManager, newVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true),
new GameLibrariesTask(dependencyManager, newVersion, true)
).withRunAsync(() -> {
// ignore failure
}))
.thenComposeAsync(repository.saveAsync(newVersion))
.thenRunAsync(repository::refreshVersions)
.whenComplete(Schedulers.javafx(), (exception) -> {
if (exception == null) {
profile.setSelectedVersion(result);
} else {
Controllers.dialog(
DownloadProviders.localizeErrorMessage(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR);
}
}), i18n("install.new_game"), TaskCancellationAction.NORMAL);
}, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName)));
}
public static void duplicateVersion(Profile profile, String version) {
Controllers.prompt(
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> {
@@ -223,6 +264,11 @@ public final class Versions {
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
if (file != null) {
if (!isValidScriptExtension(FileUtils.getExtension(file))) {
String defaultExt = getDefaultScriptExtension();
file = file.resolveSibling(file.getFileName().toString() + "." + defaultExt);
}
LauncherHelper launcherHelper = new LauncherHelper(profile, account, id);
for (Consumer<LauncherHelper> injecter : injecters) {
injecter.accept(launcherHelper);
@@ -232,6 +278,21 @@ public final class Versions {
});
}
private static boolean isValidScriptExtension(String ext) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
return ext.equalsIgnoreCase("bat") || ext.equalsIgnoreCase("ps1");
}
return ext.equalsIgnoreCase("sh") || ext.equalsIgnoreCase("bash") || ext.equalsIgnoreCase("command") || ext.equalsIgnoreCase("ps1");
}
private static String getDefaultScriptExtension() {
return switch (OperatingSystem.CURRENT_OS) {
case WINDOWS -> "bat";
case MACOS -> "command";
default -> "sh";
};
}
@SafeVarargs
public static void launch(Profile profile, String id, Consumer<LauncherHelper>... injecters) {
if (!checkVersionForLaunching(profile, id))
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleBooleanProperty;
@@ -34,6 +33,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.glavo.nbt.tag.*;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -76,7 +76,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
}
private void updateControls() {
CompoundTag dataTag = levelData.get("Data");
CompoundTag dataTag = (CompoundTag) levelData.get("Data");
CompoundTag playerTag = playerData;
ScrollPane scrollPane = new ScrollPane();
@@ -100,7 +100,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
setRightTextField(worldNamePane, worldNameField, 200);
if (dataTag.get("LevelName") instanceof StringTag worldNameTag) {
var worldName = new SimpleStringProperty(worldNameTag.getValue());
var worldName = new SimpleStringProperty(worldNameTag.get());
FXUtils.bindString(worldNameField, worldName);
worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName);
worldName.addListener((observable, oldValue, newValue) -> {
@@ -324,7 +324,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
{
locationPane.setTitle(i18n("world.info.player.location"));
Dimension dimension = Dimension.of(playerTag.get("Dimension"));
if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) {
if (dimension != null && playerTag.get("Pos") instanceof ListTag<?> posTag) {
locationPane.setText(dimension.formatPosition(posTag));
}
}
@@ -347,7 +347,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
if (playerTag.get("respawn") instanceof CompoundTag respawnTag
&& respawnTag.get("dimension") instanceof StringTag dimensionTag
&& respawnTag.get("pos") instanceof IntArrayTag intArrayTag
&& intArrayTag.length() >= 3) { // Valid after 25w07a
&& intArrayTag.size() >= 3) { // Valid after 25w07a
spawnPane.setText(Dimension.of(dimensionTag).formatPosition(intArrayTag));
} else if (playerTag.get("SpawnX") instanceof IntTag intX
&& playerTag.get("SpawnY") instanceof IntTag intY
@@ -541,7 +541,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
static Dimension of(Tag tag) {
if (tag instanceof IntTag intTag) {
return switch (intTag.getValue()) {
return switch (intTag.get()) {
case 0 -> OVERWORLD;
case -1 -> THE_NETHER;
case 1 -> THE_END;
@@ -561,30 +561,23 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
}
String formatPosition(Tag tag) {
if (tag instanceof ListTag listTag && listTag.size() == 3) {
if (tag instanceof ListTag<?> listTag && listTag.size() == 3) {
Tag x = listTag.get(0);
Tag y = listTag.get(1);
Tag z = listTag.get(2);
if (x instanceof DoubleTag && y instanceof DoubleTag && z instanceof DoubleTag) {
return this == OVERWORLD
? String.format("(%.2f, %.2f, %.2f)", x.getValue(), y.getValue(), z.getValue())
: String.format("%s (%.2f, %.2f, %.2f)", name, x.getValue(), y.getValue(), z.getValue());
if (listTag.getTag(0) instanceof DoubleTag x
&& listTag.getTag(1) instanceof DoubleTag y
&& listTag.getTag(2) instanceof DoubleTag z) {
return formatPosition(x.get(), y.get(), z.get());
}
return null;
}
if (tag instanceof IntArrayTag intArrayTag) {
int x = intArrayTag.get(0);
int y = intArrayTag.get(1);
int z = intArrayTag.get(2);
int x = intArrayTag.getValue(0);
int y = intArrayTag.getValue(1);
int z = intArrayTag.getValue(2);
return this == OVERWORLD
? String.format("(%d, %d, %d)", x, y, z)
: String.format("%s (%d, %d, %d)", name, x, y, z);
return formatPosition(x, y, z);
}
return null;
@@ -592,14 +585,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
String formatPosition(int x, int y, int z) {
return this == OVERWORLD
? String.format("(%d, %d, %d)", x, y, z)
: String.format("%s (%d, %d, %d)", name, x, y, z);
? "(%d, %d, %d)".formatted(x, y, z)
: "%s (%d, %d, %d)".formatted(name, x, y, z);
}
String formatPosition(double x, double y, double z) {
return this == OVERWORLD
? String.format("(%.2f, %.2f, %.2f)", x, y, z)
: String.format("%s (%.2f, %.2f, %.2f)", name, x, y, z);
? "(%.2f, %.2f, %.2f)".formatted(x, y, z)
: "%s (%.2f, %.2f, %.2f)".formatted(name, x, y, z);
}
}
@@ -67,7 +67,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private final TabHeader header = new TabHeader(transitionPane);
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
private final TabHeader.Tab<DataPackListPage> dataPackTab = new TabHeader.Tab<>("dataPackListPage");
public WorldManagePage(World world, Profile profile, String instanceId) {
this.world = world;
@@ -86,7 +86,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
dataPackTab.setNodeSupplier(() -> new DataPackListPage(this));
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
@@ -218,9 +218,9 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
if (getSkinnable().world.supportDatapacks()) {
getSkinnable().header.getTabs().add(getSkinnable().datapackTab);
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
if (getSkinnable().world.supportDataPacks()) {
getSkinnable().header.getTabs().add(getSkinnable().dataPackTab);
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().dataPackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
}
}
@@ -23,8 +23,9 @@ import org.jackhuang.hmcl.game.WorldLockedException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -89,20 +90,9 @@ public final class WorldManageUIUtils {
public static void copyWorld(World world, Runnable runnable) {
Path worldPath = world.getFile();
Controllers.dialog(new InputDialogPane(
Controllers.prompt(
i18n("world.duplicate.prompt"),
"",
(result, handler) -> {
if (StringUtils.isBlank(result)) {
handler.reject(i18n("world.duplicate.failed.empty_name"));
return;
}
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
handler.reject(i18n("world.duplicate.failed.invalid_name"));
return;
}
Path targetDir = worldPath.resolveSibling(result);
if (Files.exists(targetDir)) {
handler.reject(i18n("world.duplicate.failed.already_exists"));
@@ -125,7 +115,7 @@ public final class WorldManageUIUtils {
}
})
.start();
}));
}, "", new RequiredValidator(), new Validator(i18n("world.duplicate.failed.invalid_name"), FileUtils::isNameValid));
}
public static void closeSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException {
@@ -20,20 +20,20 @@ package org.jackhuang.hmcl.upgrade;
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.java.JavaRuntime;
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.MessageType;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
@@ -43,13 +43,15 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
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;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class UpdateHandler {
private UpdateHandler() {
@@ -67,7 +69,7 @@ public final class UpdateHandler {
performMigration();
} catch (IOException e) {
LOG.warning("Failed to perform migration", e);
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.MANUAL_UPDATE_URL) + "\n" + StringUtils.getStackTrace(e));
}
return true;
}
@@ -82,7 +84,7 @@ public final class UpdateHandler {
applyUpdate(Paths.get(args[1]));
} catch (IOException e) {
LOG.warning("Failed to apply update", e);
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.MANUAL_UPDATE_URL) + "\n" + StringUtils.getStackTrace(e));
}
return true;
}
@@ -125,6 +127,29 @@ public final class UpdateHandler {
throw new IOException("Current JAR is not verified");
}
CompletableFuture<Void> future = new CompletableFuture<>();
Platform.runLater(() -> {
try {
Controllers.saveWindowStates();
} finally {
future.complete(null);
}
});
try {
future.get();
} catch (ExecutionException | InterruptedException ignored) {
// Ignore
}
try {
FileSaver.waitForAllSaves();
} catch (InterruptedException ignored) {
// Ignore
}
requestUpdate(downloaded, getCurrentLocation());
EntryPoint.exit(0);
} catch (IOException e) {
@@ -31,21 +31,27 @@ public final class ChunkBaseApp {
private static final GameVersionNumber MIN_END_CITY_VERSION = GameVersionNumber.asGameVersion("1.13");
private static final String[] SEED_MAP_GAME_VERSIONS = {
"1.21.9", "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"
"26.1", "1.21.9", "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 = {
"1.20", "1.19.3", "1.19", "1.18", "1.16", "1.13", "1.9", "1.7"
"26.1", "1.21.9", "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[] NETHER_FORTRESS_GAME_VERSIONS = {
"1.18", "1.16", "1.7"
"26.1", "1.21.9", "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[] END_CITY_GAME_VERSIONS = {
"1.19", "1.13"
"26.1", "1.21.9", "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"
};
public static boolean isSupported(@NotNull World world) {
@@ -63,6 +63,16 @@ public final class NativePatcher {
});
}
// https://github.com/LWJGL/lwjgl3/issues/1111
public static boolean needPatchMemoryUtil(Version version, int javaVersion) {
return javaVersion >= 25 && javaVersion <= 26 && version.getLibraries().stream().anyMatch(library ->
"org.lwjgl".equals(library.getGroupId())
&& "lwjgl".equals(library.getArtifactId())
&& "3.4.1".equals(library.getVersion())
&& library.getClassifier() == null
);
}
public static Version patchNative(DefaultGameRepository repository,
Version version, String gameVersion,
JavaRuntime javaVersion,
@@ -179,15 +189,16 @@ public final class NativePatcher {
return version.setLibraries(newLibraries);
}
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
/// @see <a href="https://github.com/HMCL-dev/mesa-loader-windows">Java Mesa Loader for Windows</a>
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime java, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
if (renderer == Renderer.DEFAULT)
return null;
if (windowsVersion.isAtLeast(OSVersion.WINDOWS_10)) {
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
return getNatives(java.getPlatform()).get("mesa-loader");
} else if (windowsVersion.isAtLeast(OSVersion.WINDOWS_7)) {
if (renderer == Renderer.LLVMPIPE)
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
if (renderer == Renderer.OpenGL.LLVMPIPE)
return getNatives(java.getPlatform()).get("software-renderer-loader");
else
return null;
} else {
@@ -96,6 +96,10 @@ public class Translator_lzh extends Translator {
case GA -> {
// do nothing
}
case SNAPSHOT -> {
builder.append("之快照");
appendDigitByDigit(builder, release.getEaVersion().toString());
}
case PRE_RELEASE -> {
builder.append("之預");
appendDigitByDigit(builder, release.getEaVersion().toString());
@@ -155,7 +159,32 @@ public class Translator_lzh extends Translator {
case "23w13a_or_b" -> "二三週一三暨";
case "24w14potato" -> "二四週一四芋";
case "25w14craftmine" -> "二五週一四礦";
default -> version;
default -> {
if (version.length() >= 6 && version.charAt(2) == 'w') {
Matcher matcher = Pattern.compile("(?<year>[0-9]{2})w(?<week>[0-9]{2})(?<suffix>.+)").matcher(version);
if (matcher.matches()) {
int year = Integer.parseInt(matcher.group("year"));
int week = Integer.parseInt(matcher.group("week"));
String suffix = matcher.group("suffix");
char suffixFirstChar = suffix.charAt(0);
StringBuilder builder = new StringBuilder();
appendDigitByDigit(builder, String.valueOf(year));
builder.append('週');
appendDigitByDigit(builder, String.valueOf(week));
if (suffix.length() == 1
&& suffixFirstChar >= 'a' && (suffixFirstChar - 'a') < TIAN_GAN.length)
builder.append(TIAN_GAN[suffixFirstChar - 'a']);
else
builder.append(suffix);
yield builder.toString();
}
}
yield version;
}
};
} else {
return gameVersion.toString();
+18 -3
View File
@@ -45,9 +45,9 @@
"externalLink" : "https://github.com/jenkinsci/constant-pool-scanner"
},
{
"title" : "OpenNBT",
"subtitle" : "Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.",
"externalLink" : "https://github.com/GeyserMC/OpenNBT"
"title" : "HelloNBT",
"subtitle" : "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
"externalLink" : "https://github.com/HMCL-dev/HelloNBT"
},
{
"title" : "minecraft-jfx-skin",
@@ -69,6 +69,11 @@
"subtitle" : "Copyright © 2015 Andrew Ellerton.\nLicensed under the Apache 2.0 License.",
"externalLink" : "https://github.com/aellerton/japng"
},
{
"title" : "fxsvgimage",
"subtitle" : "Copyright (c) 2021, 2022, 2025 Hervé Girod.\nLicensed under the BSD 3-clause License.",
"externalLink" : "https://github.com/hervegirod/fxsvgimage"
},
{
"title": "Terracotta",
"subtitle": "Copyright © 2025 Burning_TNT.\nLicensed under the AGPL 3.0 License.",
@@ -83,5 +88,15 @@
"title": "MonetFX",
"subtitle": "Copyright © 2025 Glavo.\nLicensed under the Apache 2.0 License.",
"externalLink": "https://github.com/Glavo/MonetFX"
},
{
"title": "LWJGL Unsafe Agent",
"subtitle": "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
"externalLink": "https://github.com/HMCL-dev/lwjgl-unsafe-agent"
},
{
"title": "TomlJ",
"subtitle": "Licensed under the Apache 2.0 License.",
"externalLink": "https://github.com/tomlj/tomlj"
}
]
+69 -19
View File
@@ -29,8 +29,8 @@
.scroll-bar .thumb {
-fx-fill: -monet-surface-tint;
-fx-arc-width: 8;
-fx-arc-height: 8;
-fx-arc-width: 6;
-fx-arc-height: 6;
}
.title-label {
@@ -113,28 +113,26 @@
.memory-label {
}
.memory-used {
-fx-background-color: -monet-primary;
.memory-status-bar {
-fx-opacity: 1;
}
.memory-used:disabled {
.memory-status-bar:disabled {
-fx-opacity: 0.4;
}
.memory-allocate {
-fx-background-color: -monet-primary-transparent-50;
.memory-status-bar .track {
-fx-background-color: -monet-secondary-container;
-fx-background-radius: 2;
-fx-background-insets: 0;
}
.memory-allocate:disabled {
-fx-opacity: 0.4;
.memory-status-bar .memory-used {
-fx-fill: -monet-primary;
}
.memory-total {
-fx-background-color: -monet-surface-container;
}
.memory-total:disabled {
-fx-opacity: 0.4;
.memory-status-bar .memory-allocate {
-fx-fill: -monet-primary-transparent-50;
}
.update-label {
@@ -416,7 +414,7 @@
}
.installer-item-wrapper .installer-item:list-item > .installer-item-name {
-fx-pref-width: 80px;
-fx-pref-width: 100px;
}
.installer-item-wrapper .installer-item:list-item > .installer-item-status {
@@ -527,7 +525,8 @@
}
.jfx-dialog-layout {
-fx-padding: 24.0px 24.0px 16.0px 24.0px;
-fx-background-radius: 4px;
-fx-padding: 24px 24px 16px 24px;
-fx-background-color: -monet-surface-container-high;
-fx-text-fill: -monet-on-surface;
}
@@ -570,6 +569,7 @@
}
.task-executor-dialog-layout {
-fx-background-radius: 4px;
-fx-background-color: -monet-surface-container-high;
-fx-text-fill: -monet-on-surface;
}
@@ -1093,8 +1093,12 @@
-fx-background-color: -monet-error-container;
}
.multi-file-item {
-fx-padding: 10 0 10 0;
}
.options-sublist {
-fx-background-color: -monet-surface;
-fx-background-color: -monet-surface-container;
}
.options-list-item {
@@ -1105,6 +1109,14 @@
-fx-font-size: 12;
}
.options-sublist .options-list-item {
-fx-background-color: -monet-surface-container;
}
.options-sublist .multi-file-item {
-fx-padding: 10 10 10 10;
}
.options-list-item .svg {
-fx-fill: -monet-on-surface;
-fx-border-color: -monet-outline-variant;
@@ -1140,6 +1152,10 @@
-fx-border-width: 0;
}
.options-sublist-wrapper > .container {
-fx-background-color: -monet-surface-container;
}
.options-sublist-wrapper .expand-icon .svg {
-fx-fill: -monet-on-surface-variant;
}
@@ -1463,6 +1479,11 @@
-jfx-radius: 9;
}
.spinner-pane .spinner-pane-content {
-fx-padding: 10px;
-fx-spacing: 10px;
}
/*******************************************************************************
* *
* JFX Combo Box *
@@ -1477,6 +1498,11 @@
-fx-max-width: 1000000000;
}
.jfx-combo-box .arrow {
-fx-background-color: -monet-on-surface-variant;
-fx-shape: "M12 15 7 10H17L12 15Z";
}
.jfx-combo-box .text {
-fx-fill: -monet-on-surface;
}
@@ -1492,6 +1518,7 @@
.combo-box-popup .list-view .jfx-list-cell {
-fx-background-color: -monet-surface-container;
-fx-text-fill: -monet-on-surface;
-fx-cursor: hand;
-fx-background-insets: 0.0;
}
@@ -1501,6 +1528,10 @@
-fx-text-fill: -monet-on-secondary-container;
}
.combo-box-popup .list-view .list-cell:hover > .jfx-rippler > StackPane {
-fx-background-color: -monet-secondary-container-transparent-50;
}
.jfx-combo-box-warning {
-jfx-focus-color: #D34336;
-jfx-unfocus-color: #D34336;
@@ -1565,6 +1596,21 @@
-fx-background-color: -monet-on-surface;
}
.custom-color-dialog .jfx-color-dialog-actions {
-fx-background-color: -monet-surface;
-fx-padding: 0.0 10.0 5.0 0.0;
-fx-alignment: center-right;
}
.custom-color-dialog .jfx-color-dialog-accept {
-fx-padding: 0.7em 0.8em;
}
.custom-color-dialog .jfx-color-dialog-cancel {
-fx-text-fill: -monet-on-surface-variant;
-fx-padding: 0.7em 0.8em;
}
/*******************************************************************************
* *
* Table View *
@@ -1923,4 +1969,8 @@
.line-toggle-button .jfx-toggle-button {
-fx-padding: 0;
}
}
.update-pane:active .subtitle-label {
-fx-text-fill: -monet-tertiary;
}
@@ -167,6 +167,9 @@ account.skin.upload.failed=Failed to upload skin.
account.skin.invalid_skin=Invalid skin file.
account.username=Username
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (Snapshots)
archive.author=Author(s)
archive.date=Publish Date
archive.file.name=File Name
@@ -360,16 +363,16 @@ download.failed.empty=No versions are available. Please click here to go back.
download.failed.no_code=Failed to download
download.failed.refresh=Failed to fetch version list. Please click here to retry.
download.game=New Game
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.bmclapi=BMCLAPI & MCIM
download.provider.bmclapi.desc=This option is provided exclusively for users in Chinese Mainland.
download.provider.mojang=Official
download.provider.mojang.desc=OptiFine is provided by BMCLAPI
download.provider.mojang.desc=Note that OptiFine is provided by BMCLAPI.
download.provider.official=From Official Sources
download.provider.official.desc=Latest, but may load slowly
download.provider.official.desc=Always obtain from official sources.
download.provider.balanced=From Fastest Available
download.provider.balanced.desc=Balanced, but may not be the latest
download.provider.balanced.desc=Best choice for ALL users.
download.provider.mirror=From Mirror
download.provider.mirror.desc=Fast, but may not be the latest
download.provider.mirror.desc=Recommended option for users in Chinese Mainland.
download.java=Downloading Java
download.java.process=Java Download Process
download.javafx=Downloading dependencies for the launcher...
@@ -397,7 +400,7 @@ extension.ps1=Windows PowerShell Script
extension.sh=Shell Script
extension.command=macOS Shell Script
extension.datapack=Datapack Archive
extension.datapack=Data Pack Archive
extension.mod=Mod File
extension.modloader.installer=Mod Loader Installer
extension.resourcepack=Resource Pack Archive
@@ -449,12 +452,12 @@ fatal.unsupported_platform=Minecraft is not fully supported on your platform yet
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher has provided support for the Loongson platform.\n\
If you encounter problems when playing a game, you can visit https://docs.hmcl.net/groups.html for help.
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher has provided support for the Apple silicon platform, using native ARM Java to launch games to get a smoother gaming experience.\n\
If you encounter problems when playing a game, launching the game with Java based on x86-64 architecture may offer better compatibility.
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
\n\
If you are using the <b>Qualcomm</b> platform, you may need to install the <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL Compatibility Pack</a> before playing games.\n\
Click the link to navigate to the Microsoft Store and install the compatibility pack.
# When translating the following string into non-Latin languages, please retain the English translation
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.
file=File
@@ -702,7 +705,7 @@ game.version=Game Instance
help=Help
help.doc=Hello Minecraft! Launcher Documentation
help.detail=For datapack and modpack makers.
help.detail=For data pack and modpack makers.
input.email=The username must be an email address.
input.number=The input must be numbers.
@@ -757,6 +760,7 @@ install.new_game.already_exists=This instance name already exists. Please use an
install.new_game.current_game_version=Current Instance Version
install.new_game.installation=Instance Installation
install.new_game.malformed=Invalid name.
install.new_game.malformed_json=Invalid Minecraft instance JSON file.
install.select=Choose operation
install.success=Successfully installed.
@@ -808,6 +812,8 @@ launch.advice.forge37_0_60=Forge versions prior to 37.0.60 are not compatible wi
launch.advice.java8_1_13=Minecraft 1.13 and later can only be run on Java 8 or later. Please use Java 8 or later versions.
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 versions prior to 1.8.0_51. Please install the latest Java 8 version.
launch.advice.java9=You cannot launch Minecraft 1.12 or earlier with Java 9 or later. Please use Java 8 instead.
launch.advice.lwjgl_3_4_1=The current game version has a known issue that may cause stuttering in the game.\nHMCL can automatically optimize the game performance (using a Java Agent).\nDo you allow HMCL to automatically optimize the game?\nIf you encounter issues, you can disable the "Allow HMCL to modify the game" option in "Settings → General".
launch.advice.lwjgl_3_4_1.title=Allow HMCL to automatically optimize the game?
launch.advice.modded_java=Some mods may not be compatible with newer Java versions. It is recommended to use Java %1$s to launch Minecraft %2$s.
launch.advice.modlauncher8=The Forge version you are using is not compatible with the current Java version. Please try updating Forge.
launch.advice.newer_java=You are using an older Java version to launch the game. It is recommended to update to Java 8, otherwise some mods may cause the game to crash.
@@ -990,7 +996,7 @@ modrinth.category.colored-lighting=Colored Lighting
modrinth.category.combat=Combat
modrinth.category.core-shaders=Core Shaders
modrinth.category.cursed=Cursed
modrinth.category.datapack=Datapack
modrinth.category.datapack=Data Pack
modrinth.category.decoration=Decoration
modrinth.category.economy=Economy
modrinth.category.entities=Entities
@@ -1133,11 +1139,11 @@ nbt.open.failed=Failed to open file
nbt.save.failed=Failed to save file
nbt.title=View File - %s
datapack=Datapacks
datapack=Data Packs
datapack.add=Add
datapack.add.title=Choose datapack archive you want to add
datapack.reload.toast=Minecraft is running, please use the /reload command to reload the data pack
datapack.title=World [%s] - Datapacks
datapack.add.title=Choose data pack archive you want to add
datapack.reload.toast=Minecraft is running. Use the /reload command to reload data packs
datapack.title=World [%s] - Data Packs
web.failed=Failed to load page
web.open_in_browser=Do you want to open this address in a browser:\n%s
@@ -1167,7 +1173,7 @@ world.duplicate.failed.empty_name=Name cannot be empty
world.duplicate.failed.invalid_name=Name contains invalid characters
world.duplicate.failed=Failed to duplicate the world
world.duplicate.success.toast=Successfully duplicated the world
world.datapack=Datapacks
world.datapack=Data Packs
world.datetime=Last played on %s
world.delete=Delete the World
world.delete.failed=Failed to delete world.\n%s
@@ -1224,7 +1230,7 @@ world.load.fail=Failed to load world
world.locked=In use
world.locked.failed=The world is currently in use. Please close the game and try again.
world.manage=Worlds
world.manage.button=World Management
world.manage.button=Manage World
world.manage.title=World - %s
world.name=World Name
world.name.enter=Enter the world name
@@ -1329,6 +1335,15 @@ settings.advanced.dont_patch_natives=Do not attempt to automatically replace nat
settings.advanced.environment_variables=Environment Variables
settings.advanced.game_dir.default=Default (".minecraft/")
settings.advanced.game_dir.independent=Isolated (".minecraft/versions/<instance name>/", except for assets and libraries)
settings.advanced.graphics_backend=Graphics API
settings.advanced.graphics_backend.default=Default
settings.advanced.graphics_backend.default.desc=Follow the game's settings
settings.advanced.graphics_backend.opengl=OpenGL
settings.advanced.graphics_backend.opengl.desc=Use OpenGL for rendering
settings.advanced.graphics_backend.vulkan=Vulkan
settings.advanced.graphics_backend.vulkan.desc=Use Vulkan for rendering
settings.advanced.graphics_backend.vulkan.desc.global=Use Vulkan for rendering (Since Minecraft 26.2)
settings.advanced.graphics_backend.vulkan.desc.unsupported=Use Vulkan for rendering (not supported for the current game version)
settings.advanced.java_permanent_generation_space=PermGen Space
settings.advanced.java_permanent_generation_space.prompt=in MiB
settings.advanced.jvm=JVM Options
@@ -1348,10 +1363,6 @@ settings.advanced.natives_directory.choose=Choose the location of the desired na
settings.advanced.natives_directory.custom=Custom
settings.advanced.natives_directory.default=Default
settings.advanced.natives_directory.default.version_id=<Instance Name>
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\
Note: It is recommended to use a fully English-letters path for the specified local library file. Otherwise it may lead to game launch failure.
settings.advanced.no_jvm_args=Do not add default JVM arguments
settings.advanced.no_optimizing_jvm_args=Do not add default JVM optimization arguments
settings.advanced.precall_command=Pre-launch Command
@@ -1371,18 +1382,53 @@ settings.advanced.post_exit_command=Post-exit Command
settings.advanced.post_exit_command.prompt=Commands to execute after the game exits
settings.advanced.renderer=Renderer
settings.advanced.renderer.default=Default
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Poor performance and compatibility)
settings.advanced.renderer.default.desc=Use System Default
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=Software Vulkan Renderer
settings.advanced.renderer.dozen=Mesa Dozen
settings.advanced.renderer.dozen.desc=Vulkan renderer based on DirectX 12 (Experimental)
settings.advanced.renderer.nvidia_vulkan=NVIDIA
settings.advanced.renderer.nvidia_vulkan.desc=Official Vulkan driver for NVIDIA GPUs
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
settings.advanced.renderer.nvidia_nvk.desc=Open-source Vulkan driver for NVIDIA GPUs
settings.advanced.renderer.amdvlk=AMD
settings.advanced.renderer.amdvlk.desc=Official Vulkan driver for AMD GPUs
settings.advanced.renderer.amd_radv=AMD (RADV)
settings.advanced.renderer.amd_radv.desc=Open-source Vulkan driver for AMD GPUs
settings.advanced.renderer.intel_vulkan=Intel
settings.advanced.renderer.intel_vulkan.desc=Official Vulkan driver for Intel GPUs
settings.advanced.renderer.intel_anv=Intel (ANV)
settings.advanced.renderer.intel_anv.desc=Open-source Vulkan driver for Intel GPUs
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
settings.advanced.renderer.intel_hasvk.desc=Open-source Vulkan driver for older Intel GPUs
settings.advanced.renderer.qualcomm=Qualcomm
settings.advanced.renderer.qualcomm.desc=Official Vulkan driver for Qualcomm Adreno GPUs
settings.advanced.renderer.turnip=Qualcomm (Turnip)
settings.advanced.renderer.turnip.desc=Open-source Vulkan driver for Qualcomm Adreno GPUs
settings.advanced.renderer.moltenvk=MoltenVK
settings.advanced.renderer.moltenvk.desc=Vulkan renderer based on Metal
settings.advanced.renderer.kosmickrisp=KosmicKrisp
settings.advanced.renderer.kosmickrisp.desc=Vulkan renderer based on Metal 4 (Experimental)
settings.advanced.renderer.powervr=PowerVR
settings.advanced.renderer.powervr.desc=Open-source Vulkan driver for PowerVR GPUs (Experimental)
settings.advanced.renderer.panvk=PanVK
settings.advanced.renderer.panvk.desc=Open-source Vulkan driver for Arm Mali GPUs (Experimental)
settings.advanced.renderer.v3dv=V3DV
settings.advanced.renderer.v3dv.desc=Open-source Vulkan driver for Raspberry Pi 4/5 GPUs (Experimental)
# OpenGL Renderers
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Software (Poor performance, best compatibility)
settings.advanced.renderer.llvmpipe.desc=Software OpenGL renderer
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=OpenGL renderer based on DirectX 12
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Best performance, poor compatibility)
settings.advanced.renderer.zink.desc=OpenGL renderer based on Vulkan
settings.advanced.server_ip=Server Address
settings.advanced.server_ip.prompt=Automatically join after launching the game
settings.advanced.unsupported_system_options=Settings not applicable to the current system
settings.advanced.use_native_glfw=[Linux/FreeBSD Only] Use System GLFW
settings.advanced.use_native_openal=[Linux/FreeBSD Only] Use System OpenAL
settings.advanced.use_native_glfw=Use System GLFW
settings.advanced.use_native_openal=Use System OpenAL
settings.advanced.linux_freebsd_only=Linux/FreeBSD Only
settings.advanced.workaround=Workaround
settings.advanced.workaround.warning=Workaround options are intended only for advanced users. Tweaking with these options may crash the game. Unless you know what you are doing, please do not edit these options.
settings.advanced.wrapper_launcher=Wrapper Command
@@ -1416,6 +1462,8 @@ settings.game.working_directory.hint=Enable the "Isolated" option in "Working Di
settings.icon=Icon
settings.launcher=Launcher Settings
settings.launcher.allow_auto_agent=Allow HMCL to modify the game
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience.
settings.launcher.appearance=Appearance
settings.launcher.brightness=Theme Mode
settings.launcher.brightness.auto=Follow System Settings
@@ -1431,6 +1479,7 @@ settings.launcher.download.threads.auto=Automatically Determine
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
settings.launcher.download_source=Download Source
settings.launcher.download_source.auto=Automatically Choose Download Sources
settings.launcher.default_addon_source=Default Addon Source
settings.launcher.enable_game_list=Show instance list in homepage
settings.launcher.font=Font
settings.launcher.font.anti_aliasing=Anti-aliasing
@@ -1579,7 +1628,7 @@ update=Update
update.accept=Update
update.changelog=Changelog
update.channel.dev=Beta
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Release channel builds.\n\
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Stable channel builds.\n\
\n\
If you encounter any bugs or problems, please submit feedback via the channels provided on the <a href="hmcl://settings/feedback">Feedback</a> page.\n\
\n\
@@ -1592,7 +1641,7 @@ update.channel.nightly.hint=You are currently using a Nightly channel build of t
\n\
Follow <a href="https://space.bilibili.com/1445341">@huanghongxun</a> on Bilibili to stay up to date on important HMCL news, or <a href="https://space.bilibili.com/20314891">@Glavo</a> to learn about HMCL development progress.
update.channel.nightly.title=Nightly Channel Notice
update.channel.stable=Release
update.channel.stable=Stable
update.checking=Checking for updates
update.disable_auto_show_update_dialog=Do not show update dialog automatically
update.disable_auto_show_update_dialog.subtitle=Enable this option to prevent HMCL from automatically showing the update dialog.
@@ -1601,7 +1650,8 @@ update.found=Update Available!
update.newest_version=Latest version: %s
update.bubble.title=Update Available: %s
update.bubble.subtitle=Click here to update
update.note=Beta and Nightly channels may have more features or fixes, but they also come with more potential problems.
update.note.stable=Suitable for users who prioritize software stability.\nNew features are added to the stable version only after thorough testing.
update.note.dev=Suitable for users who want to experience new features first.\nThe development version includes the latest features and bug fixes,\nbut may also have more issues due to insufficient testing.
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
@@ -354,18 +354,11 @@ download.external_link=زيارة موقع التنزيل
download.failed=فشل تنزيل "%1$s"، رمز الاستجابة: %2$d.
download.failed.empty=لا توجد إصدارات متاحة. انقر هنا للرجوع.
download.failed.no_code=فشل التنزيل
download.failed.refresh=فشل جلب قائمة الإصدارات. انقر هنا لإعادة المحاولة.
download.game=لعبة جديدة
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.mojang=رسمي
download.provider.mojang.desc=يتم توفير OptiFine عبر BMCLAPI
download.failed.refresh=فشل في جلب قائمة الإصدارات. يرجى النقر هنا لإعادة المحاولة.
download.game=New Game
download.provider.official=من المصادر الرسمية
download.provider.official.desc=الأحدث، لكن قد يكون التحميل بطيئاً
download.provider.balanced=من أسرع المصادر المتاحة
download.provider.balanced.desc=متوازن، لكن قد لا يكون الأحدث
download.provider.mirror=من مرآة
download.provider.mirror.desc=سريع، لكن قد لا يكون الأحدث
download.java=جارٍ تنزيل Java
download.java.process=عملية تنزيل Java
download.javafx=جارٍ تنزيل مكتبات المشغّل...
@@ -445,8 +438,6 @@ fatal.unsupported_platform=لا يدعم Minecraft منصتك الحالية ب
إذا لم تتمكن من تشغيل Minecraft 1.17 أو أحدث، يمكنك محاولة تغيير "المُصيِّر" إلى "Mesa LLVMpipe" في "الإعدادات العامة/الخاصة بالنسخة ← الإعدادات المتقدمة" لاستخدام معالجة المعالج المركزي لتوافق أفضل.
fatal.unsupported_platform.loongarch=وفّر Hello Minecraft! Launcher دعماً لمنصة Loongson.\n\
إذا واجهت مشاكل أثناء اللعب، يمكنك زيارة https://docs.hmcl.net/groups.html للحصول على مساعدة.
fatal.unsupported_platform.macos_arm64=وفّر Hello Minecraft! Launcher دعماً لمنصة Apple silicon، باستخدام Java ARM الأصلي لتشغيل الألعاب للحصول على تجربة لعب أكثر سلاسة.\n\
إذا واجهت مشاكل أثناء اللعب، قد يوفر تشغيل اللعبة بـ Java المبني على معمارية x86-64 توافقاً أفضل.
fatal.unsupported_platform.windows_arm64=وفّر Hello Minecraft! Launcher دعماً أصلياً لمنصة Windows on Arm. إذا واجهت مشاكل أثناء اللعب، يُرجى محاولة تشغيل اللعبة بـ Java المبني على معمارية x86.\n\
\n\
إذا كنت تستخدم منصة <b>Qualcomm</b>، قد تحتاج إلى تثبيت <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">حزمة توافق OpenGL</a> قبل اللعب.\n\
@@ -1218,7 +1209,6 @@ world.load.fail=فشل تحميل العالم
world.locked=قيد الاستخدام
world.locked.failed=العالم قيد الاستخدام حالياً. يُرجى إغلاق اللعبة والمحاولة مجدداً.
world.manage=العوالم
world.manage.button=إدارة العوالم
world.manage.title=العالم - %s
world.name=اسم العالم
world.name.enter=أدخل اسم العالم
@@ -1342,10 +1332,6 @@ settings.advanced.natives_directory.choose=اختر موقع المكتبة ال
settings.advanced.natives_directory.custom=مخصص
settings.advanced.natives_directory.default=افتراضي
settings.advanced.natives_directory.default.version_id=<اسم النسخة>
settings.advanced.natives_directory.hint=هذا الخيار مخصص فقط لمستخدمي Apple silicon والمنصات غير المدعومة رسمياً. يُرجى عدم تعديل هذا الخيار إلا إذا كنت تعرف ما تفعله.\n\
\n\
قبل المتابعة، يُرجى التأكد من توفر جميع المكتبات (مثل lwjgl.dll وlibopenal.so) في المجلد المحدد.\n\
ملاحظة: يُنصح باستخدام مسار يحتوي على حروف إنجليزية فقط لملف المكتبة المحلية المحدد، وإلا قد يتسبب ذلك في فشل تشغيل اللعبة.
settings.advanced.no_jvm_args=عدم إضافة وسيطات JVM الافتراضية
settings.advanced.no_optimizing_jvm_args=عدم إضافة وسيطات تحسين JVM الافتراضية
settings.advanced.precall_command=أمر ما قبل التشغيل
@@ -1365,18 +1351,15 @@ settings.advanced.post_exit_command=أمر ما بعد الإغلاق
settings.advanced.post_exit_command.prompt=أوامر تُنفَّذ بعد إغلاق اللعبة
settings.advanced.renderer=المُصيِّر
settings.advanced.renderer.default=افتراضي
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (أداء وتوافق ضعيفان)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=برمجي (أداء ضعيف، توافق أفضل)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (أفضل أداء، توافق ضعيف)
settings.advanced.server_ip=عنوان الخادم
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
settings.advanced.use_native_glfw=[Linux/FreeBSD فقط] استخدام GLFW النظام
settings.advanced.use_native_openal=[Linux/FreeBSD فقط] استخدام OpenAL النظام
settings.advanced.use_native_glfw=استخدام GLFW النظام
settings.advanced.use_native_openal=استخدام OpenAL النظام
settings.advanced.linux_freebsd_only=Linux/FreeBSD فقط
settings.advanced.workaround=حلول بديلة
settings.advanced.workaround.warning=خيارات الحلول البديلة مخصصة للمستخدمين المتقدمين فقط. قد يتسبب تعديل هذه الخيارات في تعطل اللعبة. يُرجى عدم تعديل هذه الخيارات إلا إذا كنت تعرف ما تفعله.
settings.advanced.wrapper_launcher=أمر التغليف
@@ -1592,7 +1575,6 @@ update.found=تحديث متاح!
update.newest_version=أحدث إصدار: %s
update.bubble.title=تحديث متاح: %s
update.bubble.subtitle=انقر هنا للتحديث
update.note=قد تحتوي القنوات التجريبية والليلية على مزيد من الميزات والإصلاحات، لكنها تأتي أيضاً بمزيد من المشاكل المحتملة.
update.latest=هذا هو أحدث إصدار
update.no_browser=لا يمكن الفتح في متصفح النظام. لكننا نسخنا الرابط إلى حافظتك، ويمكنك فتحه يدوياً.
update.tooltip=تحديث
@@ -336,10 +336,6 @@ download.failed.empty=No hay versiones disponibles, por favor haga clic aquí pa
download.failed.no_code=No se ha podido descargar
download.failed.refresh=No se ha podido obtener la lista de versiones. Por favor, haga clic aquí para volver a intentarlo.
download.game=Nuevo juego
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.mojang=Oficial
download.provider.mojang.desc=OptiFine es proporcionado por BMCLAPI
download.provider.official=De fuentes oficiales
download.provider.balanced=De la fuente más rápida disponible
download.provider.mirror=Desde espejo
@@ -418,8 +414,6 @@ fatal.unsupported_platform=Minecraft aún no es totalmente compatible con tu pla
Si no puedes iniciar Minecraft 1.17 y versiones posteriores, puedes intentar cambiar el «Renderizador» a «Mesa LLVMpipe» en «Configuración global/específica de la instancia → Configuración avanzada» para utilizar el renderizado por CPU y mejorar la compatibilidad.
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher ha prestado apoyo a la plataforma Loongson.\n\
Si tienes problemas al jugar, puedes visitar https://docs.hmcl.net/groups.html para obtener ayuda.
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher ha proporcionado soporte para la plataforma de chips de Apple, utilizando Java nativo de ARM para ejecutar juegos y conseguir una experiencia de juego más fluida.\n\
Si tienes problemas al jugar a un juego, ejecutarlo con Java de arquitectura x86-64 puede tener mejor compatibilidad.
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher ha proporcionado soporte nativo para la plataforma Windows en Arm. Si tiene problemas al jugar a un juego, intente iniciarlo con Java de arquitectura x86.\n\
\n\
Si utilizas la plataforma <b>Qualcomm</b>, es posible que tengas que instalar el <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">paquete de compatibilidad OpenGL</a> antes de jugar.\n\
@@ -1138,7 +1132,6 @@ world.info.random_seed=Semilla
world.locked=En uso
world.locked.failed=El mundo está actualmente en uso. Por favor, cierra el juego e inténtalo de nuevo.
world.manage=Mundos
world.manage.button=Administrar
world.manage.title=Mundo - %s
world.name=Nombre del mundo
world.name.enter=Introducir el nombre del mundo
@@ -1252,9 +1245,6 @@ settings.advanced.natives_directory.choose=Elija la ubicación de la biblioteca
settings.advanced.natives_directory.custom=Personalizado
settings.advanced.natives_directory.default=Por defecto
settings.advanced.natives_directory.default.version_id=<Nombre de la instancia>
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
settings.advanced.no_optimizing_jvm_args=No añadir argumentos de optimización de JVM
settings.advanced.precall_command=Comando pre-lanzamiento:
@@ -1269,18 +1259,15 @@ settings.advanced.post_exit_command=Comando post-salida
settings.advanced.post_exit_command.prompt=El comando se ejecuta después de que el juego se detenga
settings.advanced.renderer=Renderizador
settings.advanced.renderer.default=Por defecto
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Rendimiento y compatibilidad deficientes)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Software (Bajo rendimiento, máxima compatibilidad)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Máximo rendimiento, baja compatibilidad)
settings.advanced.server_ip=Dirección del servidor
settings.advanced.server_ip.prompt=Entrar automáticamente después de ejecutar el juego
settings.advanced.unsupported_system_options=Configuración no aplicable al sistema actual
settings.advanced.use_native_glfw=[Sólo Linux/FreeBSD] Utilizar GLFW nativo
settings.advanced.use_native_openal=[Sólo Linux/FreeBSD] Utilizar OpenAL nativo
settings.advanced.use_native_glfw=Utilizar GLFW nativo
settings.advanced.use_native_openal=Utilizar OpenAL nativo
settings.advanced.linux_freebsd_only=Sólo Linux/FreeBSD
settings.advanced.workaround=Métodos alternativos
settings.advanced.workaround.warning=Éstas opciones están pensadas sólo para usuarios expertos. Jugar con estas opciones puede romper el juego. A menos que sepas lo que estás haciendo, por favor no modifiques estas opciones.
settings.advanced.wrapper_launcher=Comando Wrapper
@@ -1401,7 +1388,6 @@ update.found=¡Actualización disponible!
update.newest_version=Última versión: %s
update.bubble.title=Actualización disponible: %s
update.bubble.subtitle=Haga clic aquí para actualizar
update.note=Advertencia: Las versiones beta y las versiones nocturnas pueden tener más funciones o correcciones, pero también tienen más problemas potenciales.
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
@@ -293,10 +293,6 @@ download.failed=%1$s のダウンロードに失敗しました、応答コー
download.failed.empty=候補者なし。戻るにはここをクリックしてください。
download.failed.refresh=バージョンリストをダウンロードできません。ここをクリックして再試行してください。
download.game=ゲームのダウンロード
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=ミラーソース
download.provider.mojang=Mojang
download.provider.mojang.desc=OptiFineはBMCLAPIにより提供されます
download.provider.official=公式ソースからロード
download.provider.balanced=mcbbsのソースからロード
download.provider.mirror=ミラーソースからロード
@@ -778,7 +774,6 @@ 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=このオプションは、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=ゲーム開始前に実行されます
@@ -792,18 +787,15 @@ settings.advanced.post_exit_command=終了後のコマンド
settings.advanced.post_exit_command.prompt=ゲーム終了後に実行されます
settings.advanced.renderer=レンダラー
settings.advanced.renderer.default=デフォルト
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (低パフォーマンス、低互換性)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=ソフトウェア (低速だが、互換性は最高)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (最高パフォーマンス、低互換性)
settings.advanced.server_ip=サーバーアドレス
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
settings.advanced.unsupported_system_options=サポートされていないシステムオプション
settings.advanced.use_native_glfw=[Linux/FreeBSDのみ]システムGLFWを使用する
settings.advanced.use_native_openal=[Linux/FreeBSDのみ]システムOpenALを使用する
settings.advanced.use_native_glfw=システムGLFWを使用する
settings.advanced.use_native_openal=システムOpenALを使用する
settings.advanced.linux_freebsd_only=Linux/FreeBSDのみ
settings.advanced.workaround=デバッグ用オプション
settings.advanced.workaround.warning=デバッグオプションはプロフェッショナルのみ使用可能です。デバッグオプションにより、ゲームが起動しない場合があります。これらのオプションは、ご自分が何をしているのかが分からない限り、変更しないでください。
settings.advanced.wrapper_launcher=パッキングオーダー
@@ -911,7 +903,6 @@ update.found=アップデートが利用可能です!
update.newest_version=最新バージョン:%s
update.bubble.title=利用可能な更新:%s
update.bubble.subtitle=更新するにはここをクリックしてください
update.note=警告:ベータ版には、より多くの機能とバグ修正が含まれている可能性がありますが、エラーが発生する可能性もあります。
update.latest=これは最新バージョンです。
update.no_browser=ブラウザを開くことができません。リンクがクリップボードにコピーされました。ブラウザのアドレスバーに貼り付けて更新します。
update.tooltip=更新
@@ -332,10 +332,6 @@ download.failed.empty=[無可裝之版,點此返]\n(君可求助於右上之
download.failed.no_code=引之未成
download.failed.refresh=[載版列未成,點此重試]\n(君可求助於右上之鈕)
download.game=新戲
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93,https://bmclapi2.bangbang93.com
download.provider.mojang=官版
download.provider.mojang.desc=Optifine 猶取諸 BMCLAPI 也
download.provider.official=先官版
download.provider.official.desc=至新,容有艱
download.provider.balanced=先其快者
@@ -385,7 +381,6 @@ fatal.illegal_char=君之案夾名有諱文「=」,是以外載之戶簿、離
fatal.unsupported_platform=礦藝未盡適君之算機,是以戲事或損,至於不能啟。\n\
礦藝一點一七以晉之不能啟,或於「(全例/例殊) 戲設 → 進階置設 → 勘誤置設」改「繪製之器」以「Mesa LLVMpipe」,或能兼之。\n君可求助於右上之鈕。
fatal.unsupported_platform.loongarch=HMCL 既適龍芯。\n凡有謬,遽求助於右上之鈕。
fatal.unsupported_platform.macos_arm64=HMCL 既適蘋矽。宜啟以 ARM 之爪哇,以益君之戲事。\n誠有謬,宜啟以 x86-64 之爪哇,以益其兼。\n凡有謬,遽求助於右上之鈕。
fatal.unsupported_platform.windows_arm64=HMCL 既適 ARM 之視窗。誠有謬,宜啟以 x86 之爪哇。\n誠用<b>栝柑</b>,或須置<a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">開圖庫兼囊</a>,而後可戯。擊鏈以置之於微軟貨舍。\n君可求助於右上之鈕。
file=案
@@ -579,6 +574,8 @@ launch.advice.forge37_0_60=鍛三七點〇點五九與舊者弗兼爪哇十七
launch.advice.java8_1_13=礦藝一點一三以晉但行於爪哇八與晉者。宜行以爪哇八與晉者。
launch.advice.java8_51_1_13=爪哇之前一點八點〇之五一者或崩礦藝一點一三。宜新爪哇於一點八點〇之五一以晉,然後復啟。
launch.advice.java9=礦藝之前一點一三而有改囊者弗見兼於爪哇九與晉者。宜啟以爪哇八。
launch.advice.lwjgl_3_4_1=戲之是版有已知之謬,行戲之時或致頓滯。\nHMCL 可假爪哇 Agent 自優其戲,以弭是患。君其許 HMCL 自優其戲乎?\n誠有謬,可至於「置設 → 貫用」,廢「許改戲」之項。
launch.advice.lwjgl_3_4_1.title=君其許 HMCL 自優其戲乎?
launch.advice.modded_java=改囊或弗見兼於爪哇之新者。宜以爪哇 %s 啟礦藝 %s。
launch.advice.modlauncher8=鍛版之所行弗兼於爪哇之所行。宜新鍛。
launch.advice.newer_java=余識君之啟戲以舊爪哇也,蓋改囊或是以崩戲也。宜新爪哇於八,然後復啟。
@@ -949,8 +946,8 @@ world.info.player.xp_level=經驗之層
world.info.random_seed=種
world.locked=見用
world.manage=司生界
world.manage.button=司生界
world.manage.title=司生界 - %s
world.manage.button=司此生界
world.manage.title=生界 - %s
world.name=生界之名
world.name.enter=書生界之名
world.show_all=全顯
@@ -1064,7 +1061,6 @@ settings.advanced.natives_directory.choose=擇府庫之徑
settings.advanced.natives_directory.custom=自定 (賴君所供之府庫也)
settings.advanced.natives_directory.default=本 (賴啟者所供之府庫)
settings.advanced.natives_directory.default.version_id=<例名>
settings.advanced.natives_directory.hint=供於械網之弗見適者,有如蘋矽,以自定府庫也。微其之知,毋改是,改則或弗啟。\n\n將改,而宜錄府庫於是,蓋 lwjgl.dll (liblwjgl.so)、openal.dll (libopenal.so) 之類。啟者弗補是府庫也!\n\n誡:府庫之徑,但宜書以英文、數、下綫,否則或弗啟。
settings.advanced.no_jvm_args=無添爪哇虛機之本通弦
settings.advanced.precall_command=令於戲前
settings.advanced.precall_command.prompt=將前戲啟而行
@@ -1083,18 +1079,15 @@ settings.advanced.post_exit_command=令於戲訖
settings.advanced.post_exit_command.prompt=將後戲訖而行
settings.advanced.renderer=繪器
settings.advanced.renderer.default=本
settings.advanced.renderer.default.desc=開圖庫
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (效與所適皆差,勘誤之用)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=軟繪器 (效差,所適至)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (效至,所適差)
settings.advanced.server_ip=伺服器之址
settings.advanced.server_ip.prompt=本之址,啟訖徑入是伺服器
settings.advanced.unsupported_system_options=不逮今之械網所選
settings.advanced.use_native_glfw=[惟磷若/FreeBSD] 用 GLFW
settings.advanced.use_native_openal=[惟磷若/FreeBSD] 用開聲庫
settings.advanced.use_native_glfw=用 GLFW
settings.advanced.use_native_openal=用開聲庫
settings.advanced.linux_freebsd_only=惟磷若/FreeBSD
settings.advanced.workaround=勘誤置設
settings.advanced.workaround.warning=勘誤置設者,蓋專者之所行也。苟妄改之,戲或弗啟。微其之知,無復改焉!
settings.advanced.wrapper_launcher=包裝之令
@@ -1126,6 +1119,8 @@ settings.game.working_directory.hint=夫于「運徑」擇「例獨」者,可
settings.icon=戲徵
settings.launcher=啟者置設
settings.launcher.allow_auto_agent=許改戲
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感。
settings.launcher.appearance=外觀
settings.launcher.common_path.tooltip=啟者置戲資與府庫於是。倘戲有成案,則弗取公庫之案。
settings.launcher.debug=勘誤
@@ -1222,7 +1217,6 @@ update.found=尋得新者
update.newest_version=至新:%s
update.bubble.title=尋得更新:%s
update.bubble.subtitle=是擊以晉
update.note=开發版與預版,虽增功、修阙,然亦恐生他患。
update.latest=當版至新
update.no_browser=無啟瀏覽器。址固鈔訖,君可自粘之以啟頁。
update.tooltip=迭更
@@ -333,10 +333,6 @@ download.failed.empty=Версий нет. Нажмите здесь, чтобы
download.failed.no_code=Не удалось скачать
download.failed.refresh=Не удалось получить список версий. Нажмите здесь, чтобы повторить попытку.
download.game=Новая игра
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.mojang=Официальный
download.provider.mojang.desc=OptiFine предоставляется BMCLAPI
download.provider.official=Из официальных источников
download.provider.balanced=Из самых быстрых доступных
download.provider.mirror=Из зеркала
@@ -412,8 +408,6 @@ fatal.unsupported_platform=Minecraft еще не полностью поддер
Если вы не можете запустить Minecraft версии 1.17 и новее, попробуйте переключить «Рендерер» на «Mesa LLVMpipe» в разделе «Глобальные/настройки экземпляра → Расширенные настройки», чтобы использовать рендеринг через CPU для лучшей совместимости.
fatal.unsupported_platform.loongarch=Лаунчер обеспечил поддержку платформы Loongson.\n\
Если у вас возникнут проблемы во время игры, вы можете обратиться за помощью на сайт https://docs.hmcl.net/groups.html.
fatal.unsupported_platform.macos_arm64=Лаунчер обеспечил поддержку платформы Apple silicon, используя родную ARM Java для запуска игр, чтобы получить более плавный игровой опыт.\n\
Если вы столкнулись с проблемами во время игры, запуск игры с Java на базе архитектуры x86-64 может обеспечить лучшую совместимость.
fatal.unsupported_platform.windows_arm64=Лаунчер обеспечил нативную поддержку платформы Windows на архитектуре Arm. Если у вам возникли проблемы во время игры, попробуйте запустить игру с Java на базе архитектуры x86.\n\
\n\
Если вы используете платформу <b>Qualcomm</b>, вам может потребоваться установить <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">пакет совместимости OpenGL</a> перед началом игры.\n\
@@ -1130,7 +1124,6 @@ world.info.random_seed=Ключ генератора мира
world.locked=В эксплуатации
world.locked.failed=В настоящее время мир находится в эксплуатации. Закройте игру и попробуйте снова.
world.manage=Миры
world.manage.button=Управлять
world.manage.title=Мир - %s
world.name=Название мира
world.name.enter=Введите название мира
@@ -1243,10 +1236,6 @@ settings.advanced.natives_directory.choose=Выберите расоложени
settings.advanced.natives_directory.custom=Пользовательское
settings.advanced.natives_directory.default=По умолчанию
settings.advanced.natives_directory.default.version_id=<Название сборки>
settings.advanced.natives_directory.hint=Параметр предназначен только для пользователей Apple silicon или других официально не поддерживаемых платформ. Не изменяйте, если не знаете что делаете.\n\
\n\
Прежде чем продолжить, убедитесь что все библиотеки (например, lwjgl.dll, libopenal.so) находятся в нужной папке.\n\
Примечание: Рекомендуется использовать полностью английские пути для указанных файлов локальных библиотек, иначе игра может не запуститься.
settings.advanced.no_jvm_args=Не добавлять аргументы JVM по умолчанию
settings.advanced.precall_command=Команда перед запуском
settings.advanced.precall_command.prompt=Команды, которые необходимо выполнить перед запуском игры
@@ -1260,18 +1249,15 @@ settings.advanced.post_exit_command=Команда после выхода
settings.advanced.post_exit_command.prompt=Команды, которые необходимо выполнить после выхода из игры
settings.advanced.renderer=Рендерер
settings.advanced.renderer.default=По умолчанию
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Низкая производительность и совместимость)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=ПО (Низкая производительность, лучшая совместимость)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Лучшая производительность, низкая совместимость)
settings.advanced.server_ip=Адрес сервера
settings.advanced.server_ip.prompt=Присоединяться к серверу при запуске игры
settings.advanced.unsupported_system_options=Настройки, не применимые к текущей системе
settings.advanced.use_native_glfw=[Только для Linux/FreeBSD] Использовать системный GLFW
settings.advanced.use_native_openal=[Только для Linux/FreeBSD] Использовать системный OpenAL
settings.advanced.use_native_glfw=Использовать системный GLFW
settings.advanced.use_native_openal=Использовать системный OpenAL
settings.advanced.linux_freebsd_only=Только для Linux/FreeBSD
settings.advanced.workaround=Обходные пути
settings.advanced.workaround.warning=Варианты обхода предназначены только для опытных пользователей. Изменение этих параметров может привести к вылету игры. Если не знаете, что делаете, то не изменяйте эти параметры.
settings.advanced.wrapper_launcher=Команда-оболочка
@@ -1391,7 +1377,6 @@ update.found=Доступно обновление!
update.newest_version=Последняя версия: %s
update.bubble.title=Доступно обновление: %s
update.bubble.subtitle=Кликните здесь для обновления
update.note=Предупреждение: Бета-версии могут иметь больше функциональных возможностей и исправлений ошибок, но также и больше возможных ошибок.
update.latest=Это последняя версия.
update.no_browser=Не удалось открыть браузер. Ссылка была скопирована в буфер обмена. Вставьте его в адресную строку браузера для обновления.
update.tooltip=Обновить
@@ -332,10 +332,6 @@ download.failed.empty=Немає доступних версій. Натисні
download.failed.no_code=Не вдалося завантажити
download.failed.refresh=Не вдалося отримати список версій. Натисніть тут, щоб повторити спробу.
download.game=Нова гра
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.mojang=Офіційне
download.provider.mojang.desc=OptiFine надається BMCLAPI
download.provider.official=З офіційних джерел
download.provider.balanced=З найшвидшого доступного
download.provider.mirror=З дзеркала
@@ -398,8 +394,6 @@ fatal.unsupported_platform=Minecraft ще не повністю підтриму
Якщо ви не можете запустити Minecraft версії 1.17 і новіші, спробуйте змінити «Рендерер» на «Mesa LLVMpipe» у розділі «Глобальні/налаштування екземпляра → Розширені налаштування», щоб використовувати рендеринг через CPU для кращої сумісності.
fatal.unsupported_platform.loongarch=Hello Minecraft! Лаунчер надав підтримку платформи Loongson.\n\
Якщо у вас виникли проблеми під час гри, ви можете відвідати https://docs.hmcl.net/groups.html для отримання допомоги.
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Лаунчер надав підтримку платформи Apple silicon, використовуючи нативну ARM Java для запуску ігор для отримання плавнішого ігрового досвіду.\n\
Якщо у вас виникли проблеми під час гри, запуск гри з Java на основі архітектури x86-64 може забезпечити кращу сумісність.
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Лаунчер надав нативну підтримку платформи Windows on Arm. Якщо у вас виникли проблеми під час гри, спробуйте запустити гру з Java на основі архітектури x86.\n\
Якщо ви використовуєте платформу <b>Qualcomm</b>, вам може знадобитися встановити <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">Пакет сумісності OpenGL</a> перед грою.\n\
Натисніть посилання, щоб перейти до Microsoft Store та встановити пакет сумісності.
@@ -1078,7 +1072,6 @@ world.info.random_seed=Насіння
world.locked=Використовується
world.locked.failed=Світ наразі використовується. Закрийте гру та спробуйте знову.
world.manage=Світи
world.manage.button=Керування світами
world.manage.title=Світ - %s
world.name=Назва світу
world.name.enter=Введіть назву світу
@@ -1191,9 +1184,6 @@ settings.advanced.natives_directory.choose=Виберіть місце розт
settings.advanced.natives_directory.custom=Власний
settings.advanced.natives_directory.default=Типово
settings.advanced.natives_directory.default.version_id=<Назва екземпляра>
settings.advanced.natives_directory.hint=Ця опція призначена лише для користувачів Apple silicon або інших неофіційно підтримуваних платформ. Не редагуйте цю опцію, якщо ви не знаєте, що робите. \n\
Перед продовженням переконайтеся, що всі бібліотеки (наприклад, lwjgl.dll, libopenal.so) надано у вашому бажаному каталозі. \n\
Примітка: Рекомендується використовувати повністю англійські літери для вказаного локального файлу бібліотеки. Інакше це може призвести до невдалого запуску гри.
settings.advanced.no_jvm_args=Не додавати типові аргументи JVM
settings.advanced.precall_command=Команда перед запуском
settings.advanced.precall_command.prompt=Команди для виконання перед запуском гри
@@ -1207,18 +1197,15 @@ settings.advanced.post_exit_command=Команда після виходу
settings.advanced.post_exit_command.prompt=Команди для виконання після виходу з гри
settings.advanced.renderer=Рендерер
settings.advanced.renderer.default=По умолчанию
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (Погана продуктивність та сумісність)
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=Програмний (Погана продуктивність, найкраща сумісність)
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (Найкраща продуктивність, погана сумісність)
settings.advanced.server_ip=Адреса сервера
settings.advanced.server_ip.prompt=Автоматично приєднатися після запуску гри
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
settings.advanced.use_native_glfw=[Лише Linux/FreeBSD] Використовувати системний GLFW
settings.advanced.use_native_openal=[Лише Linux/FreeBSD] Використовувати системний OpenAL
settings.advanced.use_native_glfw=Використовувати системний GLFW
settings.advanced.use_native_openal=Використовувати системний OpenAL
settings.advanced.linux_freebsd_only=Лише Linux/FreeBSD
settings.advanced.workaround=Обхідні шляхи
settings.advanced.workaround.warning=Опції обхідних шляхів призначені лише для досвідчених користувачів. Налаштування цих опцій може призвести до збою гри. Якщо ви не знаєте, що робите, не редагуйте ці опції.
settings.advanced.wrapper_launcher=Команда обгортки
@@ -1342,7 +1329,6 @@ update.found=Доступне оновлення!
update.newest_version=Остання версія: %s
update.bubble.title=Доступне оновлення: %s
update.bubble.subtitle=Натисніть тут, щоб оновити
update.note=Канали Бета та Нічний можуть мати більше функцій або виправлень, але вони також мають більше потенційних проблем.
update.latest=Це остання версія
update.no_browser=Не вдається відкрити в системному браузері. Але ми скопіювали посилання до вашого буфера обміну, і ви можете відкрити його вручну.
update.tooltip=Оновити
@@ -164,6 +164,9 @@ account.skin.upload.failed=外觀上傳失敗
account.skin.invalid_skin=無法識別的外觀檔案
account.username=使用者名稱
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (快照)
archive.author=作者
archive.date=發布日期
archive.file.name=檔案名稱
@@ -356,16 +359,16 @@ download.failed.empty=沒有可供安裝的版本,按一下此處返回。
download.failed.no_code=下載失敗
download.failed.refresh=載入版本清單失敗,按一下此處重試。
download.game=新遊戲
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.bmclapi=BMCLAPI & MCIM
download.provider.bmclapi.desc=適用於中國大陸的鏡像源
download.provider.mojang=官方伺服器
download.provider.mojang.desc=OptiFine 由 BMCLAPI 提供
download.provider.official=盡量使用官方源
download.provider.official.desc=最新,但可能載入慢
download.provider.balanced=選取載入速度快的下載源
download.provider.balanced.desc=平衡,但可能不是最新
download.provider.mojang.desc=注意:OptiFine 的下載由 BMCLAPI 提供
download.provider.official=盡量使用官方來源
download.provider.official.desc=優先從官方來源獲取資訊
download.provider.balanced=選取載入速度快的下載來源
download.provider.balanced.desc=根據你的網路連線狀況選用最佳來源
download.provider.mirror=盡量使用鏡像源
download.provider.mirror.desc=載入快,但可能不是最新
download.provider.mirror.desc=推薦中國大陸使用者使用
download.java=下載 Java
download.java.process=下載 Java
download.javafx=正在下載必要的執行時元件
@@ -416,8 +419,9 @@ fatal.illegal_char=由於您的執行路徑中存在無效字元『=』,您將
fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n\
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「Mesa LLVMpipe」,以獲得更好的相容性。
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已為 Apple Silicon 平臺提供支援。使用 ARM 原生 Java 啟動遊戲以獲得更流暢的遊戲體驗。\n如果你在遊戲中遭遇問題,使用 x86-64 架構的 Java 啟動遊戲可能有更好的相容性。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
# When translating the following string into non-Latin languages, please retain the English translation
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其衍生版本執行 HMCL。\nHMCL 是跨平臺的,我們建議您直接在主機系統上執行。\n按下「確定」以繼續,但請注意,此操作可能會引發一些問題。
file=檔案
@@ -561,6 +565,7 @@ install.new_game.already_exists=此實例已經存在,請重新命名
install.new_game.current_game_version=目前遊戲實例
install.new_game.installation=安裝新實例
install.new_game.malformed=名稱無效
install.new_game.malformed_json=無效的 Minecraft 實例 JSON 文件。
install.select=請選取安裝方式
install.success=安裝成功
@@ -612,6 +617,8 @@ launch.advice.forge37_0_60=Forge 37.0.59 及更低版本與 Java 17 不相容。
launch.advice.java8_1_13=Minecraft 1.13 及更高版本僅支援 Java 8 或更高版本。請使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議你到 https://java.com 安裝最新版的 Java 8。
launch.advice.java9=低於 (包含) 1.13 的有安裝模組的 Minecraft 版本不支援 Java 9 或更高版本。請使用 Java 8。
launch.advice.lwjgl_3_4_1=當前遊戲版本存在已知問題,可能在遊戲中發生卡頓。\nHMCL 可以 (透過 Java Agent) 自動最佳化遊戲解決此問題。是否允許 HMCL 自動最佳化遊戲?\n如遇到問題,你可以在「設定 → 一般」中關閉「允許 HMCL 修改遊戲」選項。
launch.advice.lwjgl_3_4_1.title=是否允許 HMCL 自動最佳化遊戲?
launch.advice.modded_java=部分模組可能與高版本 Java 不相容。建議使用 Java %s 啟動 Minecraft %s。
launch.advice.modlauncher8=你所使用的 Forge 版本與目前使用的 Java 不相容。請更新 Forge。
launch.advice.newer_java=偵測到你正在使用舊版本 Java 啟動遊戲,這可能導致部分模組引發遊戲崩潰。建議更新至 Java 8 後再次啟動。
@@ -1018,8 +1025,8 @@ world.locked=使用中
world.locked.failed=該世界正在使用中,請關閉遊戲後重試。
world.game_version=遊戲版本
world.manage=世界管理
world.manage.button=世界管理
world.manage.title=世界管理 - %s
world.manage.button=管理此世界
world.manage.title=世界 - %s
world.name=世界名稱
world.name.enter=輸入世界名稱
world.show_all=全部顯示
@@ -1119,6 +1126,15 @@ 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.graphics_backend=圖形 API
settings.advanced.graphics_backend.default=預設
settings.advanced.graphics_backend.default.desc=遵循遊戲設定
settings.advanced.graphics_backend.opengl=OpenGL
settings.advanced.graphics_backend.opengl.desc=使用 OpenGL 渲染
settings.advanced.graphics_backend.vulkan=Vulkan
settings.advanced.graphics_backend.vulkan.desc=使用 Vulkan 渲染
settings.advanced.graphics_backend.vulkan.desc.global=使用 Vulkan 渲染 (適用於 Minecraft 26.2 及更高版本)
settings.advanced.graphics_backend.vulkan.desc.unsupported=使用 Vulkan 渲染 (不適用於當前遊戲版本)
settings.advanced.java_permanent_generation_space=記憶體永久儲存區域
settings.advanced.java_permanent_generation_space.prompt=單位 MiB
settings.advanced.jvm=Java 虛擬機設定
@@ -1138,7 +1154,6 @@ settings.advanced.natives_directory.choose=選取本機庫路徑
settings.advanced.natives_directory.custom=自訂 (由你提供遊戲需要的本機庫)
settings.advanced.natives_directory.default=預設 (由啟動器提供遊戲本機庫)
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.no_optimizing_jvm_args=不自動新增 Java 虛擬機最佳化參數
settings.advanced.precall_command=遊戲啟動前執行指令
@@ -1156,20 +1171,55 @@ settings.advanced.process_priority.high=高
settings.advanced.process_priority.high.desc=優先保證遊戲執行,但可能會導致其他程式卡頓
settings.advanced.post_exit_command=遊戲結束後執行指令
settings.advanced.post_exit_command.prompt=將在遊戲結束後呼叫使用
settings.advanced.renderer=繪製器
settings.advanced.renderer=繪製器/驅動
settings.advanced.renderer.default=預設
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (效能與相容性較差,用於除錯)
settings.advanced.renderer.default.desc=使用系統預設繪製器
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=軟體 Vulkan 繪製器
settings.advanced.renderer.dozen=Mesa Dozen
settings.advanced.renderer.dozen.desc=基於 DirectX 12 的 Vulkan 繪製器 (實驗性)
settings.advanced.renderer.nvidia_vulkan=NVIDIA
settings.advanced.renderer.nvidia_vulkan.desc=NVIDIA GPU 的官方 Vulkan 驅動
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
settings.advanced.renderer.nvidia_nvk.desc=NVIDIA GPU 的開源 Vulkan 驅動
settings.advanced.renderer.amdvlk=AMD
settings.advanced.renderer.amdvlk.desc=AMD GPU 的官方 Vulkan 驅動
settings.advanced.renderer.amd_radv=AMD (RADV)
settings.advanced.renderer.amd_radv.desc=AMD GPU 的開源 Vulkan 驅動
settings.advanced.renderer.intel_vulkan=Intel
settings.advanced.renderer.intel_vulkan.desc=Intel GPU 的官方 Vulkan 驅動
settings.advanced.renderer.intel_anv=Intel (ANV)
settings.advanced.renderer.intel_anv.desc=Intel GPU 的開源 Vulkan 驅動
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
settings.advanced.renderer.intel_hasvk.desc=Intel 舊 GPU 的開源 Vulkan 驅動
settings.advanced.renderer.qualcomm=高通
settings.advanced.renderer.qualcomm.desc=高通 Adreno GPU 的官方 Vulkan 驅動
settings.advanced.renderer.turnip=高通 (Turnip)
settings.advanced.renderer.turnip.desc=高通 Adreno GPU 的開源 Vulkan 驅動
settings.advanced.renderer.moltenvk=MoltenVK
settings.advanced.renderer.moltenvk.desc=基於 Metal 的 Vulkan 繪製器
settings.advanced.renderer.kosmickrisp=KosmicKrisp
settings.advanced.renderer.kosmickrisp.desc=基於 Metal 4 的 Vulkan 繪製器 (實驗性)
settings.advanced.renderer.powervr=PowerVR
settings.advanced.renderer.powervr.desc=PowerVR GPU 的開源 Vulkan 驅動 (實驗性)
settings.advanced.renderer.panvk=PanVK
settings.advanced.renderer.panvk.desc=Arm Mali GPU 的開源 Vulkan 驅動 (實驗性)
settings.advanced.renderer.v3dv=V3DV
settings.advanced.renderer.v3dv.desc=樹莓派 4/5 GPU 的開源 Vulkan 驅動 (實驗性)
# OpenGL Renderers
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=軟繪製器 (效能較差,相容性最好)
settings.advanced.renderer.llvmpipe.desc=軟體 OpenGL 繪製器
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=基於 DirectX 12 的 OpenGL 繪製器
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (效能最好,相容性較差)
settings.advanced.renderer.zink.desc=基於 Vulkan 的 OpenGL 繪製器
settings.advanced.server_ip=伺服器位址
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
settings.advanced.unsupported_system_options=不適用於目前系統的選項
settings.advanced.use_native_glfw=[僅限 Linux/FreeBSD] 使用系統 GLFW
settings.advanced.use_native_openal=[僅限 Linux/FreeBSD] 使用系統 OpenAL
settings.advanced.use_native_glfw=使用系統 GLFW
settings.advanced.use_native_openal=使用系統 OpenAL
settings.advanced.linux_freebsd_only=僅限 Linux/FreeBSD
settings.advanced.workaround=除錯選項
settings.advanced.workaround.warning=除錯選項僅提供給專業玩家使用。修改除錯選項可能會導致遊戲無法啟動。除非你知道你在做什麼,否則請不要修改這些選項。
settings.advanced.wrapper_launcher=前置指令
@@ -1201,6 +1251,8 @@ settings.game.working_directory.hint=在「執行路徑」選項中選取「各
settings.icon=遊戲圖示
settings.launcher=啟動器設定
settings.launcher.allow_auto_agent=允許 HMCL 修改遊戲
settings.launcher.allow_auto_agent.subtitle=允許 HMCL 透過附加 Java Agent 修改遊戲以改善遊戲體驗。
settings.launcher.appearance=外觀
settings.launcher.brightness=主題模式
settings.launcher.brightness.auto=跟隨系統設定
@@ -1216,6 +1268,7 @@ settings.launcher.download.threads.auto=自動選取執行緒數
settings.launcher.download.threads.hint=執行緒數過高可能導致系統卡頓。你的下載速度會受到網際網路運營商、下載來源伺服器等方面的影響。調高下載執行緒數不一定能大幅提升總下載速度。
settings.launcher.download_source=下載來源
settings.launcher.download_source.auto=自動選取下載來源
settings.launcher.default_addon_source=遊戲內容預設下載來源
settings.launcher.enable_game_list=在首頁內顯示遊戲清單
settings.launcher.font=字體
settings.launcher.font.anti_aliasing=反鋸齒
@@ -1382,7 +1435,8 @@ update.found=發現到更新
update.newest_version=最新版本為:%s
update.bubble.title=發現更新:%s
update.bubble.subtitle=點選此處進行升級
update.note=開發版與預覽版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
update.note.stable=適合優先追求軟體穩定性的使用者使用。\n新功能在經過充分測試後才會被添加到穩定版中。
update.note.dev=適合希望優先體驗新功能的使用者使用。\n開發版會包含最新功能和錯誤修復,但未經充分測試,可能會存在更多問題。
update.latest=目前版本為最新版本
update.no_browser=無法開啟瀏覽器。網址已經複製到剪貼簿了,你可以手動複製網址開啟頁面。
update.tooltip=更新
@@ -166,6 +166,9 @@ account.skin.upload.failed=皮肤上传失败
account.skin.invalid_skin=无法识别的皮肤文件
account.username=用户名
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (快照)
archive.author=作者
archive.date=发布日期
archive.file.name=文件名
@@ -358,10 +361,10 @@ download.failed.empty=[没有可供安装的版本,点击此处返回]\n(你
download.failed.no_code=下载失败
download.failed.refresh=[加载版本列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助)
download.game=新游戏
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com
download.provider.bmclapi=BMCLAPI & MCIM
download.provider.bmclapi.desc=适用于中国大陆的镜像源
download.provider.mojang=官方
download.provider.mojang.desc=OptiFine 自动安装使用 BMCLAPI 下载源
download.provider.mojang.desc=注:OptiFine 自动安装使用 BMCLAPI 下载源
download.provider.official=尽量使用官方源
download.provider.official.desc=最新,但可能加载慢
download.provider.balanced=选择加载速度快的下载源
@@ -420,8 +423,9 @@ fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“Mesa LLVMpipe”,以获得更好的兼容性。\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如遇到问题,你可以点击右上角帮助按钮进行求助。
# When translating the following string into non-Latin languages, please retain the English translation
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其发行版运行 HMCL。\nHMCL 是跨平台的,我们建议您直接在宿主系统上运行。\n按下“确定”以继续,但请注意,此操作可能会引发一些问题。
file=文件
@@ -566,6 +570,7 @@ install.new_game.already_exists=此实例已经存在,请换一个名字
install.new_game.current_game_version=当前游戏实例
install.new_game.installation=安装新游戏
install.new_game.malformed=名字不合法
install.new_game.malformed_json=不是有效的 Minecraft 实例 JSON 文件。
install.select=请选择安装方式
install.success=安装成功
@@ -617,6 +622,8 @@ launch.advice.forge37_0_60=Forge 37.0.59 及更低版本与 Java 17 不兼容。
launch.advice.java8_1_13=Minecraft 1.13 及更高版本只能在 Java 8 或更高版本上运行。请使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议更新 Java 至 1.8.0_51 或更高版本后再次启动。
launch.advice.java9=低于 1.13 的有安装模组的 Minecraft 版本与 Java 9 或更高版本不兼容。请使用 Java 8。
launch.advice.lwjgl_3_4_1=当前游戏版本存在已知问题,可能在游戏中发生卡顿。\nHMCL 可以 (通过 Java Agent) 自动优化游戏解决此问题。是否允许 HMCL 自动优化游戏?\n如遇到问题,你可以在“设置 → 通用”中关闭“允许 HMCL 修改游戏”选项。
launch.advice.lwjgl_3_4_1.title=是否允许 HMCL 自动优化游戏?
launch.advice.modded_java=部分模组可能与高版本 Java 不兼容。建议使用 Java %s 启动 Minecraft %s。
launch.advice.modlauncher8=你所使用的 Forge 版本与当前使用的 Java 不兼容。请更新 Forge。
launch.advice.newer_java=检测到你正在使用旧版本 Java 启动游戏,这可能导致部分模组引发游戏崩溃。建议更新至 Java 8 后再次启动。
@@ -1023,8 +1030,8 @@ world.load.fail=世界加载失败
world.locked=使用中
world.locked.failed=该世界正在使用中,请关闭游戏后重试。
world.manage=世界管理
world.manage.button=世界管理
world.manage.title=世界管理 - %s
world.manage.button=管理此世界
world.manage.title=世界 - %s
world.name=世界名称
world.name.enter=输入世界名称
world.show_all=显示全部
@@ -1124,6 +1131,15 @@ 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.graphics_backend=图形 API
settings.advanced.graphics_backend.default=默认
settings.advanced.graphics_backend.default.desc=遵循游戏设置
settings.advanced.graphics_backend.opengl=OpenGL
settings.advanced.graphics_backend.opengl.desc=使用 OpenGL 渲染
settings.advanced.graphics_backend.vulkan=Vulkan
settings.advanced.graphics_backend.vulkan.desc=使用 Vulkan 渲染
settings.advanced.graphics_backend.vulkan.desc.global=使用 Vulkan 渲染 (适用于 Minecraft 26.2 及更高版本)
settings.advanced.graphics_backend.vulkan.desc.unsupported=使用 Vulkan 渲染 (不适用于当前游戏版本)
settings.advanced.java_permanent_generation_space=内存永久保存区域
settings.advanced.java_permanent_generation_space.prompt=单位 MiB
settings.advanced.jvm=Java 虚拟机设置
@@ -1143,7 +1159,6 @@ settings.advanced.natives_directory.choose=选择本地库路径
settings.advanced.natives_directory.custom=自定义 (由你提供游戏需要的本地库)
settings.advanced.natives_directory.default=默认 (由启动器提供游戏本地库)
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.no_optimizing_jvm_args=不自动添加 Java 虚拟机优化参数
settings.advanced.precall_command=游戏启动前执行命令
@@ -1161,20 +1176,55 @@ settings.advanced.process_priority.high=高
settings.advanced.process_priority.high.desc=优先保证游戏运行,但可能会导致其他程序卡顿
settings.advanced.post_exit_command=游戏结束后执行命令
settings.advanced.post_exit_command.prompt=将在游戏结束后调用
settings.advanced.renderer=渲染器
settings.advanced.renderer=渲染器/驱动
settings.advanced.renderer.default=默认
settings.advanced.renderer.default.desc=OpenGL
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=DirectX 12 (性能与兼容性较差,用于调试)
settings.advanced.renderer.default.desc=使用系统默认渲染器
# Vulkan Renderers
settings.advanced.renderer.lavapipe=Mesa Lavapipe
settings.advanced.renderer.lavapipe.desc=软件 Vulkan 渲染器
settings.advanced.renderer.dozen=Mesa Dozen
settings.advanced.renderer.dozen.desc=基于 DirectX 12 的 Vulkan 渲染器 (实验性)
settings.advanced.renderer.nvidia_vulkan=NVIDIA
settings.advanced.renderer.nvidia_vulkan.desc=NVIDIA GPU 的官方 Vulkan 驱动
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
settings.advanced.renderer.nvidia_nvk.desc=NVIDIA GPU 的开源 Vulkan 驱动
settings.advanced.renderer.amdvlk=AMD
settings.advanced.renderer.amdvlk.desc=AMD GPU 的官方 Vulkan 驱动
settings.advanced.renderer.amd_radv=AMD (RADV)
settings.advanced.renderer.amd_radv.desc=AMD GPU 的开源 Vulkan 驱动
settings.advanced.renderer.intel_vulkan=Intel
settings.advanced.renderer.intel_vulkan.desc=Intel GPU 的官方 Vulkan 驱动
settings.advanced.renderer.intel_anv=Intel (ANV)
settings.advanced.renderer.intel_anv.desc=Intel GPU 的开源 Vulkan 驱动
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
settings.advanced.renderer.intel_hasvk.desc=Intel 旧 GPU 的开源 Vulkan 驱动
settings.advanced.renderer.qualcomm=高通
settings.advanced.renderer.qualcomm.desc=高通 Adreno GPU 的官方 Vulkan 驱动
settings.advanced.renderer.turnip=高通 (Turnip)
settings.advanced.renderer.turnip.desc=高通 Adreno GPU 的开源 Vulkan 驱动
settings.advanced.renderer.moltenvk=MoltenVK
settings.advanced.renderer.moltenvk.desc=基于 Metal 的 Vulkan 渲染器
settings.advanced.renderer.kosmickrisp=KosmicKrisp
settings.advanced.renderer.kosmickrisp.desc=基于 Metal 4 的 Vulkan 渲染器 (实验性)
settings.advanced.renderer.powervr=PowerVR
settings.advanced.renderer.powervr.desc=PowerVR GPU 的开源 Vulkan 驱动 (实验性)
settings.advanced.renderer.panvk=PanVK
settings.advanced.renderer.panvk.desc=Arm Mali GPU 的开源 Vulkan 驱动 (实验性)
settings.advanced.renderer.v3dv=V3DV
settings.advanced.renderer.v3dv.desc=树莓派 4/5 GPU 的开源 Vulkan 驱动 (实验性)
# OpenGL Renderers
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
settings.advanced.renderer.llvmpipe.desc=软渲染器 (性能较差,兼容性最好)
settings.advanced.renderer.llvmpipe.desc=软件 OpenGL 渲染器
settings.advanced.renderer.d3d12=Mesa D3D12
settings.advanced.renderer.d3d12.desc=基于 DirectX 12 的 OpenGL 渲染器
settings.advanced.renderer.zink=Mesa Zink
settings.advanced.renderer.zink.desc=Vulkan (性能最好,兼容性较差)
settings.advanced.renderer.zink.desc=基于 Vulkan 的 OpenGL 渲染器
settings.advanced.server_ip=服务器地址
settings.advanced.server_ip.prompt=默认,启动游戏后可以直接进入对应服务器
settings.advanced.unsupported_system_options=不适用于当前系统的选项
settings.advanced.use_native_glfw=[仅 Linux/FreeBSD] 使用系统 GLFW
settings.advanced.use_native_openal=[仅 Linux/FreeBSD] 使用系统 OpenAL
settings.advanced.use_native_glfw=使用系统 GLFW
settings.advanced.use_native_openal=使用系统 OpenAL
settings.advanced.linux_freebsd_only=仅 Linux/FreeBSD
settings.advanced.workaround=调试选项
settings.advanced.workaround.warning=调试选项仅提供给专业玩家使用。调试选项可能会导致游戏无法启动。除非你知道你在做什么,否则请不要修改这些选项!
settings.advanced.wrapper_launcher=包装命令
@@ -1206,6 +1256,8 @@ settings.game.working_directory.hint=在“版本隔离”中选择“各实例
settings.icon=游戏图标
settings.launcher=启动器设置
settings.launcher.allow_auto_agent=允许 HMCL 修改游戏
settings.launcher.allow_auto_agent.subtitle=允许 HMCL 通过附加 Java Agent 修改游戏以改善游戏体验。
settings.launcher.appearance=外观
settings.launcher.brightness=主题模式
settings.launcher.brightness.auto=跟随系统设置
@@ -1221,6 +1273,7 @@ settings.launcher.download.threads.auto=自动选择线程数
settings.launcher.download.threads.hint=线程数过高可能导致系统卡顿。你的下载速度会受到互联网运营商、下载源服务器等方面的影响。调高下载线程数不一定能大幅提升总下载速度。
settings.launcher.download_source=下载源
settings.launcher.download_source.auto=自动选择下载源
settings.launcher.default_addon_source=游戏内容默认下载源
settings.launcher.enable_game_list=在主页内显示版本列表
settings.launcher.font=字体
settings.launcher.font.anti_aliasing=抗锯齿
@@ -1387,7 +1440,8 @@ update.found=发现更新
update.newest_version=最新版本为:%s
update.bubble.title=发现更新:%s
update.bubble.subtitle=点击此处进行升级
update.note=开发版与预览版包含更多的功能以及错误修复,但也可能会包含其他的问题。
update.note.stable=适合优先追求软件稳定性的用户使用。\n新功能在经过充分测试后才会被添加到稳定版中。
update.note.dev=适合希望优先体验新功能的用户使用。\n包含最新功能和错误修复,但未经充分测试,可能会存在更多问题。
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器。网址已经复制到剪贴板,你可以手动粘贴网址打开页面。
update.tooltip=更新
+205 -103
View File
@@ -781,17 +781,6 @@
}
}
},
"org.lwjgl:lwjgl:3.4.1": {
"name": "org.lwjgl:lwjgl:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
"sha1": "6105690714874b995aa7812e4f7732a21109276c",
"size": 1157957
}
}
},
"org.lwjgl:lwjgl:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl:3.4.1:natives-linux-arm64",
"downloads": {
@@ -803,17 +792,6 @@
}
}
},
"org.lwjgl:lwjgl-jemalloc:3.4.1": {
"name": "org.lwjgl:lwjgl-jemalloc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
"sha1": "16b663092854fc6adfe0a941e61cae2bb2e437b6",
"size": 47872
}
}
},
"org.lwjgl:lwjgl-jemalloc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-jemalloc:3.4.1:natives-linux-arm64",
"downloads": {
@@ -825,17 +803,6 @@
}
}
},
"org.lwjgl:lwjgl-openal:3.4.1": {
"name": "org.lwjgl:lwjgl-openal:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
"sha1": "8508bd60589de0539aa465b78fcf838efd07ae9a",
"size": 153919
}
}
},
"org.lwjgl:lwjgl-openal:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-openal:3.4.1:natives-linux-arm64",
"downloads": {
@@ -847,17 +814,6 @@
}
}
},
"org.lwjgl:lwjgl-opengl:3.4.1": {
"name": "org.lwjgl:lwjgl-opengl:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
"sha1": "830412cab823c029cda6b9729f9d2ed36cf79959",
"size": 937660
}
}
},
"org.lwjgl:lwjgl-opengl:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-opengl:3.4.1:natives-linux-arm64",
"downloads": {
@@ -869,17 +825,6 @@
}
}
},
"org.lwjgl:lwjgl-glfw:3.4.1": {
"name": "org.lwjgl:lwjgl-glfw:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
"sha1": "a782b1ddd175c9107bf300fd92920579ea65e2a4",
"size": 151893
}
}
},
"org.lwjgl:lwjgl-glfw:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-glfw:3.4.1:natives-linux-arm64",
"downloads": {
@@ -891,17 +836,6 @@
}
}
},
"org.lwjgl:lwjgl-stb:3.4.1": {
"name": "org.lwjgl:lwjgl-stb:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
"sha1": "f077c3dafc31924fbe8acb0b913f08977ba27f8e",
"size": 142927
}
}
},
"org.lwjgl:lwjgl-stb:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-stb:3.4.1:natives-linux-arm64",
"downloads": {
@@ -913,17 +847,6 @@
}
}
},
"org.lwjgl:lwjgl-tinyfd:3.4.1": {
"name": "org.lwjgl:lwjgl-tinyfd:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
"sha1": "562697d40dbd2ed0424bf24dcf65d51170f81adf",
"size": 15931
}
}
},
"org.lwjgl:lwjgl-tinyfd:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-tinyfd:3.4.1:natives-linux-arm64",
"downloads": {
@@ -935,17 +858,6 @@
}
}
},
"org.lwjgl:lwjgl-freetype:3.4.1": {
"name": "org.lwjgl:lwjgl-freetype:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
"sha1": "caa75a37e857efc9b0686e434d708d6420c5b6ea",
"size": 464342
}
}
},
"org.lwjgl:lwjgl-freetype:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-freetype:3.4.1:natives-linux-arm64",
"downloads": {
@@ -957,6 +869,39 @@
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-arm64.jar",
"sha1": "bc5c99b2eb7e2109c6db21cfced15a6851f8111b",
"size": 3397476
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-arm64.jar",
"sha1": "e2eaa8f516d43fe11b8253e6243b3d8e1315c9c8",
"size": 1139919
}
}
},
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-vma:3.4.1:natives-linux-arm64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-arm64.jar",
"sha1": "de40d43c5947c4a4f91376af8f2c00e5396cc109",
"size": 59715
}
}
},
"org.lwjgl.lwjgl:lwjgl-platform:2.9.0:natives": {
"name": "org.glavo.hmcl:lwjgl2-natives:2.9.3",
"downloads": {
@@ -2689,6 +2634,9 @@
"org.lwjgl:lwjgl-stb:3.4.1:natives-linux": null,
"org.lwjgl:lwjgl-tinyfd:3.4.1:natives-linux": null,
"org.lwjgl:lwjgl-freetype:3.4.1:natives-linux": null,
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": null,
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": null,
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": null,
"net.java.dev.jna:jna:5.8.0": {
"name": "net.java.dev.jna:jna:5.13.0",
"downloads": {
@@ -4359,6 +4307,83 @@
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.1": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
"sha1": "5c02c88f0d879529014c78463b76f43fdecfd272",
"size": 153051
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-riscv64.jar",
"sha1": "5598b0b86d4b3ca0ae78817719d5814efb9c7a77",
"size": 3147886
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.1": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
"sha1": "188553592e97b5ab2dba54a1b403ee80b48dad4c",
"size": 148285
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-riscv64.jar",
"sha1": "ea09f614c961b14962e4cc55a05c6767f9440f3f",
"size": 1215586
}
}
},
"org.lwjgl:lwjgl-vma:3.4.1": {
"name": "org.lwjgl:lwjgl-vma:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
"sha1": "2c54ce10154feb1ede4d23c5947438cdbfdcc003",
"size": 109209
}
}
},
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-vma:3.4.1:natives-linux-riscv64",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-riscv64.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-riscv64.jar",
"sha1": "9616440ee3cb2cfeacd99a61eefded856b5d7efe",
"size": 57091
}
}
},
"org.lwjgl:lwjgl-vulkan:3.4.1": {
"name": "org.lwjgl:lwjgl-vulkan:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
"sha1": "d0601fdc38890755e5638048c14cc0f7b990964b",
"size": 8356700
}
}
},
"com.github.oshi:oshi-core:6.6.5": {
"name": "com.github.oshi:oshi-core:6.8.0",
"downloads": {
@@ -4378,13 +4403,13 @@
},
"windows-x86_64": {
"mesa-loader": {
"name": "org.glavo:mesa-loader-windows:25.0.3:x64",
"name": "org.glavo:mesa-loader-windows:26.0.4:x64",
"downloads": {
"artifact": {
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x64.jar",
"sha1": "b2552fcc8c98e95b4559c39f2ff87cdb3aaaa513",
"size": 27971214
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x64.jar",
"sha1": "f8da709c59ef61f531c91434ca0e3b4f39202981",
"size": 49873846
}
}
},
@@ -4402,13 +4427,13 @@
},
"windows-x86": {
"mesa-loader": {
"name": "org.glavo:mesa-loader-windows:25.0.3:x86",
"name": "org.glavo:mesa-loader-windows:26.0.4:x86",
"downloads": {
"artifact": {
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x86.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x86.jar",
"sha1": "6fd0fd7da88ca6636f735e1e0f6feb7f62f59715",
"size": 23311299
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x86.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x86.jar",
"sha1": "ac6afaa8baa7c17468267c09e77e1296ee92d5ed",
"size": 41742113
}
}
}
@@ -4791,13 +4816,13 @@
"com.mojang:text2speech:1.12.4:natives": null,
"com.mojang:text2speech:1.13.9:natives-windows": null,
"mesa-loader": {
"name": "org.glavo:mesa-loader-windows:25.0.3:arm64",
"name": "org.glavo:mesa-loader-windows:26.0.4:arm64",
"downloads": {
"artifact": {
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-arm64.jar",
"sha1": "40ca5de3351bf48b7b9efa5d2598d5b6b4d1ab8e",
"size": 24905270
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-arm64.jar",
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-arm64.jar",
"sha1": "5c761a344700a07eaded51c5cf0cde36ee614706",
"size": 43504189
}
}
}
@@ -6325,6 +6350,83 @@
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.1": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
"sha1": "5c02c88f0d879529014c78463b76f43fdecfd272",
"size": 153051
}
}
},
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-shaderc:3.4.1:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-freebsd.jar",
"sha1": "872460f4b007ed3a13a2d311e90bda5370406fc8",
"size": 2953522
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.1": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
"sha1": "188553592e97b5ab2dba54a1b403ee80b48dad4c",
"size": 148285
}
}
},
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-spvc:3.4.1:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-freebsd.jar",
"sha1": "7a3d0e4a74f022b3196ecd33d9ebffafec7767f4",
"size": 1094646
}
}
},
"org.lwjgl:lwjgl-vma:3.4.1": {
"name": "org.lwjgl:lwjgl-vma:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
"sha1": "2c54ce10154feb1ede4d23c5947438cdbfdcc003",
"size": 109209
}
}
},
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": {
"name": "org.lwjgl:lwjgl-vma:3.4.1:natives-freebsd",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-freebsd.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-freebsd.jar",
"sha1": "85851480bef99188bf9a84b36cbe862cb4a52016",
"size": 62862
}
}
},
"org.lwjgl:lwjgl-vulkan:3.4.1": {
"name": "org.lwjgl:lwjgl-vulkan:3.4.1",
"downloads": {
"artifact": {
"path": "org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
"sha1": "d0601fdc38890755e5638048c14cc0f7b990964b",
"size": 8356700
}
}
},
"net.java.jinput:jinput-platform:2.0.5:natives": null,
"com.mojang:text2speech:1.10.3:natives": null,
"com.mojang:text2speech:1.11.3:natives": null,
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* 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
* 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 javafx.collections.FXCollections;
+3 -2
View File
@@ -16,16 +16,17 @@ dependencies {
api(libs.kala.compress.tar)
api(libs.simple.png.javafx)
api(libs.gson)
api(libs.toml)
api(libs.tomlj)
api(libs.xz)
api(libs.lz4)
api(libs.fx.gson)
api(libs.constant.pool.scanner)
api(libs.opennbt)
api(libs.nanohttpd)
api(libs.jsoup)
api(libs.chardet)
api(libs.jna)
api(libs.pci.ids)
api(libs.hello.nbt)
compileOnlyApi(libs.jetbrains.annotations)
Loaded 100 of 183 files, more files were not shown because too many files have changed in this diff. Show more