Compare commits

...
Author SHA1 Message Date
Glavo a4f5e013cb 修复使用 Java 8 构建失败的问题 (#3326) 2024-10-14 01:24:14 +08:00
Glavo d4579852bb 优化 AboutPage (#3325)
* 优化关于页面

* update
2024-10-14 01:08:06 +08:00
Zkitefly edd95ad626 模组管理搜索操作更改为文本更新后触发 (#3221)
* 更改为文本更新后触发

* 添加防抖
2024-10-13 21:57:32 +08:00
ZkiteflyandGlavo dec307b531 安装新版本的版本名输入框显示已选择加载器名称 (#3234)
* Update InstallersPage.java

* 优化代码

* Update InstallersPage.java

* Update InstallersPage.java

* fix: checkstyle

* 给 LibraryAnalyzer.LibraryType.fromPatchId 稍微优化一下,把她里面的实现改为一个预先生成的 map,

相信你知道怎么把这个函数改成一个 lookupMap.get(patchId) 的形式

* Update InstallersPage.java

* Update InstallersPage.java

* Fix

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-13 21:46:48 +08:00
Glavo 00b7085370 更新 Linux LoongArch64 支持 (#3264) 2024-10-13 20:54:15 +08:00
Glavo 17c1998119 修复对话框标题和按钮未使用自定义字体的问题 (#3324) 2024-10-13 20:39:55 +08:00
Glavo 1c05cd981f 使用 ./gradlew run 启动 HMCL 时解析 HMCL_JAVA_OPTS 环境变量 (#3323) 2024-10-13 20:20:05 +08:00
Glavo e9c3795ffc 预生成 openjfx-dependencies.json (#3322) 2024-10-13 19:52:00 +08:00
Zkiteflyand3gf8jv4dv c3917082ee 添加打开启动器日志文件夹按钮 (#3237)
* 添加打开启动器日志文件夹

* Update HMCL/src/main/resources/assets/lang/I18N.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/resources/assets/lang/I18N_zh.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

* Update HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2024-10-13 19:37:35 +08:00
3gf8jv4dv 6265580d75 Adjust the order of sidebars in instance settings (#3278) 2024-10-13 19:35:24 +08:00
YLDK 65fc3d0c4f Support #3087: 隐藏整合包文件位置显示 (#3088)
* Support #3087: 隐藏整合包文件位置显示

* Support #3087: 移除整合包文件位置显示代码

* Support #3087: 移除整合包文件位置显示代码
2024-10-13 19:29:02 +08:00
Glavo 1c864ba072 使用 Jsoup 解析并渲染 HTML 页面 (#3321) 2024-10-13 19:19:52 +08:00
Glavo ef9159666c 使用 TypeToken 替代 Type (#3320) 2024-10-11 20:50:58 +08:00
Glavo 541f2e2619 清理不再使用的 i18n 条目 (#3318) 2024-10-09 02:00:53 +08:00
Glavo c7f248a092 优化更新窗口 (#3317)
* 优化更新窗口

* fix NPE
2024-10-09 01:40:26 +08:00
Zkitefly 7937f8ddf5 发现更新弹窗添加取消按钮 (#3282) 2024-10-09 01:03:56 +08:00
Glavo 74926704bd Close #3209: 搜索 IntelliJ IDEA 安装的 Java (#3316) 2024-10-09 00:35:07 +08:00
Glavo 9196bda537 优化 FXUtils.testLinuxCommand (#3315) 2024-10-09 00:19:06 +08:00
Glavo 4c1e607b8e 在 Linux/FreeBSD 平台读取 os-release (#3314)
* 简化 OperatingSystem.getPhysicalMemoryStatus

* Read os-release

* update

* update
2024-10-07 00:57:35 +08:00
Zkitefly 26224ae366 删除账户和删除认证服务器时弹出提示确认删除 (#3280) 2024-10-06 23:38:48 +08:00
Glavo d3e9511aca 删除 FractureiserDetector (#3313) 2024-10-06 23:31:19 +08:00
ShulkerSakura b624cebc08 修改下载页面按钮名称 (#3262) 2024-10-06 23:21:12 +08:00
Glavo 7e4d437a1d 重构 Java 管理 (#2988)
* update

* update

* Update task name

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* Update logo
2024-10-05 23:50:14 +08:00
Zkitefly 37d6857b82 根据版本类型调整下载项图标 (#3306) 2024-10-03 00:31:06 +08:00
Zkitefly f8d7c92d51 Close #3208: 隐藏版本更新界面的Fabric/Quilt API选项 (#3271) 2024-09-24 03:32:02 +08:00
YSXX1013andGlavo ea9999a1f9 支持使用 CMake 与 MinGW 构建 HMCLauncher (#3011)
* 尝试性的为HMCLauncher添加cmake及gcc支持

* 完善cmake及gcc支持

* 尝试支持Github Actions

* 尝试支持Github Actions-2

* 尝试支持Github Actions-3

* 尝试支持Github Actions-4

* 尝试支持Github Actions-5

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-24 02:51:44 +08:00
3gf8jv4dv d969fab284 Fix checkstyle (#3292) 2024-09-19 17:42:58 +08:00
vochant 97fe52c2a7 feat: Supported JRE from SDKMAN! on Linux (#3062) 2024-09-19 03:09:05 +08:00
Glavo 8a816f7f35 优化 LogWindow (#3274)
* update

* update

* update

* update

* update
2024-09-02 20:54:33 +08:00
6409841cca Fix #3138: HMCL 错误地将 Authlib 日志识别为 ERROR (#3142)
* Close#3138

* 恢复gradle-wrapper.properties

* 恢复gradle-wrapper.properties

* 添加空格

* 修复控制台输出的日志

* Merge if statement

* update

* update

---------

Co-authored-by: CiLuQwQ <a@>
Co-authored-by: burningtnt <pangyl08@163.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-02 01:51:53 +08:00
ZkiteflyandGlavo 6348166dac 支持在 Linux/BSD 平台使用 showFileInExplorer (#3233)
* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update FXUtils.java

* Update ModListPageSkin.java

* 使用 dbus-send

* Update FXUtils.java

* Update FXUtils.java

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-09-01 00:27:06 +08:00
ZkiteflyandGlavo 48417b9b7b 显示 Forge 发版时间 (#3256)
* 显示时间

* Fix

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-08-31 00:44:43 +08:00
Burning_TNT 738000ff39 移除无用类与方法 (#3132)
* Remove useless classes.

* Remove useless methods
2024-08-25 03:03:48 +08:00
ZkiteflyandGlavo 9b5445b686 修复模组详情页图标问题 (#3227)
* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* update

* 调整判断逻辑

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-08-25 02:55:55 +08:00
Haowei Wen abf58b2c20 Fix missing i18n key curse.category.7418 (#3260) 2024-08-24 23:44:13 +08:00
Zkitefly fd97f6c321 支持 Forge 官方源 (#3259)
* Revert "Revert "支持 Forge 官方源 (#3251)" (#3258)"

This reverts commit abd37a093a.

* Update ForgeVersionList.java
2024-08-24 23:43:50 +08:00
Glavo abd37a093a Revert "支持 Forge 官方源 (#3251)" (#3258)
This reverts commit 0f0cf55509.
2024-08-24 14:50:41 +08:00
Zkitefly 0f0cf55509 支持 Forge 官方源 (#3251)
* 支持 Forge 官方源

* 更新 ForgeVersionList.java

* 更新 ForgeVersionList.java
2024-08-24 03:32:45 +08:00
ZkiteflyandGlavo 95a1496389 Fix #3223: 正确解析包含 hash 的版本号 (#3224)
* Fix #3223

* 使用正则表达式删除内容

* 添加日志

添加日志

* Update GameVersion.java

* Update GameVersion.java

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-08-20 01:08:49 +08:00
Glavo 47cc9ebe2b Fix #3199: 修复运行在 Java 8 上时游戏安装界面卡片布局的问题 (#3201) 2024-07-29 15:46:11 +08:00
Glavo 3ebbdb60ef Fix #3191: 修复无法同时安装 LiteLoader 与 Forge 的问题 (#3198) 2024-07-28 02:47:52 +08:00
Burning_TNTandGlavo e8306ea59a 修复 #3099 自动安装选项卡重新选择版本(Minecraft版本和模组加载器版本)的按钮消失的问题 (#3117)
* Fix #3099

* Fix: checkstyle

* Fix.

* Fix #3144

* Update

* Fix bugs.

* Change I18N

* Fix #3164

* Delete State.SEALED

* fix typo

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-07-28 02:41:08 +08:00
Glavo 6f7c922ac0 更新多平台支持 (#3197)
* update

* Update PLATFORM.md

* Update NativePatcher

* Update NativePatcher

* Update NativePatcher
2024-07-27 09:16:33 +08:00
Burning_TNT 74abecaae7 实现 #3095 微软登录界面对 XBox 400 错误给予提示 (#3194) 2024-07-22 23:06:20 +08:00
Burning_TNTandGlavo ad5b74d8c7 修复 2830 本地模组查看信息时标签错误 (#2838)
* Clean up duplicated codes in DownloadPage.

* Remove useless codes.

* Render the raw category when no i18n is provided.

* Fix #2830.

* Fix checkstyle.

* format file

* Remove the redundant lock.

* Update ModListPageSkin

* Update ModListPageSkin

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-07-21 00:58:33 +08:00
Burning_TNT 5f84e8cfeb 修复 1.8 及 1.9 安装 OptiFine 的问题 (#3130)
* Fix: 1.8 & 1.9 OptiFine Install

* Fix: checkstyle
2024-07-20 20:09:36 +08:00
Burning_TNT 519ed0ad5f 清理 RemoteModRepository (#2798)
* Clean up duplicated codes in DownloadPage.

* Remove useless codes.

* Render the raw category when no i18n is provided.

* Fix checkstyle.
2024-07-20 05:29:30 +08:00
Burning_TNT 61096142d5 Fix #3065: 修复 CurseForge 搜索 API 对翻页总量计算不正确的 Bug (#3066)
* Fix #3065

* Fix

* Fix.
2024-07-20 05:03:08 +08:00
Burning_TNT 7c7c36f8aa 修复 #3112 模组详情中若没有这个游戏版本,仍然会出现该下拉栏的问题 (#3114) 2024-07-20 05:00:19 +08:00
Glavo dbb614dce1 Fix #3051: 支持解析更多版本号 (#3058)
* Fix #3051: 支持解析更多版本号

* Update GameVersionNumberTest

* Update versions.txt

* Update GameVersionNumber

* Update GameVersionNumberTest

* Update GameVersionNumberTest

* Update GameVersionNumberTest
2024-07-20 04:58:51 +08:00
Burning_TNT 5a4825d419 修复 #3080 微软账户无法正确读取 Token 过期时间的问题 (#3126) 2024-07-20 04:05:58 +08:00
Glavo d87cf91cdf 删除 I18N 文件中重复的条目 (#3190)
* 删除 I18N 文件中重复的项目

* update

* update
2024-07-20 02:18:41 +08:00
Burning_TNT 0782f40fe0 修复 #3127 1.21 NeoForge 下载问题 (#3128) 2024-07-20 01:57:04 +08:00
Burning_TNTandGlavo b7a5b484b5 [重要] 实现 #3095 微软登录界面对 XBox 400 错误给予提示 (#3121)
* Fix #3095

* Fix

* Update I18N_zh_CN.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-07-20 01:56:13 +08:00
Burning_TNT 1e472ea463 Fix: NPE in LauncherHelper. (#3160) 2024-07-20 01:44:26 +08:00
Burning_TNT 816a8539ea 修复 bug-report.yml 中的无效链接 (#3159) 2024-07-18 00:40:34 +08:00
Glavo c81e510bfa 支持使用环境变量指定字体 (#3181) 2024-07-15 12:33:53 +08:00
Haowei Wen 85b68ad135 Fix findMinecraftRuntimeDirs() is broken on Linux/FreeBSD 2024-06-30 13:28:49 +08:00
Burning_TNTandGlavo 7d5d74d0c6 修复 #3135 Forge 1.7.10-pre4 安装问题 (#3136)
* Fix #3135

* Update ForgeBMCLVersionList.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-06-21 23:24:16 +08:00
Burning_TNT e0ecd5e695 修复 #3131 1.7.10-pre4 版本 Forge 安装问题 (#3133)
* Fix #3131

* Fix.
2024-06-17 20:16:55 +08:00
z0z0r4 5e0687c138 Update CurseForgeRemoteModRepository.java (#3022)
432 is gameid for minecraft

https://docs.curseforge.com/#get-fingerprints-matches-by-game-id

https://github.com/Hex-Dragon/PCL2/blob/d682b598032bc4c7e3b1367cae4d51d037007ae9/Plain%20Craft%20Launcher%202/Modules/Minecraft/ModMod.vb#L1029-L1030
2024-06-15 22:31:49 +08:00
Glavo b827167a3d 将 1.21 添加至 versions.txt (#3118) 2024-06-14 00:45:48 +08:00
Burning_TNT 20711c9d10 Fix #3107 修复 HMCL 3.5.8.248 无法安装启动带 forge 的 1.11.2 版本的问题 (#3113) 2024-06-14 00:20:25 +08:00
Burning_TNT 15c1042991 Fix #3086 修复模组下载界面版本推荐功能 (#3089)
* Fix #3086

* Update
2024-05-31 23:12:33 +08:00
Burning_TNT 1275e38995 Fix #3006: 全局游戏设置中版本隔离开启后的显示的路径的版本号为”null“ (#3028) 2024-05-31 23:10:02 +08:00
Glavo 312a2e0cde 更新 Linux ARM64 支持 (#3097) 2024-05-31 23:06:38 +08:00
Burning_TNT 35ce95857a Fix #3026: resources 目录应该跟随游戏目录 (#3027) 2024-05-31 22:48:19 +08:00
Burning_TNT 6b78f56298 修复自动安装在 1.20.4 的严重问题 (#3023)
* Fix: OptiFine and Forge has compatible problem on 1.20.4 . Fix: Forge changes it's main class on 1.20.4 .

* Fix: HMCL cannot display the analyed versions installed by external launchers.

* Refuse to configure installable widgets when it's from external process.

* Add javadoc about LibraryStatus

* Adapt to the new change of Forge: https://github.com/MinecraftForge/MinecraftForge/commit/b8270cd9ff71678bd71f93938b35e5904b5f507a

* Re-arrange entries.

* Fix.
2024-05-31 22:02:56 +08:00
Glavo b43c8ceae5 移除 KOOK 入口 (#3092)
* 移除 KOOK 入口

* update
2024-05-31 21:41:41 +08:00
Burning_TNT 704d4e9cc0 Fix #3079 修复 NeoForge 整合包版本识别不正确的问题 (#3082)
* Fix #3079

* Fix.

* Fix: checkstyle.
2024-05-28 08:28:35 +08:00
CalbootandGlavo 96471e3a92 Fix #3071: 在 HMCL 被终结时停止日志线程 (#3081)
* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-05-28 08:17:55 +08:00
Burning_TNT af3719403d Support #3000: Support neoforge.mods.toml (#3031)
* Support #3000

* Fix #2371

* Rename references.

* Code cleanup.
2024-05-15 22:24:48 +08:00
Liyan Zhao e0764d6f0c support java from homebrew (#3035)
* fix: support java from homebrew

* fix: support java from homebrew

* fix: add ~/Library/Java/JavaVirtualMachine
2024-05-14 00:15:07 +08:00
zkitefly 908f47f75e Update Copyright (#3010) 2024-05-13 23:45:46 +08:00
Glavo a3af719087 支持自动下载 Java 21 (#3045) 2024-05-13 23:41:23 +08:00
zkitefly 7f13d6646b Update ExitWaiter.java (#3042) 2024-05-12 20:36:21 +08:00
Kieran Moy e8b4c38451 Add cinnamon desktop support (#3017) 2024-05-07 23:40:18 +08:00
Glavo 38ba6802a0 Bump Hub Mirror Action to 1.4 (#3034) 2024-05-07 23:36:28 +08:00
Glavo 381eead7b6 将 1.20.5 和 1.20.6 添加至 versions.txt (#3008)
* 更新 versions.txt

* update
2024-05-07 23:27:22 +08:00
3gf8jv4dv df953e5ea7 Fix NeoForge 2x icon not working (#2994) 2024-04-17 22:00:28 +08:00
Glavo 2fd2954a06 更新指向 XZ 主页的链接 (#2990) 2024-04-15 05:37:04 +08:00
Glavo a8bb5c1570 修复导入部分整合包失败的问题 (#2989)
* 修复导入部分整合包失败的问题

* update

* update
2024-04-15 04:02:24 +08:00
zkitefly a33377d1f2 add macos_failed_to_find_service_port_for_display (#2797)
* add macos_failed_to_find_service_port_for_display

日志来自 HMCL KOOK

* updata i18n

* Update I18N_zh.properties
2024-04-15 02:38:59 +08:00
zkiteflyandGlavo e515542add 还原 MCBBS 的鸣谢 (#2958)
* Update AboutPage.java

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-04-15 02:35:20 +08:00
zkitefly 48b6bd4ea1 add NEOFORGE_FOREST_OPTIFINE (#2976)
* add NEOFORGE_FOREST_OPTIFINE

* fix
2024-04-15 02:10:02 +08:00
Liyan Zhao 9cf23f69a6 Fix Microsoft authentication (#2986)
* fix: microsoft login #2979

* fix: typo
2024-04-15 02:07:47 +08:00
Glavo 4104ca082d 修复日志文件夹不存在时无法将日志写入文件的问题 (#2984) 2024-04-11 19:43:18 +08:00
Glavo 32f8ffad46 修复整合包下载失败提示窗标题未正确格式化的问题 (#2980)
* 修复 i18n 问题

* update

* update
2024-04-08 21:49:54 +08:00
Glavo a5e04a7b81 将 24w14potato 添加至 versions.txt (#2962)
* 将 24w14potato 添加至 versions.txt

* Update test
2024-04-02 13:55:52 +08:00
Glavo 57018bef47 重新实现日志记录 (#2951)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update
2024-03-28 12:16:52 +08:00
Glavo 0b65431044 清理残余 MCBBS 相关内容 (#2953)
* 清理残余 MCBBS 相关内容

* update
2024-03-25 21:44:27 +08:00
Glavo 991729b684 支持从当前路径加载字体 (#2950) 2024-03-24 19:59:38 +08:00
MythandGlavo 1605eb1775 fix: add locale to avoid wrong yggdrasil server name (#2521)
* fix: add locale to avoid wrong yggdrasil server name

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-03-23 14:19:16 +08:00
Glavo 94ccee0b76 优化 I18n (#2948)
* update

* update

* update

* update

* update
2024-03-23 14:11:52 +08:00
Glavo 8b94fe5b0b 更新模组和整合包信息 (#2947)
* update

* update
2024-03-23 11:31:09 +08:00
Glavo abec1abcff 更新默认皮肤 (#2943)
* 更新默认皮肤

* update

* update

* update

* update

* update

* update
2024-03-22 16:09:25 +08:00
Glavo 127938fa38 支持更多格式的版本图标 (#2942) 2024-03-21 20:01:21 +08:00
302 changed files with 26282 additions and 11469 deletions

No files matched your search

+5 -5
View File
@@ -8,16 +8,16 @@ body:
attributes:
value: |
提交前请确认:
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[KOOK 频道](https://kook.top/Kx7n3t)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
* 你的启动器版本是**最新的快照版本**,可以点击 [此处](https://github.com/burningtnt/HMCL-Snapshot-Update/raw/master/datas/HMCL-dev.jar) 下载最新快照版本。
* 该问题确实是 **HMCL 的错误**,而**不是 Minecraft 非正常退出**,如果你的 Minecraft 非正常退出,请前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
* 你的启动器版本是**最新的快照版本**,可以点击 [此处](https://zkitefly.github.io/HMCL-Snapshot-Update/) 下载最新快照版本。
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[KOOK 频道](https://kook.top/Kx7n3t)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
如果你的问题并不属于上述两类,你可以选取另一种 Issue 类型,或者直接前往 [QQ 群](https://docs.hmcl.net/groups.html)/[Discord 频道](https://discord.gg/jVvC7HfM6U) 中获取帮助。
Before submitting, please confirm:
* The issue is indeed **an error of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [KOOK channel](https://kook.top/Kx7n3t) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
* The issue is indeed **an error of HMCL**, not **Minecraft abnormal exit**. If your Minecraft exits abnormally, please go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
* Your launcher version is **the latest snapshot version**. You can click [here](https://github.com/burningtnt/HMCL-Snapshot-Update/raw/master/datas/HMCL-dev.jar) to download the latest snapshot version.
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [KOOK channel](https://kook.top/Kx7n3t) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
If your issue does not fall into the above two categories, you can choose another type of issue or directly go to the [QQ group](https://docs.hmcl.net/groups.html) or [Discord channel](https://discord.gg/jVvC7HfM6U) for help.
- type: input
id: platform
attributes:
+3 -6
View File
@@ -1,14 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: KOOK 频道 | KOOK Channel
url: https://kook.top/Kx7n3t
about: Hello Minecraft! Launcher 的官方 KOOK 频道。| The official KOOK channel of Hello Minecraft! Laucher.
- name: Discord 频道 | Discord Channel
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 频道。| The official Discord channel of Hello Minecraft! Launcher.
- name: QQ 群 | QQ Groups
url: https://docs.hmcl.net/groups.html
about: Hello Minecraft! Launcher 的官方 QQ 交流群。| The official QQ groups of Hello Minecraft! Laucher.
- name: Discord 频道 | Discord Channel
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 频道。| The official Discord channel of Hello Minecraft! Launcher.
- name: 其他反馈 | Others
url: https://github.com/HMCL-dev/HMCL/discussions/new/choose
about: 通过 Discussions 反馈其他问题。| Report other problems in Discussions.
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Mirror GitHub to Gitee
uses: Yikun/hub-mirror-action@v1.3
uses: Yikun/hub-mirror-action@v1.4
with:
src: github/HMCL-dev
dst: gitee/huanghongxun
+58 -6
View File
@@ -118,7 +118,7 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
manifest {
attributes(
"Created-By" to "Copyright(c) 2013-2023 huangyuhui.",
"Created-By" to "Copyright(c) 2013-2024 huangyuhui.",
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Implementation-Version" to project.version,
@@ -189,11 +189,6 @@ tasks.processResources {
from(sourceSets["java11"].output)
}
dependsOn(tasks["java11Classes"])
into("assets") {
from(project.layout.buildDirectory.file("openjfx-dependencies.json"))
}
dependsOn(rootProject.tasks["generateOpenJFXDependencies"])
}
val makeExecutables = tasks.create("makeExecutables") {
@@ -208,6 +203,56 @@ tasks.build {
dependsOn(makeExecutables)
}
fun parseToolOptions(options: String?): List<String> {
if (options == null)
return listOf()
val builder = StringBuilder()
val result = mutableListOf<String>()
var offset = 0
loop@ while (offset < options.length) {
val ch = options[offset]
if (Character.isWhitespace(ch)) {
if (builder.isNotEmpty()) {
result += builder.toString()
builder.clear()
}
while (offset < options.length && Character.isWhitespace(options[offset])) {
offset++
}
continue@loop
}
if (ch == '\'' || ch == '"') {
offset++
while (offset < options.length) {
val ch2 = options[offset++]
if (ch2 != ch) {
builder.append(ch2)
} else {
continue@loop
}
}
throw GradleException("Unmatched quote in $options")
}
builder.append(ch)
offset++
}
if (builder.isNotEmpty()) {
result += builder.toString()
}
return result
}
tasks.create<JavaExec>("run") {
dependsOn(tasks.jar)
@@ -215,4 +260,11 @@ tasks.create<JavaExec>("run") {
classpath = files(jarPath)
workingDir = rootProject.rootDir
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
jvmArgs(vmOptions)
doFirst {
logger.quiet("HMCL_JAVA_OPTS: $vmOptions")
}
}
+3 -33
View File
@@ -27,7 +27,7 @@ S = ';'
MOD_SEPARATOR = ','
CURSEFORGE_PATTERN1 = re.compile(
r'^/minecraft/(mc-mods|modpacks|customization|mc-addons|customization/configuration)/+(?P<modid>[\w-]+)(/(.*?))?$')
r'^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons)/+(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN2 = re.compile(
r'^/projects/(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN3 = re.compile(
@@ -58,30 +58,6 @@ def parseMcmod(url):
return match.group('modid')
return ''
MCBBS_HTML_PATTERN = re.compile(r'/+thread-(?P<modid>\d+)-(\d+)-(\d+).html')
MCBBS_PHP_PATTERN = re.compile(r'')
def parseMcbbs(url):
res = urlparse(url)
if res.scheme not in ['http', 'https']:
return ''
if res.netloc != 'www.mcbbs.net':
return ''
match = MCBBS_HTML_PATTERN.match(res.path)
if match:
return match.group('modid')
query = parse_qs(res.query)
if res.path == '/forum.php':
if 'mod' in query and 'tid' in query and query['mod'] == ['viewthread']:
return query['tid']
return ''
skip = [
'Minecraft',
'The Building Game'
@@ -111,12 +87,11 @@ if __name__ == '__main__':
exit(1)
black_lists = [
'Master Chef'
'Wood Converter'
]
curseforge_id = ''
mcmod_id = ''
mcbbs_id = ''
links = mod['links']['list']
if 'curseforge' in links and links['curseforge'] and sub_name not in black_lists and chinese_name not in black_lists:
for link in links['curseforge']:
@@ -131,11 +106,6 @@ if __name__ == '__main__':
if mcmod_id == '':
print('Error mcmod ' + chinese_name)
exit(1)
if 'mcbbs' in links and links['mcbbs']:
mcbbs_id = parseMcbbs(links['mcbbs'][0]['url'])
if mcbbs_id == '':
print('Error mcbbs ' + chinese_name)
exit(1)
mod_id = []
if 'modid' in mod and 'list' in mod['modid']:
@@ -148,6 +118,6 @@ if __name__ == '__main__':
mod_ids = MOD_SEPARATOR.join([str(id) for id in mod_id])
outfile.write(
f'{curseforge_id}{S}{mcmod_id}{S}{mcbbs_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
f'{curseforge_id}{S}{mcmod_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
print('Success!')
@@ -12,14 +12,10 @@ import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jetbrains.annotations.Nullable;
public class SkinCanvas extends Group {
public static final Image ALEX = FXUtils.newBuiltinImage("/assets/img/skin/alex.png");
public static final Image STEVE = FXUtils.newBuiltinImage("/assets/img/skin/steve.png");
public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true);
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
@@ -152,10 +148,6 @@ public class SkinCanvas extends Group {
capeGroup.setVisible(hasCape);
}
public SkinCanvas(double preW, double preH) {
this(STEVE, preW, preH, true);
}
public SkinCanvas(Image skin, double preW, double preH, boolean msaa) {
this.skin = skin;
this.preW = preW;
@@ -25,9 +25,6 @@ import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Stage;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
@@ -53,13 +50,10 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Launcher extends Application {
@@ -71,14 +65,12 @@ public final class Launcher extends Application {
CookieHandler.setDefault(COOKIE_MANAGER);
register();
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
try {
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
LOG.info("Prism pipeline: " + (pipeline == null ? "null" : pipeline.getClass().getName()));
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to get prism pipeline", e);
LOG.warning("Failed to get prism pipeline", e);
}
try {
@@ -87,7 +79,7 @@ public final class Launcher extends Application {
} catch (SambaException ignored) {
Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to load config", e);
LOG.error("Failed to load config", e);
checkConfigInTempDir();
checkConfigOwner();
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
@@ -128,45 +120,6 @@ public final class Launcher extends Application {
}
}
private static void register() {
Skin.registerDefaultSkinLoader((type) -> {
switch (type) {
case ALEX:
return Skin.class.getResourceAsStream("/assets/img/skin/alex.png");
case ARI:
return Skin.class.getResourceAsStream("/assets/img/skin/ari.png");
case EFE:
return Skin.class.getResourceAsStream("/assets/img/skin/efe.png");
case KAI:
return Skin.class.getResourceAsStream("/assets/img/skin/kai.png");
case MAKENA:
return Skin.class.getResourceAsStream("/assets/img/skin/makena.png");
case NOOR:
return Skin.class.getResourceAsStream("/assets/img/skin/noor.png");
case STEVE:
return Skin.class.getResourceAsStream("/assets/img/skin/steve.png");
case SUNNY:
return Skin.class.getResourceAsStream("/assets/img/skin/sunny.png");
case ZURI:
return Skin.class.getResourceAsStream("/assets/img/skin/zuri.png");
default:
return null;
}
});
RemoteMod.registerEmptyRemoteMod(new RemoteMod("", "", i18n("mods.broken_dependency.title"), i18n("mods.broken_dependency.desc"), new ArrayList<>(), "", "/assets/img/icon@8x.png", new RemoteMod.IMod() {
@Override
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
throw new IOException();
}
@Override
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
throw new IOException();
}
}));
}
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
}
@@ -211,7 +164,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) {
System.exit(0);
Main.exit(0);
}
}
@@ -224,7 +177,7 @@ public final class Launcher extends Application {
try {
owner = Files.getOwner(ConfigHolder.configLocation()).getName();
} catch (IOException ioe) {
LOG.log(Level.WARNING, "Failed to get file owner", ioe);
LOG.warning("Failed to get file owner", ioe);
return;
}
@@ -249,17 +202,18 @@ public final class Launcher extends Application {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
System.exit(1);
Main.exit(1);
}
@Override
public void stop() throws Exception {
super.stop();
Controllers.onApplicationStop();
LOG.shutdown();
}
public static void main(String[] args) {
if (UpdateHandler.processArguments(args)) {
LOG.shutdown();
return;
}
@@ -268,7 +222,9 @@ public final class Launcher extends Application {
try {
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION);
LOG.info("Operating System: " + (OperatingSystem.OS_RELEASE_PRETTY_NAME == null
? OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION
: OperatingSystem.OS_RELEASE_PRETTY_NAME + " (" + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + ')'));
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
@@ -277,6 +233,7 @@ public final class Launcher extends Application {
LOG.info("Current Directory: " + System.getProperty("user.dir"));
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
LOG.info("HMCL Jar Path: " + Lang.requireNonNullElse(JarUtils.thisJarPath(), "Not Found"));
LOG.info("HMCL Log File: " + Lang.requireNonNullElse(LOG.getLogFile(), "In Memory"));
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
LOG.info("Physical memory: " + OperatingSystem.TOTAL_MEMORY + " MB");
LOG.info("Metaspace: " + ManagementFactory.getMemoryPoolMXBeans().stream()
+17 -23
View File
@@ -20,11 +20,9 @@ package org.jackhuang.hmcl;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.FractureiserDetector;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.HttpsURLConnection;
@@ -43,10 +41,9 @@ import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Main {
@@ -59,24 +56,28 @@ public final class Main {
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
LOG.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
checkDirectoryPath();
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9)
if (JavaRuntime.CURRENT_VERSION < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
initIcon();
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
checkJavaFX();
verifyJavaFX();
detectFractureiser();
Launcher.main(args);
}
public static void exit(int exitCode) {
LOG.shutdown();
System.exit(exitCode);
}
private static void initIcon() {
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon@8x.png"));
AwtUtils.setAppleIcon(image);
@@ -91,25 +92,18 @@ public final class Main {
}
}
private static void detectFractureiser() {
if (FractureiserDetector.detect()) {
LOG.log(Level.SEVERE, "Detected that this computer is infected by fractureiser");
showErrorAndExit(i18n("fatal.fractureiser"));
}
}
private static void checkJavaFX() {
try {
SelfDependencyPatcher.patch();
} catch (SelfDependencyPatcher.PatchException e) {
LOG.log(Level.SEVERE, "unable to patch JVM", e);
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.log(Level.SEVERE, "unable to patch JVM", e);
LOG.error("unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.log(Level.SEVERE, "User cancels downloading JavaFX", e);
System.exit(0);
LOG.error("User cancels downloading JavaFX", e);
exit(0);
}
}
@@ -136,13 +130,13 @@ public final class Main {
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.ERROR, message).showAndWait();
System.exit(1);
exit(1);
}
} catch (Throwable ignored) {
}
SwingUtils.showErrorDialog(message);
System.exit(1);
exit(1);
}
/**
@@ -192,7 +186,7 @@ public final class Main {
LOG.info("Added Lets Encrypt root certificates as additional trust");
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
KeyManagementException e) {
LOG.log(Level.SEVERE, "Failed to load lets encrypt certificate. Expect problems", e);
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
}
}
}
@@ -1,86 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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.util.io.HttpRequest;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Locale;
import static org.jackhuang.hmcl.util.Pair.pair;
public class Countly {
private String deviceId;
private String endpoint;
private String serverURL;
public void sendMetric(String metrics) throws IOException {
HttpRequest.GET(serverURL + endpoint,
pair("begin_session", "1"),
pair("session_id", "1"),
pair("metrics", metrics),
pair("device_id", deviceId),
pair("timestamp", Long.toString(System.currentTimeMillis())),
pair("tz", Integer.toString(getTimezoneOffset())),
pair("hour", Integer.toString(currentHour())),
pair("dow", Integer.toString(currentDayOfWeek())),
pair("app_key", APP_KEY),
pair("sdk_name", "java-native"),
pair("sdk_version", "20.11.1"))
.getString();
}
private static int getTimezoneOffset() {
return ZonedDateTime.now().getOffset().getTotalSeconds() / 60;
}
private static String getLocale() {
final Locale locale = Locale.getDefault();
return locale.getLanguage() + "_" + locale.getCountry();
}
private static int currentHour() {
return Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
}
private int currentDayOfWeek() {
int day = Calendar.getInstance().get(Calendar.DAY_OF_WEEK);
switch (day) {
case Calendar.SUNDAY:
return 0;
case Calendar.MONDAY:
return 1;
case Calendar.TUESDAY:
return 2;
case Calendar.WEDNESDAY:
return 3;
case Calendar.THURSDAY:
return 4;
case Calendar.FRIDAY:
return 5;
case Calendar.SATURDAY:
return 6;
}
return 0;
}
private static final String APP_KEY = "";
}
@@ -1,18 +1,12 @@
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
public class CrashReport {
@@ -20,24 +14,16 @@ public class CrashReport {
private final Throwable throwable;
private final String stackTrace;
private boolean nonFatal;
public CrashReport(Thread thread, Throwable throwable) {
this.thread = thread;
this.throwable = throwable;
stackTrace = StringUtils.getStackTrace(throwable);
nonFatal = false;
}
public Throwable getThrowable() {
return this.throwable;
}
public CrashReport setNonFatal() {
nonFatal = true;
return this;
}
public boolean shouldBeReport() {
if (!stackTrace.contains("org.jackhuang"))
return false;
@@ -48,23 +34,6 @@ public class CrashReport {
return true;
}
public Map<String, Object> getMetrics(long runningTime) {
return mapOf(
pair("_run", runningTime),
pair("_app_version", Metadata.VERSION),
pair("_os", OperatingSystem.SYSTEM_NAME),
pair("_os_version", OperatingSystem.SYSTEM_VERSION),
pair("_disk_current", getDiskAvailable()),
pair("_disk_total", getDiskTotal()),
pair("_ram_current", getMemoryAvailable()),
pair("_ram_total", Runtime.getRuntime().maxMemory() / BYTES_IN_MB),
pair("_error", stackTrace),
pair("_logs", Logging.getLogs()),
pair("_name", throwable.getLocalizedMessage()),
pair("_nonfatal", nonFatal)
);
}
public String getDisplayText() {
return "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
@@ -82,29 +51,4 @@ public class CrashReport {
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
}
private static final Long BYTES_IN_MB = 1024L * 1024;
private static long getMemoryAvailable() {
Long total = Runtime.getRuntime().totalMemory();
Long availMem = Runtime.getRuntime().freeMemory();
return (total - availMem) / BYTES_IN_MB;
}
private static long getDiskAvailable() {
long total = 0, free = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
free += f.getUsableSpace();
}
return (total - free) / BYTES_IN_MB;
}
private static long getDiskTotal() {
long total = 0;
for (File f : File.listRoots()) {
total += f.getTotalSpace();
}
return total / BYTES_IN_MB;
}
}
@@ -21,7 +21,6 @@ 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.Logging;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
@@ -29,9 +28,9 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author huangyuhui
@@ -72,7 +71,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
}
if (I18n.getCurrentLocale().getLocale() != Locale.CHINA) {
if (!I18n.isUseChinese()) {
return;
}
@@ -97,7 +96,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
try {
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to generate options.txt", e);
LOG.warning("Unable to generate options.txt", e);
}
}
}
@@ -37,7 +37,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
@@ -50,12 +50,11 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public class HMCLGameRepository extends DefaultGameRepository {
@@ -124,7 +123,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (!file.exists() && !versions.isEmpty())
FileUtils.writeText(file, PROFILE);
} catch (IOException ex) {
LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
LOG.warning("Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
}
// https://github.com/HMCL-dev/HMCL/issues/938
@@ -259,8 +258,50 @@ public class HMCLGameRepository extends DefaultGameRepository {
return vs;
}
public File getVersionIconFile(String id) {
return new File(getVersionRoot(id), "icon.png");
public Optional<File> getVersionIconFile(String id) {
File root = getVersionRoot(id);
File iconFile = new File(root, "icon.png");
if (iconFile.exists()) {
return Optional.of(iconFile);
}
iconFile = new File(root, "icon.jpg");
if (iconFile.exists()) {
return Optional.of(iconFile);
}
iconFile = new File(root, "icon.bmp");
if (iconFile.exists()) {
return Optional.of(iconFile);
}
iconFile = new File(root, "icon.gif");
if (iconFile.exists()) {
return Optional.of(iconFile);
}
return Optional.empty();
}
public void setVersionIconFile(String id, File iconFile) throws IOException {
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
if (!ext.equals("png") && !ext.equals("jpg") && !ext.equals("bmp") && !ext.equals("gif")) {
throw new IllegalArgumentException("Unsupported icon file: " + ext);
}
deleteIconFile(id);
FileUtils.copyFile(iconFile, new File(getVersionRoot(id), "icon." + ext));
}
public void deleteIconFile(String id) {
File root = getVersionRoot(id);
new File(root, "icon.png").delete();
new File(root, "icon.jpg").delete();
new File(root, "icon.bmp").delete();
new File(root, "icon.gif").delete();
}
public Image getVersionIconImage(String id) {
@@ -272,12 +313,12 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (iconType == VersionIconType.DEFAULT) {
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists()) {
try (InputStream inputStream = new FileInputStream(iconFile)) {
Optional<File> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try (InputStream inputStream = new FileInputStream(iconFile.get())) {
return new Image(inputStream);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load version icon of " + id, e);
LOG.warning("Failed to load version icon of " + id, e);
}
}
@@ -316,7 +357,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
return true;
} catch (IOException e) {
LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
LOG.error("Unable to save version setting of " + id, e);
return false;
}
}
@@ -342,7 +383,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -355,7 +396,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
OperatingSystem.getPhysicalMemoryStatus().getAvailable(),
vs.isAutoMemory()
) / 1024 / 1024))
.setMinMemory(vs.getMinMemory())
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
@@ -67,8 +66,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
ModpackConfiguration<Modpack> config = null;
try {
if (json.exists()) {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
}.getType());
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), ModpackConfiguration.typeOf(Modpack.class));
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
@@ -28,15 +27,13 @@ import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.*;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.*;
@@ -55,20 +52,17 @@ import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static javafx.application.Platform.runLater;
import static javafx.application.Platform.setImplicitExit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.platform.Platform.*;
public final class LauncherHelper {
@@ -112,7 +106,7 @@ public final class LauncherHelper {
public void launch() {
FXUtils.checkFxUserThread();
Logging.LOG.info("Launching game version: " + selectedVersion);
LOG.info("Launching game version: " + selectedVersion);
Controllers.dialog(launchingStepsPane);
launch0();
@@ -133,12 +127,12 @@ public final class LauncherHelper {
CountDownLatch launchingLatch = new CountDownLatch(1);
List<String> javaAgents = new ArrayList<>(0);
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
AtomicReference<JavaRuntime> javaVersionRef = new AtomicReference<>();
TaskExecutor executor = checkGameState(profile, setting, version.get())
.thenComposeAsync(javaVersion -> {
javaVersionRef.set(Objects.requireNonNull(javaVersion));
version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), javaVersion, setting));
.thenComposeAsync(java -> {
javaVersionRef.set(Objects.requireNonNull(java));
version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), java, setting));
if (setting.isNotCheckGame())
return null;
return Task.allOf(
@@ -156,7 +150,7 @@ public final class LauncherHelper {
Task.composeAsync(() -> {
Renderer renderer = setting.getRenderer();
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getMesaLoader(javaVersion, renderer);
Library lib = NativePatcher.getMesaLoader(java, renderer);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
@@ -180,9 +174,7 @@ public final class LauncherHelper {
})
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> {
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null);
})
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, scriptFile != null);
@@ -215,7 +207,7 @@ public final class LauncherHelper {
it.fireEvent(new DialogCloseEvent());
}));
} else {
Platform.runLater(() -> {
runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
Controllers.dialog(i18n("version.launch_script.success", scriptFile.getAbsolutePath()));
});
@@ -235,14 +227,14 @@ public final class LauncherHelper {
@Override
public void onStop(boolean success, TaskExecutor executor) {
Platform.runLater(() -> {
runLater(() -> {
// Check if the application has stopped
// because onStop will be invoked if tasks fail when the executor service shut down.
if (!Controllers.isStopped()) {
launchingStepsPane.fireEvent(new DialogCloseEvent());
if (!success) {
Exception ex = executor.getException();
if (!(ex instanceof CancellationException)) {
if (ex != null && !(ex instanceof CancellationException)) {
String message;
if (ex instanceof ModpackCompletionException) {
if (ex.getCause() instanceof FileNotFoundException)
@@ -332,171 +324,181 @@ public final class LauncherHelper {
executor.start();
}
private static Task<JavaVersion> checkGameState(Profile profile, VersionSetting setting, Version version) {
private static Task<JavaRuntime> checkGameState(Profile profile, VersionSetting setting, Version version) {
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, profile.getRepository().getGameVersion(version).orElse(null));
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT));
Task<JavaRuntime> getJavaTask = Task.supplyAsync(() -> {
try {
return setting.getJava(gameVersion, version);
} catch (InterruptedException e) {
throw new CancellationException();
}
});
Task<JavaRuntime> task;
if (setting.isNotCheckJVM()) {
return Task.composeAsync(() -> setting.getJavaVersion(gameVersion, version))
.thenApplyAsync(javaVersion -> Optional.ofNullable(javaVersion).orElseGet(JavaVersion::fromCurrentEnvironment))
.withStage("launch.state.java");
}
task = getJavaTask.thenApplyAsync(java -> Lang.requireNonNullElse(java, JavaRuntime.getDefault()));
} else if (setting.getJavaVersionType() == JavaVersionType.AUTO || setting.getJavaVersionType() == JavaVersionType.VERSION) {
task = getJavaTask.thenComposeAsync(Schedulers.javafx(), java -> {
if (java != null) {
return Task.completed(java);
}
return Task.composeAsync(() -> {
return setting.getJavaVersion(gameVersion, version);
}).thenComposeAsync(Schedulers.javafx(), javaVersion -> {
// Reset invalid java version
if (javaVersion == null) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable continueAction = () -> future.complete(JavaVersion.fromCurrentEnvironment());
// Reset invalid java version
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Task<JavaRuntime> result = Task.fromCompletableFuture(future);
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("No accepted java"));
List<GameJavaVersion> supportedVersions = GameJavaVersion.getSupportedVersions(SYSTEM_PLATFORM);
if (setting.isJavaAutoSelected()) {
GameJavaVersion targetJavaVersion = null;
GameJavaVersion targetJavaVersion = null;
if (setting.getJavaVersionType() == JavaVersionType.VERSION) {
try {
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
if (org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
if (range.getMandatory().contains(VersionNumber.asVersion("17.0.1"))) {
targetJavaVersion = GameJavaVersion.JAVA_17;
} else if (range.getMandatory().contains(VersionNumber.asVersion("16.0.1"))) {
targetJavaVersion = GameJavaVersion.JAVA_16;
} else {
String java8Version;
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.getMajorVersion()) {
Controllers.dialog(
i18n("launch.failed.java_version_too_low"),
i18n("message.error"),
MessageType.ERROR,
breakAction
);
return result;
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
java8Version = "1.8.0_51";
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
java8Version = "1.8.0_202";
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
java8Version = "1.8.0_74";
} else {
java8Version = null;
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
} catch (NumberFormatException ignored) {
}
} else
targetJavaVersion = version.getJavaVersion();
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
downloadJava(targetJavaVersion, profile)
.whenCompleteAsync((downloadedJava, exception) -> {
if (exception == null) {
future.complete(downloadedJava);
} else {
LOG.warning("Failed to download java", exception);
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING,
() -> future.complete(JavaRuntime.getDefault()),
breakAction);
}
}, Schedulers.javafx());
} else {
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING,
() -> future.complete(JavaRuntime.getDefault()),
breakAction);
}
return result;
});
} else {
task = getJavaTask.thenComposeAsync(java -> {
Set<JavaVersionConstraint> violatedMandatoryConstraints = EnumSet.noneOf(JavaVersionConstraint.class);
Set<JavaVersionConstraint> violatedSuggestedConstraints = EnumSet.noneOf(JavaVersionConstraint.class);
if (java != null) {
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violatedMandatoryConstraints.add(constraint);
} else {
violatedSuggestedConstraints.add(constraint);
}
}
if (java8Version != null && range.getMandatory().contains(VersionNumber.asVersion(java8Version)))
targetJavaVersion = GameJavaVersion.JAVA_8;
else
targetJavaVersion = null;
}
}
}
if (targetJavaVersion == null) {
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> {
future.completeExceptionally(new CancellationException("No accepted java"));
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Task<JavaRuntime> result = Task.fromCompletableFuture(future);
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
if (java == null || !violatedMandatoryConstraints.isEmpty()) {
JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, version);
if (suggestedJava != null) {
FXUtils.runInFX(() -> {
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
setting.setJavaAutoSelected();
future.complete(suggestedJava);
}, breakAction);
});
return result;
} else if (java == null) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.invalid_java"),
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
} else {
downloadJava(gameVersion.toString(), targetJavaVersion, profile)
.thenAcceptAsync(downloadedJavaVersion -> {
future.complete(downloadedJavaVersion);
})
.exceptionally(throwable -> {
LOG.log(Level.WARNING, "Failed to download java", throwable);
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> {
future.completeExceptionally(new CancellationException("No accepted java"));
});
return null;
});
}
} else {
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction);
GameJavaVersion gameJavaVersion;
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
gameJavaVersion = version.getJavaVersion();
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
else
gameJavaVersion = null;
setting.setJava(null);
setting.setDefaultJavaPath(null);
setting.setJavaVersion(JavaVersion.fromCurrentEnvironment());
}
if (gameJavaVersion != null) {
FXUtils.runInFX(() -> downloadJava(gameJavaVersion, profile).whenCompleteAsync((downloadedJava, throwable) -> {
if (throwable == null) {
setting.setJavaAutoSelected();
future.complete(downloadedJava);
} else {
LOG.warning("Failed to download java", throwable);
breakAction.run();
}
}, Schedulers.javafx()));
return result;
}
return Task.fromCompletableFuture(future);
} else {
return Task.completed(javaVersion);
}
}).thenComposeAsync(javaVersion -> {
return Task.allOf(Task.completed(javaVersion), Task.supplyAsync(() -> JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version)));
}).thenComposeAsync(Schedulers.javafx(), javaVersions -> {
JavaVersion javaVersion = (JavaVersion) javaVersions.get(0);
JavaVersion suggestedJavaVersion = (JavaVersion) javaVersions.get(1);
if (setting.isJavaAutoSelected()) return Task.completed(javaVersion);
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA_LINUX_JAVA_8)) {
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
FXUtils.runInFX(() -> Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction));
return result;
} else {
violatedMandatoryConstraints.remove(JavaVersionConstraint.VANILLA_LINUX_JAVA_8);
}
}
JavaVersionConstraint violatedMandatoryConstraint = null;
List<JavaVersionConstraint> violatedSuggestedConstraints = null;
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.LAUNCH_WRAPPER)) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"),
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
}
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, javaVersion, analyzer)) {
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) {
violatedMandatoryConstraint = constraint;
} else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) {
if (violatedSuggestedConstraints == null)
violatedSuggestedConstraints = new ArrayList<>(1);
violatedSuggestedConstraints.add(constraint);
if (!violatedMandatoryConstraints.isEmpty()) {
FXUtils.runInFX(() -> Controllers.dialog(
i18n("launch.advice.unknown") + "\n" + violatedMandatoryConstraints,
i18n("message.error"),
MessageType.ERROR,
breakAction
));
return result;
}
}
}
}
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user"));
List<String> suggestions = new ArrayList<>();
if (violatedMandatoryConstraint != null) {
if (suggestedJavaVersion != null) {
Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> {
setting.setJavaAutoSelected();
future.complete(suggestedJavaVersion);
}, breakAction);
return Task.fromCompletableFuture(future);
} else {
switch (violatedMandatoryConstraint) {
case GAME_JSON:
downloadJava(gameVersion.toString(), version.getJavaVersion(), profile)
.thenAcceptAsync(downloadedJavaVersion -> {
setting.setJavaVersion(downloadedJavaVersion);
future.complete(downloadedJavaVersion);
}, Schedulers.javafx())
.whenCompleteAsync((result, throwable) -> {
LOG.log(Level.WARNING, "Failed to download java", throwable);
breakAction.run();
}, Schedulers.javafx());
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_16:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_17:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 17), i18n("message.warning"),
() -> FXUtils.openLink(OPENJDK_DOWNLOAD_LINK), null);
breakAction.run();
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_8:
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
case VANILLA_LINUX_JAVA_8:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
} else {
break;
}
case LAUNCH_WRAPPER:
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction);
return Task.fromCompletableFuture(future);
}
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && java.getPlatform().getArchitecture() == Architecture.X86) {
suggestions.add(i18n("launch.advice.different_platform"));
}
}
List<String> suggestions = new ArrayList<>(0);
// 32-bit JVM cannot make use of too much memory.
if (java.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
}
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 && javaVersion.getPlatform().getArchitecture() == Architecture.X86) {
suggestions.add(i18n("launch.advice.different_platform"));
}
// 32-bit JVM cannot make use of too much memory.
if (javaVersion.getBits() == Bits.BIT_32 && setting.getMaxMemory() > 1.5 * 1024) {
// 1.5 * 1024 is an inaccurate number.
// Actual memory limit depends on operating system and memory.
suggestions.add(i18n("launch.advice.too_large_memory_for_32bit"));
}
if (violatedSuggestedConstraints != null) {
for (JavaVersionConstraint violatedSuggestedConstraint : violatedSuggestedConstraints) {
switch (violatedSuggestedConstraint) {
case MODDED_JAVA_7:
@@ -504,7 +506,7 @@ public final class LauncherHelper {
break;
case MODDED_JAVA_8:
// Minecraft>=1.7.10+Forge accepts Java 8
if (javaVersion.getParsedVersion() < 8)
if (java.getParsedVersion() < 8)
suggestions.add(i18n("launch.advice.newer_java"));
else
suggestions.add(i18n("launch.advice.modded_java", 8, gameVersion));
@@ -528,127 +530,101 @@ public final class LauncherHelper {
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
&& isCompatibleWithX86Java()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
break;
default:
suggestions.add(violatedSuggestedConstraint.name());
}
}
}
// Cannot allocate too much memory exceeding free space.
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY));
}
VersionNumber forgeVersion = version.getLibraries().stream()
.filter(it -> it.is("net.minecraftforge", "forge"))
.findFirst()
.map(library -> VersionNumber.asVersion(library.getVersion()))
.orElse(null);
// Forge 2760~2773 will crash game with LiteLoader.
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
}
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
}
if (suggestions.isEmpty()) {
if (!future.isDone()) {
future.complete(javaVersion);
// Cannot allocate too much memory exceeding free space.
if (OperatingSystem.TOTAL_MEMORY > 0 && OperatingSystem.TOTAL_MEMORY < setting.getMaxMemory()) {
suggestions.add(i18n("launch.advice.not_enough_space", OperatingSystem.TOTAL_MEMORY));
}
} else {
String message;
if (suggestions.size() == 1) {
message = i18n("launch.advice", suggestions.get(0));
VersionNumber forgeVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.FORGE)
.map(VersionNumber::asVersion)
.orElse(null);
// Forge 2760~2773 will crash game with LiteLoader.
boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0);
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) {
suggestions.add(i18n("launch.advice.forge2760_liteloader"));
}
// OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions.
boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0);
boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine"));
if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) {
suggestions.add(i18n("launch.advice.forge28_2_2_optifine"));
}
if (suggestions.isEmpty()) {
if (!future.isDone()) {
future.complete(java);
}
} else {
message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n")));
String message;
if (suggestions.size() == 1) {
message = i18n("launch.advice", suggestions.get(0));
} else {
message = i18n("launch.advice.multi", suggestions.stream().map(it -> "→ " + it).collect(Collectors.joining("\n")));
}
FXUtils.runInFX(() -> Controllers.confirm(
message,
i18n("message.warning"),
MessageType.WARNING,
() -> future.complete(java),
breakAction));
}
Controllers.confirm(message, i18n("message.warning"), MessageType.WARNING, () -> future.complete(javaVersion), breakAction);
}
return result;
});
}
return Task.fromCompletableFuture(future);
}).withStage("launch.state.java");
return task.withStage("launch.state.java");
}
private static CompletableFuture<JavaVersion> downloadJava(String gameVersion, GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
JFXHyperlink link = new JFXHyperlink(i18n("download.external_link"));
link.setOnAction(e -> {
if (javaVersion.getMajorVersion() == JavaVersion.JAVA_8) {
FXUtils.openLink(ORACLEJDK_DOWNLOAD_LINK);
} else {
FXUtils.openLink(OPENJDK_DOWNLOAD_LINK);
}
future.completeExceptionally(new CancellationException());
});
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, Profile profile) {
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Controllers.dialog(new MessageDialogPane.Builder(
i18n("launch.advice.require_newer_java_version",
gameVersion,
javaVersion.getMajorVersion()),
i18n("launch.advice.require_newer_java_version", javaVersion.getMajorVersion()),
i18n("message.warning"),
MessageType.QUESTION)
.addAction(link)
.yesOrNo(() -> {
downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider())
.thenAcceptAsync(future::complete)
.exceptionally(throwable -> {
Throwable resolvedException = resolveException(throwable);
LOG.log(Level.WARNING, "Failed to download java", throwable);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
DownloadProvider downloadProvider = profile.getDependency().getDownloadProvider();
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
future.complete(result);
} else {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
future.completeExceptionally(new CancellationException());
}
future.completeExceptionally(new CancellationException());
return null;
});
}), i18n("download.java"), new TaskCancellationAction(() -> future.completeExceptionally(new CancellationException())));
}, () -> future.completeExceptionally(new CancellationException())).build());
return future;
}
/**
* Directly start java downloading.
*
* @param javaVersion target Java version
* @param downloadProvider download provider
* @return JavaVersion, null if we failed to download java, failed if an error occurred when downloading.
*/
private static CompletableFuture<JavaVersion> downloadJavaImpl(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
Controllers.taskDialog(JavaRepository.downloadJava(javaVersion, downloadProvider)
.whenComplete(Schedulers.javafx(), (downloadedJava, exception) -> {
if (exception != null) {
future.completeExceptionally(exception);
} else {
future.complete(downloadedJava);
}
}), i18n("download.java"), TaskCancellationAction.NORMAL);
return future;
}
private static Task<AuthInfo> logIn(Account account) {
return Task.composeAsync(() -> {
try {
return Task.completed(account.logIn());
} catch (CredentialExpiredException e) {
LOG.log(Level.INFO, "Credential has expired", e);
LOG.info("Credential has expired", e);
return Task.completed(DialogController.logIn(account));
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Authentication failed, try skipping refresh", e);
LOG.warning("Authentication failed, try skipping refresh", e);
CompletableFuture<Task<AuthInfo>> future = new CompletableFuture<>();
runInFX(() -> {
@@ -679,7 +655,7 @@ public final class LauncherHelper {
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
runLater(() -> {
Optional.ofNullable(Controllers.getStage())
.ifPresent(Stage::show);
});
@@ -690,9 +666,9 @@ public final class LauncherHelper {
case CLOSE:
throw new Error("Never get to here");
case HIDE:
Platform.runLater(() -> {
runLater(() -> {
// Shut down the platform when user closed log window.
Platform.setImplicitExit(true);
setImplicitExit(true);
// If we use Launcher.stop(), log window will be halt immediately.
Launcher.stopWithoutPlatform();
});
@@ -711,14 +687,14 @@ public final class LauncherHelper {
private final Version version;
private final LaunchOptions launchOptions;
private ManagedProcess process;
private boolean lwjgl;
private volatile boolean lwjgl;
private LogWindow logWindow;
private final boolean detectWindow;
private final ArrayDeque<String> logs;
private final ArrayDeque</*Log4jLevel*/Object> levels;
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
private final CircularArrayList<Log> logs;
private final CountDownLatch launchingLatch;
private final String forbiddenAccessToken;
private Thread submitLogThread;
private LinkedBlockingQueue<Log> logBuffer;
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
this.repository = repository;
@@ -727,10 +703,7 @@ public final class LauncherHelper {
this.launchingLatch = launchingLatch;
this.detectWindow = detectWindow;
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;
final int numLogs = config().getLogLines() + 1;
this.logs = new ArrayDeque<>(numLogs);
this.levels = new ArrayDeque<>(numLogs);
this.logs = new CircularArrayList<>(Log.getLogLines() + 1);
}
@Override
@@ -746,18 +719,66 @@ public final class LauncherHelper {
LOG.info("Process ClassPath: " + classpath);
}
if (showLogs)
Platform.runLater(() -> {
logWindow = new LogWindow(process);
logWindow.showNormal();
if (showLogs) {
CountDownLatch logWindowLatch = new CountDownLatch(1);
runLater(() -> {
logWindow = new LogWindow(process, logs);
logWindow.show();
logWindowLatch.countDown();
});
logBuffer = new LinkedBlockingQueue<>();
submitLogThread = Lang.thread(new Runnable() {
private final ArrayList<Log> currentLogs = new ArrayList<>();
private final Semaphore semaphore = new Semaphore(0);
private void submitLogs() {
if (currentLogs.size() == 1) {
Log log = currentLogs.get(0);
runLater(() -> logWindow.logLine(log));
} else {
runLater(() -> {
logWindow.logLines(currentLogs);
semaphore.release();
});
semaphore.acquireUninterruptibly();
}
currentLogs.clear();
}
@Override
public void run() {
while (true) {
try {
currentLogs.add(logBuffer.take());
//noinspection BusyWait
Thread.sleep(200); // Wait for more logs
} catch (InterruptedException e) {
break;
}
logBuffer.drainTo(currentLogs);
submitLogs();
}
do {
submitLogs();
} while (logBuffer.drainTo(currentLogs) > 0);
}
}, "Game Log Submitter", true);
try {
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void finishLaunch() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
Platform.runLater(() -> {
runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
@@ -770,11 +791,11 @@ public final class LauncherHelper {
// Never come to here.
break;
case KEEP:
Platform.runLater(launchingLatch::countDown);
runLater(launchingLatch::countDown);
break;
case HIDE:
launchingLatch.countDown();
Platform.runLater(() -> {
runLater(() -> {
// If application was stopped and execution services did not finish termination,
// these codes will be executed.
if (Controllers.getStage() != null) {
@@ -790,44 +811,37 @@ public final class LauncherHelper {
@Override
public void onLog(String log, boolean isErrorStream) {
String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, "<access token>");
if (isErrorStream)
System.err.println(filteredLog);
System.err.println(log);
else
System.out.println(filteredLog);
System.out.println(log);
Log4jLevel level;
if (isErrorStream)
level = Log4jLevel.ERROR;
else
level = showLogs ? Optional.ofNullable(Log4jLevel.guessLevel(filteredLog)).orElse(Log4jLevel.INFO) : null;
synchronized (this) {
logs.add(filteredLog);
levels.add(level != null ? level : Optional.empty()); // Use 'Optional.empty()' as hole
if (logs.size() > config().getLogLines()) {
logs.removeFirst();
levels.removeFirst();
}
}
log = StringUtils.parseEscapeSequence(log);
if (forbiddenAccessToken != null)
log = log.replace(forbiddenAccessToken, "<access token>");
Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
if (showLogs) {
try {
logWindowLatch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
if (level == null)
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
logBuffer.add(new Log(log, level));
} else {
synchronized (this) {
logs.addLast(new Log(log, level));
if (logs.size() > Log.getLogLines())
logs.removeFirst();
}
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
}
if (!lwjgl) {
String lowerCaseLog = filteredLog.toLowerCase(Locale.ROOT);
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
lwjgl = true;
finishLaunch();
synchronized (this) {
if (!lwjgl) {
lwjgl = true;
finishLaunch();
}
}
}
}
}
@@ -835,7 +849,13 @@ public final class LauncherHelper {
@Override
public void onExit(int exitCode, ExitType exitType) {
if (showLogs) {
Platform.runLater(() -> logWindow.logLine(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
logBuffer.add(new Log(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
submitLogThread.interrupt();
try {
submitLogThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
launchingLatch.countDown();
@@ -844,14 +864,16 @@ public final class LauncherHelper {
return;
// Game crashed before opening the game window.
if (!lwjgl) finishLaunch();
if (!lwjgl) {
synchronized (this) {
if (!lwjgl)
finishLaunch();
}
}
if (exitType != ExitType.NORMAL) {
ArrayList<Pair<String, Log4jLevel>> pairs = new ArrayList<>(logs.size());
Lang.forEachZipped(logs, levels,
(log, l) -> pairs.add(pair(log, l instanceof Log4jLevel ? ((Log4jLevel) l) : Optional.ofNullable(Log4jLevel.guessLevel(log)).orElse(Log4jLevel.INFO))));
repository.markVersionLaunchedAbnormally(version.getId());
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, pairs).show());
runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
}
checkExit();
@@ -27,10 +27,9 @@ import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public abstract class LocalizedRemoteModRepository implements RemoteModRepository {
private static final int CONTAIN_CHINESE_WEIGHT = 10;
@@ -69,7 +68,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
SearchResult searchResult = getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, String.join(" ", englishSearchFiltersSet), getBackedRemoteModRepositorySortOrder(), sortOrder);
for (Iterator<RemoteMod> iterator = searchResult.getUnsortedResults().iterator(); iterator.hasNext(); ) {
if (chineseIndex > englishIndex) {
LOG.log(Level.WARNING, "Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
LOG.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
continue;
}
@@ -0,0 +1,72 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.Log4jLevel;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public final class Log {
public static final int DEFAULT_LOG_LINES = 2000;
public static int getLogLines() {
Integer lines = config().getLogLines();
return lines != null && lines > 0 ? lines : DEFAULT_LOG_LINES;
}
private final String log;
private Log4jLevel level;
private boolean selected = false;
public Log(String log) {
this.log = log;
}
public Log(String log, Log4jLevel level) {
this.log = log;
this.level = level;
}
public String getLog() {
return log;
}
public Log4jLevel getLevel() {
Log4jLevel level = this.level;
if (level == null) {
level = Log4jLevel.guessLevel(log);
if (level == null)
level = Log4jLevel.INFO;
this.level = level;
}
return level;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
@Override
public String toString() {
return log;
}
}
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Zipper;
@@ -35,9 +35,8 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class LogExporter {
private LogExporter() {
@@ -70,9 +69,9 @@ public final class LogExporter {
processLogs(runDirectory, "*.log", "runDirectory", zipper);
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper);
zipper.putTextFile(Logging.getLogs(), "hmcl.log");
zipper.putTextFile(LOG.getLogs(), "hmcl.log");
zipper.putTextFile(logs, "minecraft.log");
zipper.putTextFile(Logging.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
zipper.putTextFile(Logger.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
for (String id : versions) {
Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json");
@@ -95,16 +94,16 @@ public final class LogExporter {
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
if (time.toMillis() >= processStartTime) {
try {
String crashLog = Logging.filterForbiddenToken(FileUtils.readText(file, OperatingSystem.NATIVE_CHARSET));
String crashLog = Logger.filterForbiddenToken(FileUtils.readText(file, OperatingSystem.NATIVE_CHARSET));
zipper.putTextFile(crashLog, file.getFileName().toString());
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to read log file: " + file, e);
LOG.warning("Failed to read log file: " + file, e);
}
}
}
}
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to find any log on " + logDirectory, e);
LOG.warning("Failed to find any log on " + logDirectory, e);
}
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
@@ -141,8 +140,7 @@ public final class ModpackHelper {
throw new FileNotFoundException(file.getPath());
else
try {
return JsonUtils.GSON.fromJson(FileUtils.readText(file), new TypeToken<ModpackConfiguration<?>>() {
}.getType());
return JsonUtils.GSON.fromJson(FileUtils.readText(file), ModpackConfiguration.class);
} catch (JsonParseException e) {
throw new IOException("Malformed modpack configuration");
}
@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.auth.OAuth;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
@@ -34,10 +33,10 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
@@ -80,7 +79,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
try {
session.parseBody(files);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read post data", e);
LOG.warning("Failed to read post data", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
} catch (ResponseException re) {
return newFixedLengthResponse(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
@@ -97,7 +96,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
idToken = query.get("id_token");
future.complete(query.get("code"));
} else {
Logging.LOG.warning("Error: " + parameters);
LOG.warning("Error: " + parameters);
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
}
@@ -106,7 +105,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to load html");
LOG.error("Failed to load html", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
}
thread(() -> {
@@ -114,7 +113,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
Thread.sleep(1000);
stop();
} catch (InterruptedException e) {
Logging.LOG.log(Level.SEVERE, "Failed to sleep for 1 second");
LOG.error("Failed to sleep for 1 second");
}
});
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
@@ -52,13 +52,12 @@ import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author yushijinhun
@@ -116,7 +115,7 @@ public final class TexturesLoader {
} catch (Exception e) {
if (Files.isRegularFile(file)) {
// concurrency conflict?
LOG.log(Level.WARNING, "Failed to download texture " + texture.getUrl() + ", but the file is available", e);
LOG.warning("Failed to download texture " + texture.getUrl() + ", but the file is available", e);
} else {
throw new IOException("Failed to download texture " + texture.getUrl());
}
@@ -140,30 +139,42 @@ public final class TexturesLoader {
// ====
// ==== Skins ====
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
private static final String[] DEFAULT_SKINS = {"alex", "ari", "efe", "kai", "makena", "noor", "steve", "sunny", "zuri"};
static {
loadDefaultSkin("/assets/img/skin/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/skin/alex.png", TextureModel.ALEX);
public static Image getDefaultSkinImage() {
return FXUtils.newBuiltinImage("/assets/img/skin/wide/steve.png");
}
private static void loadDefaultSkin(String path, TextureModel model) {
DEFAULT_SKINS.put(model, new LoadedTexture(FXUtils.newBuiltinImage(path), singletonMap("model", model.modelName)));
public static LoadedTexture getDefaultSkin(UUID uuid) {
int idx = Math.floorMod(uuid.hashCode(), DEFAULT_SKINS.length * 2);
TextureModel model;
Image skin;
if (idx < DEFAULT_SKINS.length) {
model = TextureModel.SLIM;
skin = FXUtils.newBuiltinImage("/assets/img/skin/slim/" + DEFAULT_SKINS[idx] + ".png");
} else {
model = TextureModel.WIDE;
skin = FXUtils.newBuiltinImage("/assets/img/skin/wide/" + DEFAULT_SKINS[idx - DEFAULT_SKINS.length] + ".png");
}
return new LoadedTexture(skin, singletonMap("model", model.modelName));
}
public static LoadedTexture getDefaultSkin(TextureModel model) {
return DEFAULT_SKINS.get(model);
public static TextureModel getDefaultModel(UUID uuid) {
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).getMetadata().get("model"))
? TextureModel.WIDE
: TextureModel.SLIM;
}
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(uuid));
LoadedTexture uuidFallback = getDefaultSkin(uuid);
return BindingMapping.of(service.getProfileRepository().binding(uuid))
.map(profile -> profile
.flatMap(it -> {
try {
return YggdrasilService.getTextures(it);
} catch (ServerResponseMalformedException e) {
LOG.log(Level.WARNING, "Failed to parse texture payload", e);
LOG.warning("Failed to parse texture payload", e);
return Optional.empty();
}
})
@@ -176,7 +187,7 @@ public final class TexturesLoader {
try {
return loadTexture(texture);
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
@@ -187,7 +198,7 @@ public final class TexturesLoader {
}
public static ObservableValue<LoadedTexture> skinBinding(Account account) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
LoadedTexture uuidFallback = getDefaultSkin(account.getUUID());
if (account instanceof OfflineAccount) {
OfflineAccount offlineAccount = (OfflineAccount) account;
@@ -200,7 +211,7 @@ public final class TexturesLoader {
if (skin != null) {
skin.load(username).setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
LOG.log(Level.WARNING, "Failed to load texture", exception);
LOG.warning("Failed to load texture", exception);
} else if (result != null && result.getSkin() != null && result.getSkin().getImage() != null) {
Map<String, String> metadata;
if (result.getModel() != null) {
@@ -231,7 +242,7 @@ public final class TexturesLoader {
try {
return loadTexture(texture);
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
@@ -348,7 +359,7 @@ public final class TexturesLoader {
fxAvatarBinding(canvas, skinBinding(account));
else {
unbindAvatar(canvas);
drawAvatar(canvas, getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image);
drawAvatar(canvas, getDefaultSkin(account.getUUID()).image);
}
}
@@ -0,0 +1,221 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaRemoteFiles;
import org.jackhuang.hmcl.game.DownloadInfo;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class HMCLJavaRepository implements JavaRepository {
public static final String MOJANG_JAVA_PREFIX = "mojang-";
private final Path root;
public HMCLJavaRepository(Path root) {
this.root = root;
}
public Path getPlatformRoot(Platform platform) {
return root.resolve(platform.toString());
}
@Override
public Path getJavaDir(Platform platform, String name) {
return getPlatformRoot(platform).resolve(name);
}
public Path getJavaDir(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaDir(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
@Override
public Path getManifestFile(Platform platform, String name) {
return getPlatformRoot(platform).resolve(name + ".json");
}
public Path getManifestFile(Platform platform, GameJavaVersion gameJavaVersion) {
return getManifestFile(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
public boolean isInstalled(Platform platform, String name) {
return Files.exists(getManifestFile(platform, name));
}
public boolean isInstalled(Platform platform, GameJavaVersion gameJavaVersion) {
return isInstalled(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
public @Nullable Path getJavaExecutable(Platform platform, String name) {
Path javaDir = getJavaDir(platform, name);
try {
return JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException ignored) {
if (platform.getOperatingSystem() == OperatingSystem.OSX) {
try {
return JavaManager.getMacExecutable(javaDir).toRealPath();
} catch (IOException ignored1) {
}
}
}
return null;
}
public @Nullable Path getJavaExecutable(Platform platform, GameJavaVersion gameJavaVersion) {
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.getComponent());
}
@Override
public Collection<JavaRuntime> getAllJava(Platform platform) {
Path root = getPlatformRoot(platform);
if (!Files.isDirectory(root))
return Collections.emptyList();
ArrayList<JavaRuntime> list = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(root)) {
for (Path file : stream) {
try {
String name = file.getFileName().toString();
if (name.endsWith(".json") && Files.isRegularFile(file)) {
Path javaDir = file.resolveSibling(name.substring(0, name.length() - ".json".length()));
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException e) {
if (platform.getOperatingSystem() == OperatingSystem.OSX)
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
else
throw e;
}
if (Files.isDirectory(javaDir)) {
JavaManifest manifest;
try (InputStream input = Files.newInputStream(file)) {
manifest = JsonUtils.fromJsonFully(input, JavaManifest.class);
}
list.add(JavaRuntime.of(executable, manifest.getInfo(), true));
}
}
} catch (Throwable e) {
LOG.warning("Failed to parse " + file, e);
}
}
} catch (IOException ignored) {
}
return list;
}
@Override
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
Path javaDir = getJavaDir(platform, gameJavaVersion);
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
Path executable;
try {
executable = JavaManager.getExecutable(javaDir).toRealPath();
} catch (IOException e) {
if (platform.getOperatingSystem() == OperatingSystem.OSX)
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
else
throw e;
}
JavaInfo info;
if (JavaManager.isCompatible(platform))
info = JavaInfo.fromExecutable(executable, false);
else
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
Map<String, Object> update = new LinkedHashMap<>();
update.put("provider", "mojang");
update.put("component", gameJavaVersion.getComponent());
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
result.remoteFiles.getFiles().forEach((path, file) -> {
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
if (downloadInfo != null) {
files.put(path, new JavaLocalFiles.LocalFile(downloadInfo.getSha1(), downloadInfo.getSize()));
}
} else if (file instanceof MojangJavaRemoteFiles.RemoteDirectory) {
files.put(path, new JavaLocalFiles.LocalDirectory());
} else if (file instanceof MojangJavaRemoteFiles.RemoteLink) {
files.put(path, new JavaLocalFiles.LocalLink(((MojangJavaRemoteFiles.RemoteLink) file).getTarget()));
}
});
JavaManifest manifest = new JavaManifest(info, update, files);
FileUtils.writeText(getManifestFile(platform, gameJavaVersion), JsonUtils.GSON.toJson(manifest));
return JavaRuntime.of(executable, info, true);
});
}
public Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
Path javaDir = getJavaDir(platform, name);
return new JavaInstallTask(javaDir, update, archiveFile).thenApplyAsync(result -> {
if (!result.getInfo().getPlatform().equals(platform))
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
FileUtils.writeText(getManifestFile(platform, name), JsonUtils.GSON.toJson(result));
return JavaRuntime.of(executable, result.getInfo(), true);
});
}
@Override
public Task<Void> getUninstallJavaTask(Platform platform, String name) {
return Task.runAsync(() -> {
Files.deleteIfExists(getManifestFile(platform, name));
FileUtils.deleteDirectory(getJavaDir(platform, name).toFile());
});
}
@Override
public Task<Void> getUninstallJavaTask(JavaRuntime java) {
return Task.runAsync(() -> {
Path root = getPlatformRoot(java.getPlatform());
Path relativized = root.relativize(java.getBinary());
if (relativized.getNameCount() > 1) {
String name = relativized.getName(0).toString();
Files.deleteIfExists(getManifestFile(java.getPlatform(), name));
FileUtils.deleteDirectory(getJavaDir(java.getPlatform(), name).toFile());
}
});
}
}
@@ -0,0 +1,116 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Hex;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author Glavo
*/
public final class JavaInstallTask extends Task<JavaManifest> {
private final Path targetDir;
private final Map<String, Object> update;
private final Path archiveFile;
private final Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
private final ArrayList<String> nameStack = new ArrayList<>();
private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
private final MessageDigest messageDigest = DigestUtils.getDigest("SHA-1");
public JavaInstallTask(Path targetDir, Map<String, Object> update, Path archiveFile) {
this.targetDir = targetDir;
this.update = update;
this.archiveFile = archiveFile;
}
@Override
public void execute() throws Exception {
JavaInfo info;
try (ArchiveFileTree<?, ?> tree = ArchiveFileTree.open(archiveFile)) {
info = JavaInfo.fromArchive(tree);
copyDirContent(tree, targetDir);
}
setResult(new JavaManifest(info, update, files));
}
private <F, E extends ArchiveEntry> void copyDirContent(ArchiveFileTree<F, E> tree, Path targetDir) throws IOException {
copyDirContent(tree, tree.getRoot().getSubDirs().values().iterator().next(), targetDir);
}
private <F, E extends ArchiveEntry> void copyDirContent(ArchiveFileTree<F, E> tree, ArchiveFileTree.Dir<E> dir, Path targetDir) throws IOException {
Files.createDirectories(targetDir);
for (Map.Entry<String, E> pair : dir.getFiles().entrySet()) {
Path path = targetDir.resolve(pair.getKey());
E entry = pair.getValue();
nameStack.add(pair.getKey());
if (tree.isLink(entry)) {
String linkTarget = tree.getLink(entry);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalLink(linkTarget));
Files.createSymbolicLink(path, Paths.get(linkTarget));
} else {
long size = 0L;
try (InputStream input = tree.getInputStream(entry);
OutputStream output = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
messageDigest.reset();
int c;
while ((c = input.read(buffer)) > 0) {
size += c;
output.write(buffer, 0, c);
messageDigest.update(buffer, 0, c);
}
}
if (tree.isExecutable(entry))
//noinspection ResultOfMethodCallIgnored
path.toFile().setExecutable(true);
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalFile(Hex.encodeHex(messageDigest.digest()), size));
}
nameStack.remove(nameStack.size() - 1);
}
for (Map.Entry<String, ArchiveFileTree.Dir<E>> pair : dir.getSubDirs().entrySet()) {
nameStack.add(pair.getKey());
files.put(String.join("/", nameStack), new JavaLocalFiles.LocalDirectory());
copyDirContent(tree, pair.getValue(), targetDir.resolve(pair.getKey()));
nameStack.remove(nameStack.size() - 1);
}
}
}
@@ -0,0 +1,128 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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.java;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import java.lang.reflect.Type;
/**
* @author Glavo
*/
public final class JavaLocalFiles {
@JsonAdapter(Serializer.class)
public abstract static class Local {
private final String type;
Local(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
public static final class LocalFile extends Local {
private final String sha1;
private final long size;
public LocalFile(String sha1, long size) {
super("file");
this.sha1 = sha1;
this.size = size;
}
public String getSha1() {
return sha1;
}
public long getSize() {
return size;
}
}
public static final class LocalDirectory extends Local {
public LocalDirectory() {
super("directory");
}
}
public static final class LocalLink extends Local {
private final String target;
public LocalLink(String target) {
super("link");
this.target = target;
}
public String getTarget() {
return target;
}
}
public static class Serializer implements JsonSerializer<Local>, JsonDeserializer<Local> {
@Override
public JsonElement serialize(Local src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty("type", src.getType());
if (src instanceof LocalFile) {
obj.addProperty("sha1", ((LocalFile) src).getSha1());
obj.addProperty("size", ((LocalFile) src).getSize());
} else if (src instanceof LocalLink) {
obj.addProperty("target", ((LocalLink) src).getTarget());
}
return obj;
}
@Override
public Local deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject())
throw new JsonParseException(json.toString());
JsonObject obj = json.getAsJsonObject();
if (!obj.has("type"))
throw new JsonParseException(json.toString());
String type = obj.getAsJsonPrimitive("type").getAsString();
try {
switch (type) {
case "file": {
String sha1 = obj.getAsJsonPrimitive("sha1").getAsString();
long size = obj.getAsJsonPrimitive("size").getAsLong();
return new LocalFile(sha1, size);
}
case "directory": {
return new LocalDirectory();
}
case "link": {
String target = obj.getAsJsonPrimitive("target").getAsString();
return new LocalLink(target);
}
default:
throw new AssertionError("unknown type: " + type);
}
} catch (Throwable e) {
throw new JsonParseException(json.toString());
}
}
}
}
@@ -0,0 +1,698 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.game.JavaVersionConstraint;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.ConfigHolder;
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.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaManager {
private JavaManager() {
}
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_DIRECTORY.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return "windows-x86";
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "windows-x64";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "windows-arm64";
}
} else if (platform.getOperatingSystem() == OperatingSystem.LINUX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return "linux-i386";
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "linux";
}
} else if (platform.getOperatingSystem() == OperatingSystem.OSX) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "mac-os";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "mac-os-arm64";
}
}
return null;
}
public static Path getExecutable(Path javaHome) {
return javaHome.resolve("bin").resolve(OperatingSystem.CURRENT_OS.getJavaExecutable());
}
public static Path getMacExecutable(Path javaHome) {
return javaHome.resolve("jre.bundle/Contents/Home/bin/java");
}
public static boolean isCompatible(Platform platform) {
if (platform.getOperatingSystem() != OperatingSystem.CURRENT_OS)
return false;
Architecture architecture = platform.getArchitecture();
if (architecture == Architecture.SYSTEM_ARCH || architecture == Architecture.CURRENT_ARCH)
return true;
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86;
break;
case LINUX:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
break;
case OSX:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return architecture == Architecture.X86_64;
break;
}
return false;
}
private static volatile Map<Path, JavaRuntime> allJava;
private static final CountDownLatch LATCH = new CountDownLatch(1);
private static final ObjectProperty<Collection<JavaRuntime>> allJavaProperty = new SimpleObjectProperty<>();
private static Map<Path, JavaRuntime> getAllJavaMap() throws InterruptedException {
Map<Path, JavaRuntime> map = allJava;
if (map == null) {
LATCH.await();
map = allJava;
}
return map;
}
private static void updateAllJavaProperty(Map<Path, JavaRuntime> javaRuntimes) {
JavaRuntime[] array = javaRuntimes.values().toArray(new JavaRuntime[0]);
Arrays.sort(array);
allJavaProperty.set(Arrays.asList(array));
}
public static boolean isInitialized() {
return allJava != null;
}
public static Collection<JavaRuntime> getAllJava() throws InterruptedException {
return getAllJavaMap().values();
}
public static ObjectProperty<Collection<JavaRuntime>> getAllJavaProperty() {
return allJavaProperty;
}
public static JavaRuntime getJava(Path executable) throws IOException, InterruptedException {
executable = executable.toRealPath();
JavaRuntime javaRuntime = getAllJavaMap().get(executable);
if (javaRuntime != null) {
return javaRuntime;
}
JavaInfo info = JavaInfo.fromExecutable(executable);
return JavaRuntime.of(executable, info, false);
}
public static void refresh() {
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (result != null) {
LATCH.await();
allJava = result;
updateAllJavaProperty(result);
}
}).start();
}
public static Task<JavaRuntime> getAddJavaTask(Path binary) {
return Task.supplyAsync("Get Java", () -> JavaManager.getJava(binary))
.thenApplyAsync(Schedulers.javafx(), javaRuntime -> {
if (!JavaManager.isCompatible(javaRuntime.getPlatform())) {
throw new UnsupportedPlatformException("Incompatible platform: " + javaRuntime.getPlatform());
}
String pathString = javaRuntime.getBinary().toString();
ConfigHolder.globalConfig().getDisabledJava().remove(pathString);
if (ConfigHolder.globalConfig().getUserJava().add(pathString)) {
addJava(javaRuntime);
}
return javaRuntime;
});
}
public static Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
return REPOSITORY.getDownloadJavaTask(downloadProvider, platform, gameJavaVersion)
.thenApplyAsync(Schedulers.javafx(), java -> {
addJava(java);
return java;
});
}
public static Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
return REPOSITORY.getInstallJavaTask(platform, name, update, archiveFile)
.thenApplyAsync(Schedulers.javafx(), java -> {
addJava(java);
return java;
});
}
public static Task<Void> getUninstallJavaTask(JavaRuntime java) {
assert java.isManaged();
Path root = REPOSITORY.getPlatformRoot(java.getPlatform());
Path relativized = root.relativize(java.getBinary());
if (relativized.getNameCount() > 1) {
FXUtils.runInFX(() -> {
try {
removeJava(java);
} catch (InterruptedException e) {
throw new AssertionError("Unreachable code", e);
}
});
String name = relativized.getName(0).toString();
return REPOSITORY.getUninstallJavaTask(java.getPlatform(), name);
} else {
return Task.completed(null);
}
}
// FXThread
public static void addJava(JavaRuntime java) throws InterruptedException {
Map<Path, JavaRuntime> oldMap = getAllJavaMap();
if (!oldMap.containsKey(java.getBinary())) {
HashMap<Path, JavaRuntime> newMap = new HashMap<>(oldMap);
newMap.put(java.getBinary(), java);
allJava = newMap;
updateAllJavaProperty(newMap);
}
}
// FXThread
public static void removeJava(JavaRuntime java) throws InterruptedException {
removeJava(java.getBinary());
}
// FXThread
public static void removeJava(Path realPath) throws InterruptedException {
Map<Path, JavaRuntime> oldMap = getAllJavaMap();
if (oldMap.containsKey(realPath)) {
HashMap<Path, JavaRuntime> newMap = new HashMap<>(oldMap);
newMap.remove(realPath);
allJava = newMap;
updateAllJavaProperty(newMap);
}
}
private static int compareJavaVersion(JavaRuntime java1, JavaRuntime java2, GameJavaVersion suggestedJavaVersion) {
if (suggestedJavaVersion != null) {
boolean b1 = java1.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
boolean b2 = java2.getParsedVersion() == suggestedJavaVersion.getMajorVersion();
if (b1 != b2)
return b1 ? 1 : -1;
}
return java1.getVersionNumber().compareTo(java2.getVersionNumber());
}
@Nullable
public static JavaRuntime findSuitableJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
return findSuitableJava(getAllJava(), gameVersion, version);
}
@Nullable
public static JavaRuntime findSuitableJava(Collection<JavaRuntime> javaRuntimes, GameVersionNumber gameVersion, Version version) {
LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null;
boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64
&& (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
GameJavaVersion suggestedJavaVersion =
(version != null && gameVersion != null && gameVersion.compareTo("1.7.10") >= 0) ? version.getJavaVersion() : null;
JavaRuntime mandatory = null;
JavaRuntime suggested = null;
for (JavaRuntime java : javaRuntimes) {
if (forceX86) {
if (!java.getArchitecture().isX86())
continue;
} else {
if (java.getArchitecture() != Architecture.SYSTEM_ARCH)
continue;
}
boolean violationMandatory = false;
boolean violationSuggested = false;
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
if (!constraint.checkJava(gameVersion, version, java)) {
if (constraint.isMandatory()) {
violationMandatory = true;
} else {
violationSuggested = true;
}
}
}
}
if (!violationMandatory) {
if (mandatory == null) mandatory = java;
else if (compareJavaVersion(java, mandatory, suggestedJavaVersion) > 0)
mandatory = java;
if (!violationSuggested) {
if (suggested == null) suggested = java;
else if (compareJavaVersion(java, suggested, suggestedJavaVersion) > 0)
suggested = java;
}
}
}
return suggested != null ? suggested : mandatory;
}
public static void initialize() {
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
JavaManager.allJava = allJava;
LATCH.countDown();
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
}
// search java
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables() {
Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM);
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
searchAllJavaInRepository(javaRuntimes, 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);
}
break;
case OSX:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searchAllJavaInRepository(javaRuntimes, Platform.OSX_X86_64);
break;
}
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Runtime Environment\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Java Development Kit\\");
queryJavaInRegistryKey(javaRuntimes, "HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JRE\\");
queryJavaInRegistryKey(javaRuntimes, "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)");
}
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!
break;
case OSX:
tryAddJavaHome(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines/Contents/Home"));
tryAddJavaHome(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines/Contents/Home"));
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"));
// 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@*")) {
for (Path dir : dirs) {
searchAllJavaInDirectory(javaRuntimes, dir);
}
} catch (IOException e) {
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
}
break;
default:
break;
}
// 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));
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, 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);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
}
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
// Search in PATH.
if (System.getenv("PATH") != null) {
String[] paths = System.getenv("PATH").split(OperatingSystem.PATH_SEPARATOR);
for (String path : paths) {
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
} catch (InvalidPathException ignored) {
}
}
}
if (System.getenv("HMCL_JRES") != null) {
String[] paths = System.getenv("HMCL_JRES").split(OperatingSystem.PATH_SEPARATOR);
for (String path : paths) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(path));
} catch (InvalidPathException ignored) {
}
}
}
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
try {
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path: " + javaPath);
}
}
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
if (currentJava != null
&& !javaRuntimes.containsKey(currentJava.getBinary())
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
javaRuntimes.put(currentJava.getBinary(), currentJava);
}
LOG.trace(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;
}
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
Path executable = getExecutable(javaHome);
if (!Files.isRegularFile(executable)) {
return;
}
try {
executable = executable.toRealPath();
} catch (IOException e) {
LOG.warning("Failed to resolve path " + executable, e);
return;
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
Path releaseFile = javaHome.resolve("release");
if (Files.exists(releaseFile)) {
try {
info = JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException e) {
try {
info = JavaInfo.fromExecutable(executable, false);
} catch (IOException e2) {
e2.addSuppressed(e);
LOG.warning("Failed to lookup Java executable at " + executable, e2);
}
}
}
if (info != null && isCompatible(info.getPlatform()))
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
private static void tryAddJavaExecutable(Map<Path, JavaRuntime> javaRuntimes, Path executable) {
try {
executable = executable.toRealPath();
} catch (IOException e) {
return;
}
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
return;
}
JavaInfo info = null;
try {
info = JavaInfo.fromExecutable(executable);
} 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());
}
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return;
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(javaRuntimes, macPath);
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);
}
}
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);
}
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.OSX_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);
}
} 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);
}
} 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));
}
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, String location) {
for (String java : querySubFolders(location)) {
if (!querySubFolders(java).contains(java + "\\MSI"))
continue;
String home = queryRegisterValue(java, "JavaHome");
if (home != null) {
try {
tryAddJavaHome(javaRuntimes, Paths.get(home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
private static List<String> querySubFolders(String location) {
List<String> res = new ArrayList<>();
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (line.startsWith(location) && !line.equals(location)) {
res.add(line);
}
}
}
} catch (IOException e) {
LOG.warning("Failed to query sub folders of " + location, e);
}
return res;
}
private static String queryRegisterValue(String location, String name) {
boolean last = false;
try {
Process process = Runtime.getRuntime().exec(new String[]{"cmd", "/c", "reg", "query", location, "/v", name});
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
for (String line; (line = reader.readLine()) != null; ) {
if (StringUtils.isNotBlank(line)) {
if (last && line.trim().startsWith(name)) {
int begins = line.indexOf(name);
if (begins > 0) {
String s2 = line.substring(begins + name.length());
begins = s2.indexOf("REG_SZ");
if (begins > 0) {
return s2.substring(begins + "REG_SZ".length()).trim();
}
}
}
if (location.equals(line.trim())) {
last = true;
}
}
}
}
} catch (IOException e) {
LOG.warning("Failed to query register value of " + location, e);
}
return null;
}
}
@@ -0,0 +1,112 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
/**
* @author Glavo
*/
@JsonAdapter(JavaManifest.Serializer.class)
public final class JavaManifest {
private final JavaInfo info;
@Nullable
private final Map<String, Object> update;
@Nullable
private final Map<String, JavaLocalFiles.Local> files;
public JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
this.info = info;
this.update = update;
this.files = files;
}
public JavaInfo getInfo() {
return info;
}
public Map<String, Object> getUpdate() {
return update;
}
public Map<String, JavaLocalFiles.Local> getFiles() {
return files;
}
public static final class Serializer implements JsonSerializer<JavaManifest>, JsonDeserializer<JavaManifest> {
private static final Type LOCAL_FILES_TYPE = mapTypeOf(String.class, JavaLocalFiles.Local.class).getType();
@Override
public JsonElement serialize(JavaManifest src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject res = new JsonObject();
res.addProperty("os.name", src.getInfo().getPlatform().getOperatingSystem().getCheckedName());
res.addProperty("os.arch", src.getInfo().getPlatform().getArchitecture().getCheckedName());
res.addProperty("java.version", src.getInfo().getVersion());
res.addProperty("java.vendor", src.getInfo().getVendor());
if (src.getUpdate() != null)
res.add("update", context.serialize(src.getUpdate()));
if (src.getFiles() != null)
res.add("files", context.serialize(src.getFiles(), LOCAL_FILES_TYPE));
return res;
}
@Override
public JavaManifest deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!json.isJsonObject())
throw new JsonParseException(json.toString());
try {
JsonObject jsonObject = json.getAsJsonObject();
OperatingSystem osName = OperatingSystem.parseOSName(jsonObject.getAsJsonPrimitive("os.name").getAsString());
Architecture osArch = Architecture.parseArchName(jsonObject.getAsJsonPrimitive("os.arch").getAsString());
String javaVersion = jsonObject.getAsJsonPrimitive("java.version").getAsString();
String javaVendor = Optional.ofNullable(jsonObject.get("java.vendor")).map(JsonElement::getAsString).orElse(null);
Map<String, Object> update = jsonObject.has("update") ? context.deserialize(jsonObject.get("update"), Map.class) : null;
Map<String, JavaLocalFiles.Local> files = jsonObject.has("files") ? context.deserialize(jsonObject.get("files"), LOCAL_FILES_TYPE) : null;
if (osName == null || osArch == null || javaVersion == null)
throw new JsonParseException(json.toString());
return new JavaManifest(new JavaInfo(Platform.getPlatform(osName, osArch), javaVersion, javaVendor), update, files);
} catch (JsonParseException e) {
throw e;
} catch (Throwable e) {
throw new JsonParseException(e);
}
}
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.reflect.TypeToken;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
@@ -47,7 +46,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.logging.Level;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
@@ -55,24 +53,28 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.Lang.immutableListOf;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author huangyuhui
*/
public final class Accounts {
private Accounts() {}
private Accounts() {
}
private static final AuthlibInjectorArtifactProvider AUTHLIB_INJECTOR_DOWNLOADER = createAuthlibInjectorArtifactProvider();
private static void triggerAuthlibInjectorUpdateCheck() {
if (AUTHLIB_INJECTOR_DOWNLOADER instanceof AuthlibInjectorDownloader) {
Schedulers.io().execute(() -> {
try {
((AuthlibInjectorDownloader) AUTHLIB_INJECTOR_DOWNLOADER).checkUpdate();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to check update for authlib-injector", e);
LOG.warning("Failed to check update for authlib-injector", e);
}
});
}
@@ -88,6 +90,7 @@ public final class Accounts {
// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
static {
type2factory.put("offline", FACTORY_OFFLINE);
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
@@ -131,7 +134,7 @@ public final class Accounts {
private static final String GLOBAL_PREFIX = "$GLOBAL:";
private static final ObservableList<Map<Object, Object>> globalAccountStorages = FXCollections.observableArrayList();
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[]{account});
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
/**
@@ -169,16 +172,13 @@ public final class Accounts {
config().getAccountStorages().setAll(portable);
}
@SuppressWarnings("unchecked")
private static void loadGlobalAccountStorages() {
Path globalAccountsFile = Metadata.HMCL_DIRECTORY.resolve("accounts.json");
if (Files.exists(globalAccountsFile)) {
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
globalAccountStorages.setAll((List<Map<Object, Object>>)
Config.CONFIG_GSON.fromJson(reader, new TypeToken<List<Map<Object, Object>>>() {
}.getType()));
globalAccountStorages.setAll(Config.CONFIG_GSON.fromJson(reader, listTypeOf(mapTypeOf(Object.class, Object.class))));
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to load global accounts", e);
LOG.warning("Failed to load global accounts", e);
}
}
@@ -190,7 +190,7 @@ public final class Accounts {
FileUtils.saveSafely(globalAccountsFile, json);
}
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save global accounts", e);
LOG.error("Failed to save global accounts", e);
}
}
});
@@ -209,7 +209,7 @@ public final class Accounts {
try {
return factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
LOG.warning("Failed to load account: " + storage, e);
return null;
}
}
@@ -321,7 +321,7 @@ public final class Accounts {
try {
finalSelected.logIn();
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to log " + finalSelected + " in", e);
LOG.warning("Failed to log " + finalSelected + " in", e);
}
});
}
@@ -335,7 +335,7 @@ public final class Accounts {
try {
server.fetchMetadataResponse();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + server, e);
LOG.warning("Failed to fetch authlib-injector server metdata: " + server, e);
}
});
}
@@ -466,6 +466,8 @@ public final class Accounts {
} else {
return i18n("account.methods.microsoft.error.unknown", errorCode);
}
} else if (exception instanceof MicrosoftService.XBox400Exception) {
return i18n("account.methods.microsoft.error.wrong_verify_method");
} else if (exception instanceof MicrosoftService.NoMinecraftJavaEditionProfileException) {
return i18n("account.methods.microsoft.error.no_character");
} else if (exception instanceof MicrosoftService.NoXuiException) {
@@ -34,10 +34,9 @@ import java.nio.file.Paths;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class AuthlibInjectorServers implements Validation {
@@ -77,7 +76,7 @@ public final class AuthlibInjectorServers implements Validation {
String content = FileUtils.readText(configLocation);
configInstance = JsonUtils.GSON.fromJson(content, AuthlibInjectorServers.class);
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Malformed authlib-injectors.json", e);
LOG.warning("Malformed authlib-injectors.json", e);
return;
}
@@ -164,7 +164,7 @@ public final class Config implements Cloneable, Observable {
private StringProperty launcherFontFamily = new SimpleStringProperty();
@SerializedName("logLines")
private IntegerProperty logLines = new SimpleIntegerProperty(1000);
private ObjectProperty<Integer> logLines = new SimpleObjectProperty<>();
@SerializedName("titleTransparent")
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
@@ -573,15 +573,15 @@ public final class Config implements Cloneable, Observable {
this.launcherFontFamily.set(launcherFontFamily);
}
public int getLogLines() {
public Integer getLogLines() {
return logLines.get();
}
public void setLogLines(int logLines) {
public void setLogLines(Integer logLines) {
this.logLines.set(logLines);
}
public IntegerProperty logLinesProperty() {
public ObjectProperty<Integer> logLinesProperty() {
return logLines;
}
@@ -29,9 +29,8 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.nio.file.*;
import java.util.Locale;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ConfigHolder {
@@ -81,7 +80,7 @@ public final class ConfigHolder {
configLocation = locateConfig();
LOG.log(Level.INFO, "Config location: " + configLocation);
LOG.info("Config location: " + configLocation);
configInstance = loadConfig();
configInstance.addListener(source -> markConfigDirty());
@@ -91,6 +90,7 @@ public final class ConfigHolder {
Locale.setDefault(config().getLocalization().getLocale());
I18n.setLocale(configInstance.getLocalization());
LOG.setLogRetention(globalConfig().getLogRetention());
Settings.init();
if (newlyCreated) {
@@ -101,7 +101,7 @@ public final class ConfigHolder {
try {
Files.setAttribute(configLocation, "dos:hidden", true);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to set hidden attribute of " + configLocation, e);
LOG.warning("Failed to set hidden attribute of " + configLocation, e);
}
}
}
@@ -161,7 +161,7 @@ public final class ConfigHolder {
ownerChanged = true;
}
} catch (IOException e1) {
LOG.log(Level.WARNING, "Failed to get owner");
LOG.warning("Failed to get owner");
}
try {
String content = FileUtils.readText(configLocation);
@@ -173,7 +173,7 @@ public final class ConfigHolder {
return deserialized;
}
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
LOG.warning("Malformed config.", e);
}
}
@@ -186,7 +186,7 @@ public final class ConfigHolder {
try {
writeToConfig(content);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
LOG.error("Failed to save config", e);
}
});
@@ -218,7 +218,7 @@ public final class ConfigHolder {
return deserialized;
}
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
LOG.warning("Malformed config.", e);
}
}
@@ -230,7 +230,7 @@ public final class ConfigHolder {
try {
writeToGlobalConfig(content);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
LOG.error("Failed to save config", e);
}
});
@@ -23,10 +23,9 @@ import org.jackhuang.hmcl.util.StringUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
final class ConfigUpgrader {
private ConfigUpgrader() {
@@ -48,12 +47,12 @@ final class ConfigUpgrader {
}
if (configVersion > CURRENT_VERSION) {
LOG.log(Level.WARNING, String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", CURRENT_VERSION, configVersion));
deserialized.setConfigVersion(CURRENT_VERSION);
return;
}
LOG.log(Level.INFO, String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, CURRENT_VERSION));
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
if (configVersion < 1) {
@@ -23,6 +23,8 @@ import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jetbrains.annotations.Nullable;
@@ -49,6 +51,12 @@ public final class GlobalConfig implements Cloneable, Observable {
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
private final IntegerProperty logRetention = new SimpleIntegerProperty();
private final ObservableSet<String> userJava = FXCollections.observableSet(new LinkedHashSet<>());
private final ObservableSet<String> disabledJava = FXCollections.observableSet(new LinkedHashSet<>());
private final Map<String, Object> unknownFields = new HashMap<>();
private final transient ObservableHelper helper = new ObservableHelper(this);
@@ -100,10 +108,33 @@ public final class GlobalConfig implements Cloneable, Observable {
this.platformPromptVersion.set(platformPromptVersion);
}
public int getLogRetention() {
return logRetention.get();
}
public IntegerProperty logRetentionProperty() {
return logRetention;
}
public void setLogRetention(int logRetention) {
this.logRetention.set(logRetention);
}
public ObservableSet<String> getUserJava() {
return userJava;
}
public ObservableSet<String> getDisabledJava() {
return disabledJava;
}
public static final class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"platformPromptVersion"
"platformPromptVersion",
"logRetention",
"userJava",
"disabledJava"
));
@Override
@@ -115,6 +146,13 @@ public final class GlobalConfig implements Cloneable, Observable {
JsonObject jsonObject = new JsonObject();
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("logRetention", context.serialize(src.getLogRetention()));
if (!src.getUserJava().isEmpty())
jsonObject.add("userJava", context.serialize(src.getUserJava()));
if (!src.getDisabledJava().isEmpty())
jsonObject.add("disabledJava", context.serialize(src.getDisabledJava()));
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
}
@@ -131,6 +169,21 @@ public final class GlobalConfig implements Cloneable, Observable {
GlobalConfig config = new GlobalConfig();
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
config.setLogRetention(Optional.ofNullable(obj.get("logRetention")).map(JsonElement::getAsInt).orElse(20));
JsonElement userJava = obj.get("userJava");
if (userJava != null && userJava.isJsonArray()) {
for (JsonElement element : userJava.getAsJsonArray()) {
config.userJava.add(element.getAsString());
}
}
JsonElement disabledJava = obj.get("disabledJava");
if (disabledJava != null && disabledJava.isJsonArray()) {
for (JsonElement element : disabledJava.getAsJsonArray()) {
config.disabledJava.add(element.getAsString());
}
}
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
if (!knownFields.contains(entry.getKey())) {
@@ -0,0 +1,25 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
/**
* @author Glavo
*/
public enum JavaVersionType {
DEFAULT, AUTO, VERSION, DETECTED, CUSTOM
}
@@ -29,7 +29,7 @@ import java.net.Proxy;
import java.net.Proxy.Type;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ProxyManager {
private ProxyManager() {
@@ -23,23 +23,27 @@ import com.google.gson.stream.JsonWriter;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@JsonAdapter(Theme.TypeAdapter.class)
public class Theme {
@@ -82,6 +86,30 @@ public class Theme {
return cssCharset;
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static Optional<Font> font;
private static Optional<Font> tryLoadFont() {
//noinspection OptionalAssignedToNull
if (font != null) {
return font;
}
Path path = Paths.get("font.ttf");
if (!Files.isRegularFile(path)) {
path = Paths.get("font.otf");
}
if (Files.isRegularFile(path)) {
try {
return font = Optional.ofNullable(Font.loadFont(path.toAbsolutePath().toUri().toURL().toExternalForm(), 0));
} catch (MalformedURLException ignored) {
}
}
return font = Optional.empty();
}
public static Theme getTheme() {
Theme theme = config().getTheme();
return theme == null ? BLUE : theme;
@@ -120,10 +148,28 @@ public class Theme {
public String[] getStylesheets(String overrideFontFamily) {
String css = "/assets/css/blue.css";
String fontFamily = System.getProperty("hmcl.font.override", overrideFontFamily);
String fontFamily = overrideFontFamily == null
? System.getProperty("hmcl.font.override", System.getenv("HMCL_FONT"))
: overrideFontFamily;
String fontStyle = null;
if (fontFamily == null) {
Optional<Font> font = tryLoadFont();
if (font.isPresent()) {
fontFamily = font.get().getFamily();
fontStyle = font.get().getStyle();
}
}
if (fontFamily != null || !this.color.equalsIgnoreCase(BLUE.color)) {
Color textFill = getForegroundColor();
String fontCss = "";
if (fontFamily != null) {
fontCss = "-fx-font-family: \"" + fontFamily + "\";";
if (fontStyle != null && !fontStyle.isEmpty())
fontCss += " -fx-font-style: \"" + fontStyle + "\";";
}
try {
File temp = File.createTempFile("hmcl", ".css");
String themeText = IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
@@ -134,12 +180,12 @@ public class Theme {
.replace("%base-rippler-color%", String.format("rgba(%d, %d, %d, 0.3)", (int) Math.ceil(paint.getRed() * 256), (int) Math.ceil(paint.getGreen() * 256), (int) Math.ceil(paint.getBlue() * 256)))
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int) Math.ceil(textFill.getRed() * 256), (int) Math.ceil(textFill.getGreen() * 256), (int) Math.ceil(textFill.getBlue() * 256)))
.replace("%font-color%", getColorDisplayName(getForegroundColor()))
.replace("%font%", Optional.ofNullable(fontFamily).map(f -> "-fx-font-family: \"" + f + "\";").orElse(""));
.replace("%font%", fontCss);
FileUtils.writeText(temp, themeText, getCssCharset());
temp.deleteOnExit();
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
LOG.error("Unable to create theme stylesheet. Fallback to blue theme.", e);
}
}
@@ -22,29 +22,23 @@ import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.InvalidPathException;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
*
* @author huangyuhui
*/
@JsonAdapter(VersionSetting.Serializer.class)
@@ -84,38 +78,43 @@ public final class VersionSetting implements Cloneable {
// java
private final StringProperty javaProperty = new SimpleStringProperty(this, "java", "");
private final ObjectProperty<JavaVersionType> javaVersionTypeProperty = new SimpleObjectProperty<>(this, "javaVersionType", JavaVersionType.AUTO);
public StringProperty javaProperty() {
return javaProperty;
public ObjectProperty<JavaVersionType> javaVersionTypeProperty() {
return javaVersionTypeProperty;
}
/**
* Java version or "Custom" if user customizes java directory, "Default" if the jvm that this app relies on.
*/
public String getJava() {
return javaProperty.get();
public JavaVersionType getJavaVersionType() {
return javaVersionTypeProperty.get();
}
public void setJava(String java) {
javaProperty.set(java);
public void setJavaVersionType(JavaVersionType javaVersionType) {
javaVersionTypeProperty.set(javaVersionType);
}
public boolean isUsesCustomJavaDir() {
return "Custom".equals(getJava());
private final StringProperty javaVersionProperty = new SimpleStringProperty(this, "javaVersion", "");
public StringProperty javaVersionProperty() {
return javaVersionProperty;
}
public String getJavaVersion() {
return javaVersionProperty.get();
}
public void setJavaVersion(String java) {
javaVersionProperty.set(java);
}
public void setUsesCustomJavaDir() {
setJava("Custom");
setJavaVersionType(JavaVersionType.CUSTOM);
setJavaVersion("");
setDefaultJavaPath(null);
}
public boolean isJavaAutoSelected() {
return "Auto".equals(getJava());
}
public void setJavaAutoSelected() {
setJava("Auto");
setJavaVersionType(JavaVersionType.AUTO);
setJavaVersion("");
setDefaultJavaPath(null);
}
@@ -644,58 +643,74 @@ public final class VersionSetting implements Cloneable {
launcherVisibilityProperty.set(launcherVisibility);
}
public Task<JavaVersion> getJavaVersion(GameVersionNumber gameVersion, Version version) {
return getJavaVersion(gameVersion, version, true);
}
public JavaRuntime getJava(GameVersionNumber gameVersion, Version version) throws InterruptedException {
switch (getJavaVersionType()) {
case DEFAULT:
return JavaRuntime.getDefault();
case AUTO:
return JavaManager.findSuitableJava(gameVersion, version);
case CUSTOM:
try {
return JavaManager.getJava(Paths.get(getJavaDir()));
} catch (IOException | InvalidPathException e) {
return null; // Custom Java not found
}
case VERSION: {
String javaVersion = getJavaVersion();
if (StringUtils.isBlank(javaVersion)) {
return JavaManager.findSuitableJava(gameVersion, version);
}
public Task<JavaVersion> getJavaVersion(GameVersionNumber gameVersion, Version version, boolean checkJava) {
return Task.runAsync(Schedulers.javafx(), () -> {
if (StringUtils.isBlank(getJava())) {
setJava(StringUtils.isBlank(getJavaDir()) ? "Auto" : "Custom");
}
}).thenSupplyAsync(() -> {
try {
if ("Default".equals(getJava())) {
return JavaVersion.fromCurrentEnvironment();
} else if (isJavaAutoSelected()) {
return JavaVersionConstraint.findSuitableJavaVersion(gameVersion, version);
} else if (isUsesCustomJavaDir()) {
try {
if (checkJava)
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
else
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.UNKNOWN));
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
}
} else if (StringUtils.isNotBlank(getJava())) {
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
FXUtils.runInFX(() -> setJava("Auto"));
return JavaVersion.fromCurrentEnvironment();
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
}
} else throw new Error();
} catch (InterruptedException e) {
throw new CancellationException();
}
});
}
int majorVersion = -1;
try {
majorVersion = Integer.parseInt(javaVersion);
} catch (NumberFormatException ignored) {
}
public void setJavaVersion(JavaVersion java) {
setJava(java.getVersion());
setDefaultJavaPath(java.getBinary().toString());
if (majorVersion < 0) {
LOG.warning("Invalid Java version: " + javaVersion);
return null;
}
final int finalMajorVersion = majorVersion;
Collection<JavaRuntime> allJava = JavaManager.getAllJava().stream()
.filter(it -> it.getParsedVersion() == finalMajorVersion)
.collect(Collectors.toList());
return JavaManager.findSuitableJava(allJava, gameVersion, version);
}
case DETECTED: {
String javaVersion = getJavaVersion();
if (StringUtils.isBlank(javaVersion)) {
return JavaManager.findSuitableJava(gameVersion, version);
}
try {
String defaultJavaPath = getDefaultJavaPath();
if (StringUtils.isNotBlank(defaultJavaPath)) {
JavaRuntime java = JavaManager.getJava(Paths.get(defaultJavaPath).toRealPath());
if (java != null && java.getVersion().equals(javaVersion)) {
return java;
}
}
} catch (IOException | InvalidPathException ignored) {
}
for (JavaRuntime java : JavaManager.getAllJava()) {
if (java.getVersion().equals(javaVersion)) {
return java;
}
}
return null;
}
default:
throw new AssertionError("JavaVersionType: " + getJavaVersionType());
}
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaProperty.addListener(listener);
javaVersionProperty.addListener(listener);
javaDirProperty.addListener(listener);
wrapperProperty.addListener(listener);
permSizeProperty.addListener(listener);
@@ -733,7 +748,8 @@ public final class VersionSetting implements Cloneable {
public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting();
versionSetting.setUsesGlobal(isUsesGlobal());
versionSetting.setJava(getJava());
versionSetting.setJavaVersionType(getJavaVersionType());
versionSetting.setJavaVersion(getJavaVersion());
versionSetting.setDefaultJavaPath(getDefaultJavaPath());
versionSetting.setJavaDir(getJavaDir());
versionSetting.setWrapper(getWrapper());
@@ -787,7 +803,6 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("precalledCommand", src.getPreLaunchCommand());
obj.addProperty("postExitCommand", src.getPostExitCommand());
obj.addProperty("serverIp", src.getServerIp());
obj.addProperty("java", src.getJava());
obj.addProperty("wrapper", src.getWrapper());
obj.addProperty("fullscreen", src.isFullscreen());
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
@@ -806,6 +821,24 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("nativesDirType", src.getNativesDirType().ordinal());
obj.addProperty("versionIcon", src.getVersionIcon().ordinal());
obj.addProperty("javaVersionType", src.getJavaVersionType().name());
String java;
switch (src.getJavaVersionType()) {
case DEFAULT:
java = "Default";
break;
case AUTO:
java = "Auto";
break;
case CUSTOM:
java = "Custom";
break;
default:
java = src.getJavaVersion();
break;
}
obj.addProperty("java", java);
obj.addProperty("renderer", src.getRenderer().name());
if (src.getRenderer() == Renderer.LLVMPIPE)
obj.addProperty("useSoftwareRenderer", true);
@@ -846,7 +879,6 @@ public final class VersionSetting implements Cloneable {
vs.setPreLaunchCommand(Optional.ofNullable(obj.get("precalledCommand")).map(JsonElement::getAsString).orElse(""));
vs.setPostExitCommand(Optional.ofNullable(obj.get("postExitCommand")).map(JsonElement::getAsString).orElse(""));
vs.setServerIp(Optional.ofNullable(obj.get("serverIp")).map(JsonElement::getAsString).orElse(""));
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""));
vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse(""));
vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""));
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
@@ -865,6 +897,27 @@ public final class VersionSetting implements Cloneable {
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
if (obj.get("javaVersionType") != null) {
JavaVersionType javaVersionType = parseJsonPrimitive(obj.getAsJsonPrimitive("javaVersionType"), JavaVersionType.class, JavaVersionType.AUTO);
vs.setJavaVersionType(javaVersionType);
vs.setJavaVersion(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(null));
} else {
String java = Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse("");
switch (java) {
case "Default":
vs.setJavaVersionType(JavaVersionType.DEFAULT);
break;
case "Auto":
vs.setJavaVersionType(JavaVersionType.AUTO);
break;
case "Custom":
vs.setJavaVersionType(JavaVersionType.CUSTOM);
break;
default:
vs.setJavaVersion(java);
}
}
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
.flatMap(name -> {
try {
@@ -892,5 +945,25 @@ public final class VersionSetting implements Cloneable {
else
return Lang.parseInt(primitive.getAsString(), defaultValue);
}
private <E extends Enum<E>> E parseJsonPrimitive(JsonPrimitive primitive, Class<E> clazz, E defaultValue) {
if (primitive == null)
return defaultValue;
else {
E[] enumConstants = clazz.getEnumConstants();
if (primitive.isNumber()) {
int index = primitive.getAsInt();
return index >= 0 && index < enumConstants.length ? enumConstants[index] : defaultValue;
} else {
String name = primitive.getAsString();
for (E enumConstant : enumConstants) {
if (enumConstant.name().equalsIgnoreCase(name)) {
return enumConstant;
}
}
return defaultValue;
}
}
}
}
}
@@ -37,6 +37,7 @@ import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -53,7 +54,6 @@ import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
@@ -61,6 +61,7 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.setting.ConfigHolder.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Controllers {
@@ -165,7 +166,7 @@ public final class Controllers {
}
public static void initialize(Stage stage) {
Logging.LOG.info("Start initializing application");
LOG.info("Start initializing application");
Controllers.stage = stage;
@@ -251,7 +252,7 @@ public final class Controllers {
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.runAsync(JavaVersion::initialize).start();
Lang.thread(JavaManager::initialize, "Search Java", true);
scene = new Scene(decorator.getDecorator());
scene.setFill(Color.TRANSPARENT);
@@ -298,7 +299,7 @@ public final class Controllers {
});
JFXButton noButton = new JFXButton(i18n("launcher.agreement.decline"));
noButton.getStyleClass().add("dialog-cancel");
noButton.setOnAction(e -> System.exit(1));
noButton.setOnAction(e -> javafx.application.Platform.exit());
agreementPane.setActions(agreementLink, yesButton, noButton);
Controllers.dialog(agreementPane);
}
@@ -72,7 +72,7 @@ public class CrashWindow extends Stage {
FXUtils.setIcon(this);
setTitle(i18n("message.error"));
setOnCloseRequest(e -> System.exit(1));
setOnCloseRequest(e -> javafx.application.Platform.exit());
}
}
@@ -31,6 +31,7 @@ import javafx.beans.value.WritableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.*;
import javafx.scene.image.Image;
@@ -40,6 +41,7 @@ import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
@@ -50,13 +52,12 @@ import javafx.util.StringConverter;
import org.glavo.png.PNGType;
import org.glavo.png.PNGWriter;
import org.glavo.png.javafx.PNGJavaFXUtils;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
@@ -68,12 +69,9 @@ import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.io.*;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
@@ -82,18 +80,18 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class FXUtils {
@@ -110,7 +108,7 @@ public final class FXUtils {
try {
Files.deleteIfExists(entry.getValue());
} catch (IOException e) {
LOG.log(Level.WARNING, String.format("Failed to delete cache file %s.", entry.getValue()), e);
LOG.warning(String.format("Failed to delete cache file %s.", entry.getValue()), e);
}
remoteImageCache.remove(entry.getKey());
}
@@ -337,7 +335,7 @@ public final class FXUtils {
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
} catch (ReflectiveOperationException e2) {
e.addSuppressed(e2);
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
LOG.error("Cannot install tooltip", e);
}
Tooltip.install(node, tooltip);
}
@@ -370,7 +368,7 @@ public final class FXUtils {
public static void openFolder(File file) {
if (!FileUtils.makeDirectory(file)) {
LOG.log(Level.SEVERE, "Unable to make directory " + file);
LOG.error("Unable to make directory " + file);
return;
}
@@ -397,7 +395,7 @@ public final class FXUtils {
else
LOG.warning("Open " + path + " failed with code " + exitCode);
} catch (Throwable e) {
LOG.log(Level.WARNING, "Unable to open " + path + " by executing " + openCommand, e);
LOG.warning("Unable to open " + path + " by executing " + openCommand, e);
}
}
@@ -405,11 +403,27 @@ public final class FXUtils {
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
LOG.error("Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
});
}
private static String which(String command) {
String path = System.getenv("PATH");
if (path == null)
return null;
for (String item : path.split(OperatingSystem.PATH_SEPARATOR)) {
try {
Path program = Paths.get(item, command);
if (Files.isExecutable(program))
return program.toRealPath().toString();
} catch (Throwable ignored) {
}
}
return null;
}
public static void showFileInExplorer(Path file) {
String path = file.toAbsolutePath().toString();
@@ -418,6 +432,16 @@ public final class FXUtils {
openCommands = new String[]{"explorer.exe", "/select,", path};
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
openCommands = new String[]{"/usr/bin/open", "-R", path};
else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && which("dbus-send") != null)
openCommands = new String[]{
"dbus-send",
"--print-reply",
"--dest=org.freedesktop.FileManager1",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
"array:string:" + file.toAbsolutePath().toUri(),
"string:"
};
else
openCommands = null;
@@ -432,7 +456,7 @@ public final class FXUtils {
else
LOG.warning("Show " + path + " in explorer failed with code " + exitCode);
} catch (Throwable e) {
LOG.log(Level.WARNING, "Unable to show " + path + " in explorer", e);
LOG.warning("Unable to show " + path + " in explorer", e);
}
// Fallback to open folder
@@ -469,19 +493,20 @@ public final class FXUtils {
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
return;
} catch (Throwable e) {
LOG.log(Level.WARNING, "An exception occurred while calling rundll32", e);
LOG.warning("An exception occurred while calling rundll32", e);
}
}
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
for (String browser : linuxBrowsers) {
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
if (is.read() != -1) {
String path = which(browser);
if (path != null) {
try {
Runtime.getRuntime().exec(new String[]{browser, link});
return;
} catch (Throwable ignored) {
}
} catch (Throwable ignored) {
}
Logging.LOG.log(Level.WARNING, "No known browser found");
LOG.warning("No known browser found");
}
}
try {
@@ -491,59 +516,13 @@ public final class FXUtils {
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
LOG.warning("Unable to open link: " + link, ex);
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
LOG.warning("Failed to open link: " + link, e);
}
});
}
public static void showWebDialog(String title, String content) {
showWebDialog(title, content, 800, 480);
}
public static void showWebDialog(String title, String content, int width, int height) {
try {
WebStage stage = new WebStage(width, height);
stage.getWebView().getEngine().loadContent(content);
stage.setTitle(title);
stage.showAndWait();
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.log(Level.WARNING, "WebView is missing or initialization failed, use JEditorPane replaced", e);
SwingUtils.initLookAndFeel();
SwingUtilities.invokeLater(() -> {
final JFrame frame = new JFrame(title);
frame.setSize(width, height);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setLocationByPlatform(true);
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
frame.setLayout(new BorderLayout());
final JProgressBar progressBar = new JProgressBar();
progressBar.setIndeterminate(true);
frame.add(progressBar, BorderLayout.PAGE_START);
Schedulers.defaultScheduler().execute(() -> {
final JEditorPane pane = new JEditorPane("text/html", content);
pane.setEditable(false);
pane.addHyperlinkListener(event -> {
if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
openLink(event.getURL().toExternalForm());
}
});
SwingUtilities.invokeLater(() -> {
progressBar.setVisible(false);
frame.add(new JScrollPane(pane), BorderLayout.CENTER);
});
});
frame.setVisible(true);
frame.toFront();
});
}
}
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
textField.setText(converter == null ? (String) property.getValue() : converter.toString(property.getValue()));
TextFieldBindingListener<T> listener = new TextFieldBindingListener<>(textField, property, converter);
@@ -761,7 +740,7 @@ public final class FXUtils {
try (InputStream inputStream = Files.newInputStream(currentPath)) {
return new Image(inputStream, requestedWidth, requestedHeight, preserveRatio, smooth);
} catch (IOException e) {
LOG.log(Level.WARNING, "An exception encountered while reading data from cached image file.", e);
LOG.warning("An exception encountered while reading data from cached image file.", e);
}
}
@@ -771,7 +750,7 @@ public final class FXUtils {
try {
Files.deleteIfExists(currentPath);
} catch (IOException e) {
LOG.log(Level.WARNING, "An exception encountered while deleting broken cached image file.", e);
LOG.warning("An exception encountered while deleting broken cached image file.", e);
}
}
@@ -817,6 +796,17 @@ public final class FXUtils {
return button;
}
public static Label truncatedLabel(String text, int limit) {
Label label = new Label();
if (text.length() <= limit) {
label.setText(text);
} else {
label.setText(StringUtils.truncate(text, limit));
installFastTooltip(label, text);
}
return label;
}
public static void applyDragListener(Node node, FileFilter filter, Consumer<List<File>> callback) {
applyDragListener(node, filter, callback, null);
}
@@ -968,7 +958,7 @@ public final class FXUtils {
}
return texts;
} catch (SAXException | ParserConfigurationException | IOException e) {
LOG.log(Level.WARNING, "Failed to parse xml", e);
LOG.warning("Failed to parse xml", e);
return Collections.singletonList(new Text(segment));
}
}
@@ -979,4 +969,11 @@ public final class FXUtils {
return tf;
}
public static String toWeb(Color color) {
int r = (int) Math.round(color.getRed() * 255.0);
int g = (int) Math.round(color.getGreen() * 255.0);
int b = (int) Math.round(color.getBlue() * 255.0);
return String.format("#%02x%02x%02x", r, g, b);
}
}
@@ -20,8 +20,6 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@@ -43,8 +41,9 @@ import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -61,14 +60,13 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -78,8 +76,6 @@ public class GameCrashWindow extends Stage {
private final String total_memory;
private final String java;
private final LibraryAnalyzer analyzer;
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
private final TextFlow reasonTextFlow = new TextFlow(new Text(i18n("game.crash.reason.unknown")));
private final BooleanProperty loading = new SimpleBooleanProperty();
private final TextFlow feedbackTextFlow = new TextFlow();
@@ -90,9 +86,9 @@ public class GameCrashWindow extends Stage {
private final LaunchOptions launchOptions;
private final View view;
private final Collection<Pair<String, Log4jLevel>> logs;
private final List<Log> logs;
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, Collection<Pair<String, Log4jLevel>> logs) {
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Log> logs) {
this.managedProcess = managedProcess;
this.exitType = exitType;
this.repository = repository;
@@ -125,14 +121,14 @@ public class GameCrashWindow extends Stage {
private void analyzeCrashReport() {
loading.set(true);
Task.allOf(Task.supplyAsync(() -> {
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
String rawLog = logs.stream().map(Log::getLog).collect(Collectors.joining("\n"));
// Get the crash-report from the crash-reports/xxx, or the output of console.
String crashReport = null;
try {
crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to read crash report", e);
LOG.warning("Failed to read crash report", e);
}
if (crashReport == null) {
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
@@ -149,7 +145,7 @@ public class GameCrashWindow extends Stage {
try {
log = FileUtils.readText(latestLog);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to read logs/latest.log", e);
LOG.warning("Failed to read logs/latest.log", e);
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
}
@@ -158,7 +154,7 @@ public class GameCrashWindow extends Stage {
loading.set(false);
if (exception != null) {
LOG.log(Level.WARNING, "Failed to analyze crash report", exception);
LOG.warning("Failed to analyze crash report", exception);
reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction));
} else {
EnumMap<CrashReportAnalyzer.Rule, CrashReportAnalyzer.Result> results = new EnumMap<>(CrashReportAnalyzer.Rule.class);
@@ -172,7 +168,7 @@ public class GameCrashWindow extends Stage {
List<Node> segments = new ArrayList<>(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
LOG.log(Level.INFO, "Number of reasons: " + results.size());
LOG.info("Number of reasons: " + results.size());
if (results.size() > 1) {
segments.add(new Text("\n"));
segments.addAll(FXUtils.parseSegment(i18n("game.crash.reason.multiple"), Controllers::onHyperlinkAction));
@@ -203,8 +199,9 @@ public class GameCrashWindow extends Stage {
case TWILIGHT_FOREST_OPTIFINE:
case PERFORMANT_FOREST_OPTIFINE:
case JADE_FOREST_OPTIFINE:
case NEOFORGE_FOREST_OPTIFINE:
message = i18n("game.crash.reason.mod", "OptiFine");
LOG.log(Level.INFO, "Crash cause: " + result.getRule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
LOG.info("Crash cause: " + result.getRule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
break;
default:
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
@@ -212,17 +209,17 @@ public class GameCrashWindow extends Stage {
.toArray());
break;
}
LOG.log(Level.INFO, "Crash cause: " + result.getRule() + ": " + message);
LOG.info("Crash cause: " + result.getRule() + ": " + message);
segments.addAll(FXUtils.parseSegment(message, Controllers::onHyperlinkAction));
segments.add(new Text("\n\n"));
}
if (results.isEmpty()) {
if (!keywords.isEmpty()) {
reasonTextFlow.getChildren().setAll(new Text(i18n("game.crash.reason.stacktrace", String.join(", ", keywords))));
LOG.log(Level.INFO, "Crash reason unknown, but some log keywords have been found: " + String.join(", ", keywords));
LOG.info("Crash reason unknown, but some log keywords have been found: " + String.join(", ", keywords));
} else {
reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction));
LOG.log(Level.INFO, "Crash reason unknown");
LOG.info("Crash reason unknown");
}
} else {
feedbackTextFlow.setVisible(false);
@@ -264,20 +261,18 @@ public class GameCrashWindow extends Stage {
private void showLogWindow() {
LogWindow logWindow = new LogWindow(managedProcess);
logWindow.logLine(Logging.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
logWindow.logLine(new Log(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO));
if (managedProcess.getClasspath() != null)
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
logWindow.showNormal();
logWindow.logLine(new Log("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO));
logWindow.logLines(logs);
logWindow.show();
}
private void 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(() ->
logs.stream().map(Pair::getKey).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
logs.stream().map(Log::getLog).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)))
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.handleAsync((result, exception) -> {
@@ -287,7 +282,7 @@ public class GameCrashWindow extends Stage {
FXUtils.showFileInExplorer(logFile);
alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
} else {
LOG.log(Level.WARNING, "Failed to export game crash info", exception);
LOG.warning("Failed to export game crash info", exception);
alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception));
}
@@ -337,7 +332,7 @@ public class GameCrashWindow extends Stage {
TwoLineListItem version = new TwoLineListItem();
version.getStyleClass().setAll("two-line-item-second-large");
version.setTitle(i18n("archive.game_version"));
version.setTitle(i18n("game.version"));
version.setSubtitle(GameCrashWindow.this.version.getId());
TwoLineListItem total_memory = new TwoLineListItem();
@@ -358,12 +353,12 @@ public class GameCrashWindow extends Stage {
TwoLineListItem os = new TwoLineListItem();
os.getStyleClass().setAll("two-line-item-second-large");
os.setTitle(i18n("system.operating_system"));
os.subtitleProperty().bind(GameCrashWindow.this.os);
os.setSubtitle(Lang.requireNonNullElse(OperatingSystem.OS_RELEASE_NAME, OperatingSystem.SYSTEM_NAME));
TwoLineListItem arch = new TwoLineListItem();
arch.getStyleClass().setAll("two-line-item-second-large");
arch.setTitle(i18n("system.architecture"));
arch.subtitleProperty().bind(GameCrashWindow.this.arch);
arch.setSubtitle(Architecture.SYSTEM_ARCH.getDisplayName());
infoPane.getChildren().setAll(launcher, version, total_memory, memory, java, os, arch);
}
@@ -0,0 +1,266 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Cursor;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class HTMLRenderer {
private static URI resolveLink(Node linkNode) {
String href = linkNode.absUrl("href");
if (href.isEmpty())
return null;
try {
return new URI(href);
} catch (Throwable e) {
return null;
}
}
private final List<javafx.scene.Node> children = new ArrayList<>();
private final List<Node> stack = new ArrayList<>();
private boolean bold;
private boolean italic;
private boolean underline;
private boolean strike;
private boolean highlight;
private String headerLevel;
private Node hyperlink;
private final Consumer<URI> onClickHyperlink;
public HTMLRenderer(Consumer<URI> onClickHyperlink) {
this.onClickHyperlink = onClickHyperlink;
}
private void updateStyle() {
bold = false;
italic = false;
underline = false;
strike = false;
highlight = false;
headerLevel = null;
hyperlink = null;
for (Node node : stack) {
String nodeName = node.nodeName();
switch (nodeName) {
case "b":
case "strong":
bold = true;
break;
case "i":
case "em":
italic = true;
break;
case "ins":
underline = true;
break;
case "del":
strike = true;
break;
case "mark":
highlight = true;
break;
case "a":
hyperlink = node;
break;
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
headerLevel = nodeName;
break;
}
}
}
private void pushNode(Node node) {
stack.add(node);
updateStyle();
}
private void popNode() {
stack.remove(stack.size() - 1);
updateStyle();
}
private void applyStyle(Text text) {
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
if (target != null) {
text.setOnMouseClicked(event -> onClickHyperlink.accept(target));
text.setCursor(Cursor.HAND);
}
text.getStyleClass().add("html-hyperlink");
}
if (hyperlink != null || underline)
text.setUnderline(true);
if (strike)
text.setStrikethrough(true);
if (bold || highlight)
text.getStyleClass().add("html-bold");
if (italic)
text.getStyleClass().add("html-italic");
if (headerLevel != null)
text.getStyleClass().add("html-" + headerLevel);
}
private void appendText(String text) {
Text textNode = new Text(text);
applyStyle(textNode);
children.add(textNode);
}
private void appendImage(Node node) {
String src = node.absUrl("src");
URI imageUri = null;
try {
if (!src.isEmpty())
imageUri = URI.create(src);
} catch (Exception ignored) {
}
String alt = node.attr("alt");
if (imageUri != null) {
URI uri = URI.create(src);
String widthAttr = node.attr("width");
String heightAttr = node.attr("height");
double width = 0;
double height = 0;
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
try {
width = Double.parseDouble(widthAttr);
height = Double.parseDouble(heightAttr);
} catch (NumberFormatException ignored) {
}
if (width <= 0 || height <= 0) {
width = 0;
height = 0;
}
}
Image image = FXUtils.newRemoteImage(uri.toString(), width, height, true, true, false);
if (image.isError()) {
LOG.warning("Failed to load image: " + uri, image.getException());
} else {
ImageView imageView = new ImageView(image);
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
if (target != null) {
imageView.setOnMouseClicked(event -> onClickHyperlink.accept(target));
imageView.setCursor(Cursor.HAND);
}
}
children.add(imageView);
return;
}
}
if (!alt.isEmpty())
appendText(alt);
}
public void appendNode(Node node) {
if (node instanceof TextNode) {
appendText(((TextNode) node).text());
}
String name = node.nodeName();
switch (name) {
case "img":
appendImage(node);
break;
case "li":
appendText("\n \u2022 ");
break;
case "dt":
appendText(" ");
break;
case "p":
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
case "tr":
if (!children.isEmpty())
appendText("\n\n");
break;
}
if (node.childNodeSize() > 0) {
pushNode(node);
for (Node childNode : node.childNodes()) {
appendNode(childNode);
}
popNode();
}
switch (name) {
case "br":
case "dd":
case "p":
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
appendText("\n");
break;
}
}
public TextFlow render() {
TextFlow textFlow = new TextFlow();
textFlow.getStyleClass().add("html");
textFlow.getChildren().setAll(children);
return textFlow;
}
}
@@ -18,9 +18,10 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
@@ -55,29 +56,77 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class InstallerItem extends Control {
private final String id;
private final VersionIconType iconType;
public final StringProperty libraryVersion = new SimpleStringProperty();
public final StringProperty incompatibleLibraryName = new SimpleStringProperty();
public final StringProperty dependencyName = new SimpleStringProperty();
public final BooleanProperty incompatibleWithGame = new SimpleBooleanProperty();
public final BooleanProperty removable = new SimpleBooleanProperty();
public final BooleanProperty upgradable = new SimpleBooleanProperty(false);
public final BooleanProperty installable = new SimpleBooleanProperty(true);
public final ObjectProperty<EventHandler<? super MouseEvent>> removeAction = new SimpleObjectProperty<>();
public final ObjectProperty<EventHandler<? super MouseEvent>> action = new SimpleObjectProperty<>();
private final Style style;
private final ObjectProperty<InstalledState> versionProperty = new SimpleObjectProperty<>(this, "version", null);
private final ObjectProperty<State> resolvedStateProperty = new SimpleObjectProperty<>(this, "resolvedState", InstallableState.INSTANCE);
private Style style = Style.LIST_ITEM;
private final ObjectProperty<EventHandler<? super MouseEvent>> installActionProperty = new SimpleObjectProperty<>(this, "installAction");
private final ObjectProperty<EventHandler<? super MouseEvent>> removeActionProperty = new SimpleObjectProperty<>(this, "removeAction");
public interface State {
}
public static final class InstallableState implements State {
public static final InstallableState INSTANCE = new InstallableState();
private InstallableState() {
}
}
public static final class IncompatibleState implements State {
private final String incompatibleItemName;
private final String incompatibleItemVersion;
public IncompatibleState(String incompatibleItemName, String incompatibleItemVersion) {
this.incompatibleItemName = incompatibleItemName;
this.incompatibleItemVersion = incompatibleItemVersion;
}
public String getIncompatibleItemName() {
return incompatibleItemName;
}
public String getIncompatibleItemVersion() {
return incompatibleItemVersion;
}
}
public static final class InstalledState implements State {
private final String version;
private final boolean external;
private final boolean incompatibleWithGame;
public InstalledState(String version, boolean external, boolean incompatibleWithGame) {
this.version = version;
this.external = external;
this.incompatibleWithGame = incompatibleWithGame;
}
public String getVersion() {
return version;
}
public boolean isExternal() {
return external;
}
public boolean isIncompatibleWithGame() {
return incompatibleWithGame;
}
}
public enum Style {
LIST_ITEM,
CARD,
}
public InstallerItem(LibraryAnalyzer.LibraryType id) {
this(id.getPatchId());
public InstallerItem(LibraryAnalyzer.LibraryType id, Style style) {
this(id.getPatchId(), style);
}
public InstallerItem(String id) {
public InstallerItem(String id, Style style) {
this.id = id;
this.style = style;
switch (id) {
case "game":
@@ -109,55 +158,51 @@ public class InstallerItem extends Control {
}
}
public void setStyleMode(Style style) {
this.style = style;
}
public void setState(String libraryVersion, boolean incompatibleWithGame, boolean removable) {
this.libraryVersion.set(libraryVersion);
this.incompatibleWithGame.set(incompatibleWithGame);
this.removable.set(removable);
}
public String getLibraryId() {
return id;
}
public ObjectProperty<InstalledState> versionProperty() {
return versionProperty;
}
public ObjectProperty<State> resolvedStateProperty() {
return resolvedStateProperty;
}
public ObjectProperty<EventHandler<? super MouseEvent>> installActionProperty() {
return installActionProperty;
}
public ObjectProperty<EventHandler<? super MouseEvent>> removeActionProperty() {
return removeActionProperty;
}
@Override
protected Skin<?> createDefaultSkin() {
return new InstallerItemSkin(this);
}
public final static class InstallerItemGroup {
public final InstallerItem game = new InstallerItem(MINECRAFT);
public final InstallerItem fabric = new InstallerItem(FABRIC);
public final InstallerItem fabricApi = new InstallerItem(FABRIC_API);
public final InstallerItem forge = new InstallerItem(FORGE);
public final InstallerItem neoForge = new InstallerItem(NEO_FORGE);
public final InstallerItem liteLoader = new InstallerItem(LITELOADER);
public final InstallerItem optiFine = new InstallerItem(OPTIFINE);
public final InstallerItem quilt = new InstallerItem(QUILT);
public final InstallerItem quiltApi = new InstallerItem(QUILT_API);
private final InstallerItem game;
private final InstallerItem[] libraries;
private final HashMap<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
private Set<InstallerItem> getIncompatibles(InstallerItem item) {
private Set<InstallerItem> getIncompatibles(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem item) {
return incompatibleMap.computeIfAbsent(item, it -> new HashSet<>());
}
private void addIncompatibles(InstallerItem item, InstallerItem... others) {
Set<InstallerItem> set = getIncompatibles(item);
private void addIncompatibles(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem item, InstallerItem... others) {
Set<InstallerItem> set = getIncompatibles(incompatibleMap, item);
for (InstallerItem other : others) {
set.add(other);
getIncompatibles(other).add(item);
getIncompatibles(incompatibleMap, other).add(item);
}
}
private void mutualIncompatible(InstallerItem... items) {
private void mutualIncompatible(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem... items) {
for (InstallerItem item : items) {
Set<InstallerItem> set = getIncompatibles(item);
Set<InstallerItem> set = getIncompatibles(incompatibleMap, item);
for (InstallerItem item2 : items) {
if (item2 != item) {
@@ -167,43 +212,66 @@ public class InstallerItem extends Control {
}
}
public InstallerItemGroup(String gameVersion) {
mutualIncompatible(forge, fabric, quilt, neoForge, liteLoader);
addIncompatibles(optiFine, fabric, quilt, neoForge);
addIncompatibles(fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine);
addIncompatibles(quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine);
public InstallerItemGroup(String gameVersion, Style style) {
game = new InstallerItem(MINECRAFT, style);
InstallerItem fabric = new InstallerItem(FABRIC, style);
InstallerItem fabricApi = new InstallerItem(FABRIC_API, style);
InstallerItem forge = new InstallerItem(FORGE, style);
InstallerItem neoForge = new InstallerItem(NEO_FORGE, style);
InstallerItem liteLoader = new InstallerItem(LITELOADER, style);
InstallerItem optiFine = new InstallerItem(OPTIFINE, style);
InstallerItem quilt = new InstallerItem(QUILT, style);
InstallerItem quiltApi = new InstallerItem(QUILT_API, style);
InvalidationListener listener = o -> {
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
InstallerItem item = entry.getKey();
Map<InstallerItem, Set<InstallerItem>> incompatibleMap = new HashMap<>();
mutualIncompatible(incompatibleMap, forge, fabric, quilt, neoForge);
addIncompatibles(incompatibleMap, liteLoader, fabric, quilt, neoForge);
addIncompatibles(incompatibleMap, optiFine, fabric, quilt, neoForge);
addIncompatibles(incompatibleMap, fabricApi, forge, quiltApi, neoForge, liteLoader, optiFine);
addIncompatibles(incompatibleMap, quiltApi, forge, fabric, fabricApi, neoForge, liteLoader, optiFine);
String incompatibleId = null;
for (InstallerItem other : entry.getValue()) {
if (other.libraryVersion.get() != null) {
incompatibleId = other.id;
break;
for (Map.Entry<InstallerItem, Set<InstallerItem>> entry : incompatibleMap.entrySet()) {
InstallerItem item = entry.getKey();
Set<InstallerItem> incompatibleItems = entry.getValue();
Observable[] bindings = new Observable[incompatibleItems.size() + 1];
bindings[0] = item.versionProperty;
int i = 1;
for (InstallerItem other : incompatibleItems) {
bindings[i++] = other.versionProperty;
}
item.resolvedStateProperty.bind(Bindings.createObjectBinding(() -> {
InstalledState itemVersion = item.versionProperty.get();
if (itemVersion != null) {
return itemVersion;
}
for (InstallerItem other : incompatibleItems) {
InstalledState otherVersion = other.versionProperty.get();
if (otherVersion != null) {
return new IncompatibleState(other.id, otherVersion.version);
}
}
item.incompatibleLibraryName.set(incompatibleId);
}
};
for (InstallerItem item : incompatibleMap.keySet()) {
item.libraryVersion.addListener(listener);
return InstallableState.INSTANCE;
}, bindings));
}
fabricApi.dependencyName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() == null) return FABRIC.getPatchId();
else return null;
}, fabric.libraryVersion));
if (gameVersion != null) {
game.versionProperty.set(new InstalledState(gameVersion, false, false));
}
quiltApi.dependencyName.bind(Bindings.createStringBinding(() -> {
if (quilt.libraryVersion.get() == null) return QUILT.getPatchId();
else return null;
}, quilt.libraryVersion));
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
for (InstallerItem item : all) {
if (!item.resolvedStateProperty.isBound()) {
item.resolvedStateProperty.bind(item.versionProperty);
}
}
if (gameVersion == null) {
this.libraries = new InstallerItem[]{game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
this.libraries = all;
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
} else {
@@ -211,15 +279,19 @@ public class InstallerItem extends Control {
}
}
public InstallerItem getGame() {
return game;
}
public InstallerItem[] getLibraries() {
return libraries;
}
}
public static class InstallerItemSkin extends SkinBase<InstallerItem> {
private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
private static final WeakListenerHolder holder = new WeakListenerHolder();
InstallerItemSkin(InstallerItem control) {
super(control);
@@ -227,6 +299,7 @@ public class InstallerItem extends Control {
Pane pane;
if (control.style == Style.CARD) {
pane = new VBox();
holder.add(FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7)));
} else {
pane = new HBox();
}
@@ -262,18 +335,25 @@ public class InstallerItem extends Control {
pane.getChildren().add(statusLabel);
HBox.setHgrow(statusLabel, Priority.ALWAYS);
statusLabel.textProperty().bind(Bindings.createStringBinding(() -> {
String incompatibleWith = control.incompatibleLibraryName.get();
String version = control.libraryVersion.get();
if (control.incompatibleWithGame.get()) {
return i18n("install.installer.change_version", version);
} else if (incompatibleWith != null) {
return i18n("install.installer.incompatible", i18n("install.installer." + incompatibleWith));
} else if (version == null) {
State state = control.resolvedStateProperty.get();
if (state instanceof InstalledState) {
InstalledState s = (InstalledState) state;
if (s.incompatibleWithGame) {
return i18n("install.installer.change_version", s.version);
}
if (s.external) {
return i18n("install.installer.external_version", s.version);
}
return i18n("install.installer.version", s.version);
} else if (state instanceof InstallableState) {
return i18n("install.installer.not_installed");
} else if (state instanceof IncompatibleState) {
return i18n("install.installer.incompatible", i18n("install.installer." + ((IncompatibleState) state).incompatibleItemName));
} else {
return i18n("install.installer.version", version);
throw new AssertionError("Unknown state type: " + state.getClass());
}
}, control.incompatibleLibraryName, control.incompatibleWithGame, control.libraryVersion));
}, control.resolvedStateProperty));
BorderPane.setMargin(statusLabel, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
@@ -282,30 +362,48 @@ public class InstallerItem extends Control {
buttonsContainer.setAlignment(Pos.CENTER);
pane.getChildren().add(buttonsContainer);
JFXButton closeButton = new JFXButton();
closeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1, -1));
closeButton.getStyleClass().add("toggle-icon4");
closeButton.visibleProperty().bind(control.removable);
closeButton.managedProperty().bind(closeButton.visibleProperty());
closeButton.onMouseClickedProperty().bind(control.removeAction);
buttonsContainer.getChildren().add(closeButton);
JFXButton removeButton = new JFXButton();
removeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1, -1));
removeButton.getStyleClass().add("toggle-icon4");
if (control.id.equals(MINECRAFT.getPatchId())) {
removeButton.setVisible(false);
} else {
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> control.resolvedStateProperty.get() instanceof InstalledState, control.resolvedStateProperty));
}
removeButton.managedProperty().bind(removeButton.visibleProperty());
removeButton.onMouseClickedProperty().bind(control.removeActionProperty);
buttonsContainer.getChildren().add(removeButton);
JFXButton arrowButton = new JFXButton();
arrowButton.graphicProperty().bind(Bindings.createObjectBinding(() -> control.upgradable.get()
? SVG.UPDATE.createIcon(Theme.blackFill(), -1, -1)
: SVG.ARROW_RIGHT.createIcon(Theme.blackFill(), -1, -1),
control.upgradable));
arrowButton.getStyleClass().add("toggle-icon4");
arrowButton.visibleProperty().bind(Bindings.createBooleanBinding(
() -> control.installable.get() && control.incompatibleLibraryName.get() == null,
control.installable, control.incompatibleLibraryName));
arrowButton.managedProperty().bind(arrowButton.visibleProperty());
arrowButton.onMouseClickedProperty().bind(control.action);
buttonsContainer.getChildren().add(arrowButton);
JFXButton installButton = new JFXButton();
installButton.graphicProperty().bind(Bindings.createObjectBinding(() ->
control.resolvedStateProperty.get() instanceof InstallableState ?
SVG.ARROW_RIGHT.createIcon(Theme.blackFill(), -1, -1) :
SVG.UPDATE.createIcon(Theme.blackFill(), -1, -1),
control.resolvedStateProperty
));
installButton.getStyleClass().add("toggle-icon4");
installButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
if (control.installActionProperty.get() == null) {
return false;
}
FXUtils.onChangeAndOperate(arrowButton.visibleProperty(), clickable -> {
State state = control.resolvedStateProperty.get();
if (state instanceof InstallableState) {
return true;
}
if (state instanceof InstalledState) {
return !((InstalledState) state).external;
}
return false;
}, control.resolvedStateProperty, control.installActionProperty));
installButton.managedProperty().bind(installButton.visibleProperty());
installButton.onMouseClickedProperty().bind(control.installActionProperty);
buttonsContainer.getChildren().add(installButton);
FXUtils.onChangeAndOperate(installButton.visibleProperty(), clickable -> {
if (clickable) {
container.onMouseClickedProperty().bind(control.action);
container.onMouseClickedProperty().bind(control.installActionProperty);
pane.setCursor(Cursor.HAND);
} else {
container.onMouseClickedProperty().unbind();
@@ -25,8 +25,8 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -38,13 +38,14 @@ import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.IOException;
@@ -54,15 +55,11 @@ import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -70,128 +67,120 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
*/
public final class LogWindow extends Stage {
private final ArrayDeque<Log> logs = new ArrayDeque<>();
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<Log4jLevel, SimpleIntegerProperty>(Log4jLevel.class) {
{
for (Log4jLevel level : Log4jLevel.values()) put(level, new SimpleIntegerProperty());
private static final Log4jLevel[] LEVELS = {Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
private final CircularArrayList<Log> logs;
private final Map<Log4jLevel, SimpleIntegerProperty> levelCountMap = new EnumMap<>(Log4jLevel.class);
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<>(Log4jLevel.class);
{
for (Log4jLevel level : Log4jLevel.values()) {
levelCountMap.put(level, new SimpleIntegerProperty());
levelShownMap.put(level, new SimpleBooleanProperty(true));
}
};
private final Map<Log4jLevel, SimpleBooleanProperty> levelShownMap = new EnumMap<Log4jLevel, SimpleBooleanProperty>(Log4jLevel.class) {
{
for (Log4jLevel level : Log4jLevel.values()) {
SimpleBooleanProperty property = new SimpleBooleanProperty(true);
put(level, property);
}
}
};
private final LogWindowImpl impl = new LogWindowImpl();
private final ChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
private Consumer<String> exportGameCrashInfoCallback;
private boolean stopCheckLogCount = false;
}
private final LogWindowImpl impl;
private final ManagedProcess gameProcess;
public LogWindow(ManagedProcess gameProcess) {
this(gameProcess, new CircularArrayList<>());
}
public LogWindow(ManagedProcess gameProcess, CircularArrayList<Log> logs) {
this.logs = logs;
this.impl = new LogWindowImpl();
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("logwindow.title"));
FXUtils.setIcon(this);
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
for (SimpleBooleanProperty property : levelShownMap.values()) {
property.addListener(o -> shakeLogs());
}
this.gameProcess = gameProcess;
}
public void logLine(String filteredLine, Log4jLevel level) {
Log log = new Log(parseEscapeSequence(filteredLine), level);
public void logLine(Log log) {
Log4jLevel level = log.getLevel();
logs.add(log);
if (levelShownMap.get(level).get())
impl.listView.getItems().add(log);
levelCountMap.get(level).setValue(levelCountMap.get(level).getValue() + 1);
if (!stopCheckLogCount) checkLogCount();
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
property.set(property.get() + 1);
checkLogCount();
autoScroll();
}
public void showGameCrashReport(Consumer<String> exportGameCrashInfoCallback) {
this.exportGameCrashInfoCallback = exportGameCrashInfoCallback;
this.impl.showCrashReport.set(true);
stopCheckLogCount = true;
for (Log log : impl.listView.getItems()) {
if (log.log.contains("Minecraft Crash Report")) {
Platform.runLater(() -> {
impl.listView.scrollTo(log);
});
break;
}
public void logLines(List<Log> logs) {
for (Log log : logs) {
Log4jLevel level = log.getLevel();
this.logs.add(log);
if (levelShownMap.get(level).get())
impl.listView.getItems().add(log);
SimpleIntegerProperty property = levelCountMap.get(log.getLevel());
property.set(property.get() + 1);
}
show();
}
public void showNormal() {
this.impl.showCrashReport.set(false);
show();
checkLogCount();
autoScroll();
}
private void shakeLogs() {
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.level).get()).collect(Collectors.toList()));
impl.listView.getItems().setAll(logs.stream().filter(log -> levelShownMap.get(log.getLevel()).get()).collect(Collectors.toList()));
autoScroll();
}
private void checkLogCount() {
while (logs.size() > config().getLogLines()) {
int nRemove = logs.size() - Log.getLogLines();
if (nRemove <= 0)
return;
ObservableList<Log> items = impl.listView.getItems();
int itemsSize = items.size();
int count = 0;
for (int i = 0; i < nRemove; i++) {
Log removedLog = logs.removeFirst();
if (!impl.listView.getItems().isEmpty() && impl.listView.getItems().get(0) == removedLog) {
impl.listView.getItems().remove(0);
}
if (itemsSize > count && items.get(count) == removedLog)
count++;
}
items.remove(0, count);
}
private static class Log {
private final String log;
private final Log4jLevel level;
private boolean selected = false;
public Log(String log, Log4jLevel level) {
this.log = log;
this.level = level;
}
private void autoScroll() {
if (!impl.listView.getItems().isEmpty() && impl.autoScroll.get())
impl.listView.scrollTo(impl.listView.getItems().size() - 1);
}
public class LogWindowImpl extends Control {
private final class LogWindowImpl extends Control {
private final ListView<Log> listView = new JFXListView<>();
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
private final JFXComboBox<Integer> cboLines = new JFXComboBox<>();
LogWindowImpl() {
getStyleClass().add("log-window");
listView.setItems(FXCollections.observableList(new CircularArrayList<>(config().getLogLines() + 1)));
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
boolean flag = false;
cboLines.getItems().setAll("500", "2000", "5000", "10000");
for (String i : cboLines.getItems())
if (Integer.toString(config().getLogLines()).equals(i)) {
cboLines.getSelectionModel().select(i);
flag = true;
}
for (int i = 0; i < LEVELS.length; i++) {
buttonText[i] = new SimpleStringProperty();
showLevel[i] = new SimpleBooleanProperty(true);
}
if (!flag)
cboLines.getSelectionModel().select(2);
cboLines.getItems().setAll(500, 2000, 5000, 10000);
cboLines.setValue(Log.getLogLines());
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> config().setLogLines(newValue));
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
config().setLogLines(newValue == null ? 1000 : Integer.parseInt(newValue));
});
Log4jLevel[] levels = new Log4jLevel[]{Log4jLevel.FATAL, Log4jLevel.ERROR, Log4jLevel.WARN, Log4jLevel.INFO, Log4jLevel.DEBUG};
String[] suffix = new String[]{"fatals", "errors", "warns", "infos", "debugs"};
for (int i = 0; i < 5; ++i) {
buttonText.get(i).bind(Bindings.concat(levelCountMap.get(levels[i]), " " + suffix[i]));
levelShownMap.get(levels[i]).bind(showLevel.get(i));
for (int i = 0; i < LEVELS.length; ++i) {
buttonText[i].bind(Bindings.concat(levelCountMap.get(LEVELS[i]), " " + LEVELS[i].name().toLowerCase(Locale.ROOT) + "s"));
levelShownMap.get(LEVELS[i]).bind(showLevel[i]);
}
}
@@ -208,9 +197,9 @@ public final class LogWindow extends Stage {
thread(() -> {
Path logFile = Paths.get("minecraft-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
try {
Files.write(logFile, logs.stream().map(x -> x.log).collect(Collectors.toList()));
Files.write(logFile, logs.stream().map(Log::getLog).collect(Collectors.toList()));
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to export logs", e);
LOG.warning("Failed to export logs", e);
return;
}
@@ -224,38 +213,31 @@ public final class LogWindow extends Stage {
});
}
private void onExportDump(JFXButton button) {
private void onExportDump(SpinnerPane pane) {
assert SystemUtils.supportJVMAttachment();
pane.setLoading(true);
thread(() -> {
if (button.getText().equals(i18n("logwindow.export_dump.dependency_ok.button"))) {
if (SystemUtils.supportJVMAttachment()) {
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.doing_button")));
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to create minecraft jstack dump", e);
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump.dependency_ok.button"));
alert.setTitle(i18n("message.error"));
alert.showAndWait();
});
}
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.button")));
try {
if (gameProcess.isRunning()) {
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
FXUtils.showFileInExplorer(dumpFile);
}
}
});
}
} catch (Throwable e) {
LOG.warning("Failed to create minecraft jstack dump", e);
private void onExportGameCrashInfo() {
if (exportGameCrashInfoCallback == null) return;
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump"));
alert.setTitle(i18n("message.error"));
alert.showAndWait();
});
}
Platform.runLater(() -> pane.setLoading(false));
});
}
@Override
@@ -264,7 +246,7 @@ public final class LogWindow extends Stage {
}
}
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static final class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
@@ -276,17 +258,7 @@ public final class LogWindow extends Stage {
private final Set<ListCell<Log>> selected = new HashSet<>();
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + backgroundColor + ";");
button.getStyleClass().add("log-toggle");
button.textProperty().bind(buttonText);
button.setSelected(true);
showLevel.bind(button.selectedProperty());
return button;
}
protected LogWindowSkin(LogWindowImpl control) {
LogWindowSkin(LogWindowImpl control) {
super(control);
VBox vbox = new VBox(3);
@@ -311,13 +283,16 @@ public final class LogWindow extends Stage {
{
HBox hBox = new HBox(3);
hBox.getChildren().setAll(
createToggleButton("#F7A699", control.buttonText.get(0), control.showLevel.get(0)),
createToggleButton("#FFCCBB", control.buttonText.get(1), control.showLevel.get(1)),
createToggleButton("#FFEECC", control.buttonText.get(2), control.showLevel.get(2)),
createToggleButton("#FBFBFB", control.buttonText.get(3), control.showLevel.get(3)),
createToggleButton("#EEE9E0", control.buttonText.get(4), control.showLevel.get(4))
);
for (int i = 0; i < LEVELS.length; i++) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";");
button.getStyleClass().add("log-toggle");
button.textProperty().bind(control.buttonText[i]);
button.setSelected(true);
control.showLevel[i].bind(button.selectedProperty());
hBox.getChildren().add(button);
}
borderPane.setRight(hBox);
}
@@ -348,11 +323,11 @@ public final class LogWindow extends Stage {
setOnMouseClicked(event -> {
if (!event.isControlDown()) {
for (ListCell<Log> logListCell: selected) {
for (ListCell<Log> logListCell : selected) {
if (logListCell != this) {
logListCell.pseudoClassStateChanged(SELECTED, false);
if (logListCell.getItem() != null) {
logListCell.getItem().selected = false;
logListCell.getItem().setSelected(false);
}
}
}
@@ -363,7 +338,7 @@ public final class LogWindow extends Stage {
selected.add(this);
pseudoClassStateChanged(SELECTED, true);
if (getItem() != null) {
getItem().selected = true;
getItem().setSelected(true);
}
});
}
@@ -378,18 +353,18 @@ public final class LogWindow extends Stage {
lastCell.value = this;
pseudoClassStateChanged(EMPTY, empty);
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
pseudoClassStateChanged(WARN, !empty && item.level == Log4jLevel.WARN);
pseudoClassStateChanged(INFO, !empty && item.level == Log4jLevel.INFO);
pseudoClassStateChanged(DEBUG, !empty && item.level == Log4jLevel.DEBUG);
pseudoClassStateChanged(TRACE, !empty && item.level == Log4jLevel.TRACE);
pseudoClassStateChanged(SELECTED, !empty && item.selected);
pseudoClassStateChanged(FATAL, !empty && item.getLevel() == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.getLevel() == Log4jLevel.ERROR);
pseudoClassStateChanged(WARN, !empty && item.getLevel() == Log4jLevel.WARN);
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);
} else {
setText(item.log);
setText(item.getLog());
}
}
});
@@ -399,10 +374,9 @@ public final class LogWindow extends Stage {
StringBuilder stringBuilder = new StringBuilder();
for (Log item : listView.getItems()) {
if (item != null && item.selected) {
if (item.log != null) {
stringBuilder.append(item.log);
}
if (item != null && item.isSelected()) {
if (item.getLog() != null)
stringBuilder.append(item.getLog());
stringBuilder.append('\n');
}
}
@@ -418,11 +392,6 @@ public final class LogWindow extends Stage {
{
BorderPane bottom = new BorderPane();
JFXButton exportGameCrashInfoButton = new JFXButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnMouseClicked(e -> getSkinnable().onExportGameCrashInfo());
exportGameCrashInfoButton.visibleProperty().bind(getSkinnable().showCrashReport);
bottom.setLeft(exportGameCrashInfoButton);
HBox hBox = new HBox(3);
bottom.setRight(hBox);
hBox.setAlignment(Pos.CENTER_RIGHT);
@@ -433,24 +402,24 @@ public final class LogWindow extends Stage {
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs());
exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs());
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
terminateButton.setOnAction(e -> getSkinnable().onTerminateGame());
JFXButton exportDumpButton = new JFXButton();
SpinnerPane exportDumpPane = new SpinnerPane();
JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump"));
if (SystemUtils.supportJVMAttachment()) {
exportDumpButton.setText(i18n("logwindow.export_dump.dependency_ok.button"));
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpButton));
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
} else {
exportDumpButton.setText(i18n("logwindow.export_dump.no_dependency.button"));
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency.tooltip")));
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency")));
exportDumpButton.setDisable(true);
}
exportDumpPane.setContent(exportDumpButton);
JFXButton clearButton = new JFXButton(i18n("button.clear"));
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpButton, clearButton);
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
vbox.getChildren().add(bottom);
}
@@ -110,7 +110,8 @@ public enum SVG {
LAN("M10,2C8.89,2 8,2.89 8,4V7C8,8.11 8.89,9 10,9H11V11H2V13H6V15H5C3.89,15 3,15.89 3,17V20C3,21.11 3.89,22 5,22H9C10.11,22 11,21.11 11,20V17C11,15.89 10.11,15 9,15H8V13H16V15H15C13.89,15 13,15.89 13,17V20C13,21.11 13.89,22 15,22H19C20.11,22 21,21.11 21,20V17C21,15.89 20.11,15 19,15H18V13H22V11H13V9H14C15.11,9 16,8.11 16,7V4C16,2.89 15.11,2 14,2H10M10,4H14V7H10V4M5,17H9V20H5V17M15,17H19V20H15V17Z"),
THUMB_UP_OUTLINE("M5,9V21H1V9H5M9,21A2,2 0 0,1 7,19V9C7,8.45 7.22,7.95 7.59,7.59L14.17,1L15.23,2.06C15.5,2.33 15.67,2.7 15.67,3.11L15.64,3.43L14.69,8H21C22.11,8 23,8.9 23,10V12C23,12.26 22.95,12.5 22.86,12.73L19.84,19.78C19.54,20.5 18.83,21 18,21H9M9,19H18.03L21,12V10H12.21L13.34,4.68L9,9.03V19Z"),
THUMB_DOWN_OUTLINE("M19,15V3H23V15H19M15,3A2,2 0 0,1 17,5V15C17,15.55 16.78,16.05 16.41,16.41L9.83,23L8.77,21.94C8.5,21.67 8.33,21.3 8.33,20.88L8.36,20.57L9.31,16H3C1.89,16 1,15.1 1,14V12C1,11.74 1.05,11.5 1.14,11.27L4.16,4.22C4.46,3.5 5.17,3 6,3H15M15,5H5.97L3,12V14H11.78L10.65,19.32L15,14.97V5Z"),
SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z");
SELECT_ALL("M9,9H15V15H9M7,17H17V7H7M15,5H17V3H15M15,21H17V19H15M19,17H21V15H19M19,9H21V7H19M19,21A2,2 0 0,0 21,19H19M19,13H21V11H19M11,21H13V19H11M9,3H7V5H9M3,17H5V15H3M5,21V19H3A2,2 0 0,0 5,21M19,3V5H21A2,2 0 0,0 19,3M13,3H11V5H13M3,9H5V7H3M7,21H9V19H7M3,13H5V11H3M3,5H5V3A2,2 0 0,0 3,5Z"),
JAVA("m 12.400746,23.498132 c 0,0 -1.047682,0.609185 0.745753,0.816159 2.173248,0.247515 3.284943,0.212301 5.680103,-0.241112 0,0 0.629462,0.394742 1.509642,0.737207 -5.369636,2.301245 -12.1518319,-0.133351 -7.934431,-1.312254 z m -0.656135,-3.003246 c 0,0 -1.175708,0.870569 0.619861,1.056206 2.321545,0.238977 4.155521,0.25925 7.32844,-0.35207 0,0 0.43849,0.444887 1.128765,0.688135 -6.492002,1.897965 -13.7233551,0.149351 -9.077066,-1.392271 z m 5.531806,-5.094318 c 1.322937,1.523496 -0.347807,2.894427 -0.347807,2.894427 0,0 3.359626,-1.733668 1.816908,-3.905821 -1.441364,-2.024926 -2.545591,-3.030987 3.43644,-6.5004569 0,0 -9.389665,2.3449859 -4.905541,7.5129179 z m 7.101193,10.318794 c 0,0 0.775626,0.639057 -0.854578,1.133019 C 20.42373,27.79123 10.623314,28.075018 7.9006204,26.88973 6.9222859,26.464048 8.75733,25.873001 9.3345153,25.749244 9.9362392,25.619093 10.280844,25.642568 10.280844,25.642568 9.1926195,24.875486 3.2457961,27.147927 7.2604887,27.798718 18.208875,29.573994 27.21766,26.999632 24.37761,25.718316 Z M 12.904316,17.383886 c 0,0 -4.9855587,1.184229 -1.765696,1.614178 1.359213,0.182426 4.069103,0.140825 6.594422,-0.07042 2.063359,-0.173899 4.135252,-0.544104 4.135252,-0.544104 0,0 -0.727616,0.311527 -1.253592,0.671064 -5.062375,1.331456 -14.8414526,0.711604 -12.02594,-0.649727 2.38129,-1.151156 4.316621,-1.019929 4.316621,-1.019929 z m 8.943706,4.998298 c 5.146658,-2.673583 2.766435,-5.24368 1.106361,-4.898013 -0.407551,0.08428 -0.58892,0.1579 -0.58892,0.1579 0,0 0.151501,-0.236845 0.439557,-0.339265 3.284942,-1.155424 5.812393,3.406524 -1.060485,5.213807 0,0 0.08003,-0.07147 0.103478,-0.134425 z M 18.744451,2.2855 c 0,0 2.849653,2.8506848 -2.703489,7.2344656 -4.453183,3.5164124 -1.015676,5.5221324 -0.0021,7.8127104 C 13.439893,14.987688 11.532301,12.92329 12.811497,11.001852 14.690284,8.1810394 19.893487,6.8143757 18.743384,2.2855 Z M 13.41002,29.628384 c 4.939684,0.315794 12.525242,-0.174974 12.70448,-2.512485 0,0 -0.345672,0.886571 -4.081906,1.589641 -4.216334,0.793754 -9.416337,0.700936 -12.4996379,0.192025 0,0 0.6315969,0.522768 3.8781309,0.730807 z");
private final String path;
@@ -19,59 +19,73 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.concurrent.Worker;
import javafx.scene.control.Label;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import org.jackhuang.hmcl.Metadata;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Node;
import java.util.logging.Level;
import java.io.IOException;
import java.net.URL;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class UpgradeDialog extends JFXDialogLayout {
public final class UpgradeDialog extends JFXDialogLayout {
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
}
setHeading(new Label(i18n("update.changelog")));
setBody(new ProgressIndicator());
{
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html#nowchange";
try {
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
WebEngine engine = webView.getEngine();
engine.load(url);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == Worker.State.FAILED) {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
});
setBody(webView);
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.log(Level.WARNING, "WebView is missing or initialization failed", e);
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
Task.supplyAsync(Schedulers.io(), () -> {
Document document = Jsoup.parse(new URL(url), 30 * 1000);
Node node = document.selectFirst("#nowchange");
if (node == null)
throw new IOException("Cannot find #nowchange in document");
HTMLRenderer renderer = new HTMLRenderer(uri -> {
LOG.info("Open link: " + uri);
FXUtils.openLink(uri.toString());
});
do {
renderer.appendNode(node);
node = node.nextSibling();
} while (node != null);
return renderer.render();
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
ScrollPane scrollPane = new ScrollPane(result);
scrollPane.setFitToWidth(true);
setBody(scrollPane);
} else {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
}
}).start();
{
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnMouseClicked(e -> updateRunnable.run());
JFXHyperlink openInBrowser = new JFXHyperlink(i18n("web.view_in_browser"));
openInBrowser.setExternalLink(url);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnAction(e -> updateRunnable.run());
setActions(updateButton, cancelButton);
onEscPressed(this, cancelButton::fire);
}
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
setActions(openInBrowser, updateButton, cancelButton);
onEscPressed(this, cancelButton::fire);
}
}
@@ -0,0 +1,75 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class WebPage extends SpinnerPane implements DecoratorPage {
private final ObjectProperty<DecoratorPage.State> stateProperty;
public WebPage(String title, String content) {
this.stateProperty = new SimpleObjectProperty<>(DecoratorPage.State.fromTitle(title));
this.setBackground(new Background(new BackgroundFill(Color.WHITE, null, null)));
Task.supplyAsync(() -> {
Document document = Jsoup.parseBodyFragment(content);
HTMLRenderer renderer = new HTMLRenderer(uri -> {
Controllers.confirm(i18n("web.open_in_browser", uri), i18n("message.confirm"), () -> {
FXUtils.openLink(uri.toString());
}, null);
});
renderer.appendNode(document);
return renderer.render();
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
scrollPane.setContent(result);
setContent(scrollPane);
setFailedReason(null);
} else {
LOG.warning("Failed to load content", exception);
setFailedReason(i18n("web.failed"));
}
})).start();
}
@Override
public ReadOnlyObjectProperty<DecoratorPage.State> stateProperty() {
return stateProperty;
}
}
@@ -1,72 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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.jfoenix.controls.JFXProgressBar;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Theme;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
public class WebStage extends Stage {
protected final StackPane pane = new StackPane();
protected final JFXProgressBar progressBar = new JFXProgressBar();
protected final WebView webView = new WebView();
protected final WebEngine webEngine = webView.getEngine();
public WebStage() {
this(800, 480);
}
public WebStage(int width, int height) {
setScene(new Scene(pane, width, height));
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
FXUtils.setIcon(this);
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
webView.setContextMenuEnabled(false);
progressBar.progressProperty().bind(webView.getEngine().getLoadWorker().progressProperty());
progressBar.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
switch (webView.getEngine().getLoadWorker().getState()) {
case SUCCEEDED:
case FAILED:
case CANCELLED:
return false;
default:
return true;
}
}, webEngine.getLoadWorker().stateProperty()));
BorderPane borderPane = new BorderPane();
borderPane.setPickOnBounds(false);
borderPane.setTop(progressBar);
progressBar.prefWidthProperty().bind(borderPane.widthProperty());
pane.getChildren().setAll(webView, borderPane);
}
public WebView getWebView() {
return webView;
}
}
@@ -27,7 +27,6 @@ import javafx.scene.control.Tooltip;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
@@ -58,7 +57,7 @@ public class AccountAdvancedListItem extends AdvancedListItem {
tooltip.setText(i18n("account.create"));
TexturesLoader.unbindAvatar(canvas);
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage());
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkinImage());
} else {
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
@@ -54,11 +54,10 @@ import java.io.IOException;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import static java.util.Collections.emptySet;
import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountListItem extends RadioButton {
@@ -109,11 +108,11 @@ public class AccountListItem extends RadioButton {
} catch (CancellationException e1) {
// ignore cancellation
} catch (Exception e1) {
LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
LOG.warning("Failed to refresh " + account + " with password", e1);
throw e1;
}
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to refresh " + account + " with token", e);
LOG.warning("Failed to refresh " + account + " with token", e);
throw e;
}
});
@@ -177,7 +177,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
right.getChildren().add(spinnerCopyUUID);
JFXButton btnRemove = new JFXButton();
btnRemove.setOnMouseClicked(e -> skinnable.remove());
btnRemove.setOnMouseClicked(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.DELETE.createIcon(Theme.blackFill(), -1, -1));
@@ -44,10 +44,9 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.net.URI;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
@@ -124,7 +123,9 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
JFXButton btnRemove = new JFXButton();
btnRemove.setOnAction(e -> {
skinnable.authServersProperty().remove(server);
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
skinnable.authServersProperty().remove(server);
}, null);
e.consume();
});
btnRemove.getStyleClass().add("toggle-icon4");
@@ -137,7 +138,7 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
try {
host = URI.create(server.getUrl()).getHost();
} catch (IllegalArgumentException e) {
LOG.log(Level.WARNING, "Unparsable authlib-injector server url " + server.getUrl(), e);
LOG.warning("Unparsable authlib-injector server url " + server.getUrl(), e);
}
item.subtitleProperty().set(host);
Tooltip tooltip = new Tooltip();
@@ -35,11 +35,10 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class AddAuthlibInjectorServerPane extends TransitionPane implements DialogAware {
@@ -201,7 +200,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
this.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else {
LOG.log(Level.WARNING, "Failed to resolve auth server: " + url, exception);
LOG.warning("Failed to resolve auth server: " + url, exception);
lblCreationWarning.setText(resolveFetchExceptionMessage(exception));
}
}).start();
@@ -36,10 +36,9 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import java.util.function.Consumer;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ClassicAccountLoginDialog extends StackPane {
@@ -109,7 +108,7 @@ public class ClassicAccountLoginDialog extends StackPane {
fireEvent(new DialogCloseEvent());
progressBar.setVisible(false);
}, e -> {
LOG.log(Level.INFO, "Failed to login with password: " + oldAccount, e);
LOG.info("Failed to login with password: " + oldAccount, e);
if (e instanceof NoSelectedCharacterException) {
fireEvent(new DialogCloseEvent());
} else {
@@ -21,9 +21,8 @@ import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import java.util.function.Consumer;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class OAuthAccountLoginDialog extends DialogPane {
@@ -96,7 +95,7 @@ public class OAuthAccountLoginDialog extends DialogPane {
success.accept(authInfo);
onSuccess();
} else {
LOG.log(Level.INFO, "Failed to login when credentials expired: " + account, exception);
LOG.info("Failed to login when credentials expired: " + account, exception);
onFailure(Accounts.localizeErrorMessage(exception));
}
}).start();
@@ -25,7 +25,6 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
@@ -35,6 +34,7 @@ import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -42,11 +42,11 @@ import org.jackhuang.hmcl.ui.construct.*;
import java.io.File;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.UUID;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class OfflineAccountSkinPane extends StackPane {
@@ -71,7 +71,7 @@ public class OfflineAccountSkinPane extends StackPane {
BorderPane pane = new BorderPane();
SkinCanvas canvas = new SkinCanvas(SkinCanvas.STEVE, 300, 300, true);
SkinCanvas canvas = new SkinCanvas(TexturesLoader.getDefaultSkinImage(), 300, 300, true);
StackPane canvasPane = new StackPane(canvas);
canvasPane.setPrefWidth(300);
canvasPane.setPrefHeight(300);
@@ -116,11 +116,11 @@ public class OfflineAccountSkinPane extends StackPane {
));
modelCombobox.setConverter(stringConverter(model -> i18n("account.skin.model." + model.modelName)));
modelCombobox.getItems().setAll(TextureModel.STEVE, TextureModel.ALEX);
modelCombobox.getItems().setAll(TextureModel.WIDE, TextureModel.SLIM);
if (account.getSkin() == null) {
skinItem.setSelectedData(Skin.Type.DEFAULT);
modelCombobox.setValue(TextureModel.STEVE);
modelCombobox.setValue(TextureModel.WIDE);
} else {
skinItem.setSelectedData(account.getSkin().getType());
cslApiField.setText(account.getSkin().getCslApi());
@@ -133,16 +133,21 @@ public class OfflineAccountSkinPane extends StackPane {
getSkin().load(account.getUsername())
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
LOG.log(Level.WARNING, "Failed to load skin", exception);
LOG.warning("Failed to load skin", exception);
Controllers.showToast(i18n("message.failed"));
} else {
UUID uuid = this.account.getUUID();
if (result == null || result.getSkin() == null && result.getCape() == null) {
canvas.updateSkin(getDefaultTexture(), isDefaultSlim(), null);
canvas.updateSkin(
TexturesLoader.getDefaultSkin(uuid).getImage(),
TexturesLoader.getDefaultModel(uuid) == TextureModel.SLIM,
null
);
return;
}
canvas.updateSkin(
result.getSkin() != null ? result.getSkin().getImage() : getDefaultTexture(),
result.getModel() == TextureModel.ALEX,
result.getSkin() != null ? result.getSkin().getImage() : TexturesLoader.getDefaultSkin(uuid).getImage(),
result.getModel() == TextureModel.SLIM,
result.getCape() != null ? result.getCape().getImage() : null);
}
}).start();
@@ -198,17 +203,4 @@ public class OfflineAccountSkinPane extends StackPane {
private Skin getSkin() {
return new Skin(skinItem.getSelectedData(), cslApiField.getText(), modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue());
}
private boolean isDefaultSlim() {
return TextureModel.detectUUID(account.getUUID()) == TextureModel.ALEX;
}
private Image getDefaultTexture() {
if (isDefaultSlim()) {
return SkinCanvas.ALEX;
} else {
return SkinCanvas.STEVE;
}
}
}
@@ -166,6 +166,16 @@ public final class MessageDialogPane extends HBox {
return this;
}
public Builder addAction(String text, @Nullable Runnable action) {
JFXButton btnAction = new JFXButton(text);
btnAction.getStyleClass().add("dialog-accept");
if (action != null) {
btnAction.setOnAction(e -> action.run());
}
dialog.addButton(btnAction);
return this;
}
public Builder ok(@Nullable Runnable ok) {
JFXButton btnOk = new JFXButton(i18n("button.ok"));
btnOk.getStyleClass().add("dialog-accept");
@@ -57,7 +57,7 @@ public final class MultiFileItem<T> extends VBox {
if (toggleSelectedListener != null)
toggleSelectedListener.accept(newValue);
selectedData.set((T) newValue.getUserData());
selectedData.set(newValue != null ? (T) newValue.getUserData() : null);
});
selectedData.addListener((a, b, newValue) -> {
Optional<Toggle> selecting = group.getToggles().stream()
@@ -183,6 +183,10 @@ public final class MultiFileItem<T> extends VBox {
super(title, data);
}
public JFXTextField getCustomField() {
return customField;
}
public String getValue() {
return customField.getText();
}
@@ -31,11 +31,11 @@ import org.jackhuang.hmcl.ui.animation.AnimationProducer;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.Logging;
import java.util.Optional;
import java.util.Stack;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class Navigator extends TransitionPane {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = Navigator.class.getName() + ".closeListener";
@@ -65,7 +65,7 @@ public class Navigator extends TransitionPane {
if (from == node)
return;
Logging.LOG.info("Navigate to " + node);
LOG.info("Navigate to " + node);
stack.push(node);
backable.set(canGoBack());
@@ -104,11 +104,11 @@ public class Navigator extends TransitionPane {
if (stack.peek() != from) {
// Allow page to be closed multiple times.
Logging.LOG.log(Level.INFO, "Closing already closed page: " + from, new Throwable());
LOG.info("Closing already closed page: " + from, new Throwable());
return;
}
Logging.LOG.info("Closed page " + from);
LOG.info("Closed page " + from);
Node poppedNode = stack.pop();
NavigationEvent exited = new NavigationEvent(this, poppedNode, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.EXITED);
@@ -19,11 +19,12 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.util.Logging;
import java.util.Optional;
import java.util.Stack;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class StackContainerPane extends StackPane {
private final Stack<Node> stack = new Stack<>();
@@ -39,7 +40,7 @@ public class StackContainerPane extends StackPane {
stack.push(node);
getChildren().setAll(node);
Logging.LOG.info(this + " " + stack);
LOG.info(this + " " + stack);
}
public void pop(Node node) {
@@ -49,7 +50,7 @@ public class StackContainerPane extends StackPane {
else
getChildren().setAll(stack.peek());
Logging.LOG.info(this + " " + stack + ", removed: " + flag + ", object: " + node);
LOG.info(this + " " + stack + ", removed: " + flag + ", object: " + node);
}
public boolean isEmpty() {
@@ -34,7 +34,7 @@ import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.java.JavaDownloadTask;
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask;
import org.jackhuang.hmcl.download.quilt.QuiltInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
import org.jackhuang.hmcl.java.JavaInstallTask;
import org.jackhuang.hmcl.mod.MinecraftInstanceTask;
import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
@@ -159,8 +160,10 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(i18n("modpack.scan"));
} else if (task instanceof JavaDownloadTask) {
} else if (task instanceof MojangJavaDownloadTask) {
task.setName(i18n("download.java"));
} else if (task instanceof JavaInstallTask) {
task.setName(i18n("java.install"));
}
Platform.runLater(() -> {
@@ -62,15 +62,13 @@ import java.util.Locale;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Stream;
import static java.util.logging.Level.WARNING;
import static java.util.stream.Collectors.toList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newBuiltinImage;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.io.FileUtils.getExtension;
public class DecoratorController {
@@ -186,7 +184,7 @@ public class DecoratorController {
}
}
} catch (Exception e) {
LOG.log(WARNING, "Couldn't load background image", e);
LOG.warning("Couldn't load background image", e);
}
}
break;
@@ -234,7 +232,7 @@ public class DecoratorController {
})
.collect(toList());
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to list files in ./bg", e);
LOG.warning("Failed to list files in ./bg", e);
return Optional.empty();
}
@@ -263,12 +261,12 @@ public class DecoratorController {
try {
img = new Image(url);
} catch (IllegalArgumentException e) {
LOG.log(WARNING, "Couldn't load background image", e);
LOG.warning("Couldn't load background image", e);
return Optional.empty();
}
if (img.getException() != null) {
LOG.log(WARNING, "Couldn't load background image", img.getException());
LOG.warning("Couldn't load background image", img.getException());
return Optional.empty();
}
@@ -33,7 +33,7 @@ import org.jackhuang.hmcl.util.Lang;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
class AdditionalInstallersPage extends InstallersPage {
@@ -59,7 +59,7 @@ class AdditionalInstallersPage extends InstallersPage {
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (libraryId.equals("game")) continue;
library.removeAction.set(e -> {
library.removeActionProperty().set(e -> {
controller.getSettings().put(libraryId, new UpdateInstallerWizardProvider.RemoveVersionAction(libraryId));
reload();
});
@@ -99,12 +99,12 @@ class AdditionalInstallersPage extends InstallersPage {
if (!"game".equals(libraryId) && currentGameVersion != null && !currentGameVersion.equals(game) && getVersion(libraryId) == null && alreadyInstalled) {
// For third-party libraries, if game version is being changed, and the library is not being reinstalled,
// warns the user that we should update the library.
library.setState(libraryVersion, /* incompatibleWithGame */ true, /* removable */ true);
library.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, true));
compatible = false;
} else if (alreadyInstalled || getVersion(libraryId) != null) {
library.setState(libraryVersion, /* incompatibleWithGame */ false, /* removable */ true);
library.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
} else {
library.setState(/* libraryVersion */ null, /* incompatibleWithGame */ false, /* removable */ false);
library.versionProperty().set(null);
}
}
@@ -69,7 +69,6 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
private final TabHeader.Tab<DownloadListPage> modTab = new TabHeader.Tab<>("modTab");
private final TabHeader.Tab<DownloadListPage> modpackTab = new TabHeader.Tab<>("modpackTab");
private final TabHeader.Tab<DownloadListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
private final TabHeader.Tab<DownloadListPage> customizationTab = new TabHeader.Tab<>("customizationTab");
private final TabHeader.Tab<DownloadListPage> worldTab = new TabHeader.Tab<>("worldTab");
private final TransitionPane transitionPane = new TransitionPane();
private final DownloadNavigator versionPageNavigator = new DownloadNavigator();
@@ -80,7 +79,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
"game", versionPageNavigator::onGameSelected)));
modpackTab.setNodeSupplier(loadVersionFor(() -> {
ModpackDownloadListPage page = new ModpackDownloadListPage(Versions::downloadModpackImpl, false);
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack(Versions::downloadModpackImpl, false);
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
installLocalModpackButton.setOnAction(e -> Versions.importModpack());
@@ -88,9 +87,8 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
page.getActions().add(installLocalModpackButton);
return page;
}));
modTab.setNodeSupplier(loadVersionFor(() -> new ModDownloadListPage((profile, version, file) -> download(profile, version, file, "mods"), true)));
resourcePackTab.setNodeSupplier(loadVersionFor(() -> new ResourcePackDownloadListPage((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
customizationTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.CUSTOMIZATIONS)));
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, file) -> download(profile, version, file, "mods"), true)));
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, file) -> download(profile, version, file, "resourcepacks"), true)));
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
tab = new TabHeader(newGameTab, modpackTab, modTab, resourcePackTab, worldTab);
@@ -129,12 +127,6 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab));
item.setOnAction(e -> tab.select(resourcePackTab));
})
// .addNavigationDrawerItem(item -> {
// item.setTitle(i18n("download.curseforge.customization"));
// item.setLeftGraphic(wrap(SVG::script));
// item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(customizationTab));
// item.setOnAction(e -> selectTabIfCurseForgeAvailable(customizationTab));
// })
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("world"));
item.setLeftGraphic(wrap(SVG.EARTH));
@@ -170,7 +162,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
Controllers.prompt(i18n("archive.name"), (result, resolve, reject) -> {
Controllers.prompt(i18n("archive.file.name"), (result, resolve, reject) -> {
if (!OperatingSystem.isNameValid(result)) {
reject.accept(i18n("install.new_game.malformed"));
return;
@@ -212,9 +204,6 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
if (resourcePackTab.isInitialized()) {
resourcePackTab.getNode().loadVersion(profile, null);
}
if (customizationTab.isInitialized()) {
customizationTab.getNode().loadVersion(profile, null);
}
if (worldTab.isInitialized()) {
worldTab.getNode().loadVersion(profile, null);
}
@@ -52,44 +52,45 @@ public class InstallersPage extends Control implements WizardPage {
protected JFXTextField txtName = new JFXTextField();
protected BooleanProperty installable = new SimpleBooleanProperty();
private boolean isNameModifiedByUser = false;
public InstallersPage(WizardController controller, HMCLGameRepository repository, String gameVersion, DownloadProvider downloadProvider) {
this.controller = controller;
this.group = new InstallerItem.InstallerItemGroup(gameVersion);
this.group = new InstallerItem.InstallerItemGroup(gameVersion, getInstallerItemStyle());
txtName.getValidators().addAll(
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
installable.bind(createBooleanBinding(txtName::validate, txtName.textProperty()));
txtName.setText(gameVersion);
group.game.installable.setValue(false);
for (InstallerItem item : group.getLibraries()) {
item.setStyleMode(InstallerItem.Style.CARD);
}
txtName.textProperty().addListener((obs, oldText, newText) -> isNameModifiedByUser = true);
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (libraryId.equals(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())) continue;
library.action.set(e -> {
library.installActionProperty().set(e -> {
if (LibraryAnalyzer.LibraryType.FABRIC_API.getPatchId().equals(libraryId)) {
Controllers.dialog(i18n("install.installer.fabric-api.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING);
}
if (library.incompatibleLibraryName.get() == null)
if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState))
controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false)));
});
library.removeAction.set(e -> {
library.removeActionProperty().set(e -> {
controller.getSettings().remove(libraryId);
reload();
});
}
}
protected InstallerItem.Style getInstallerItemStyle() {
return InstallerItem.Style.CARD;
}
@Override
public String getTitle() {
return i18n("install.new_game");
return group.getGame().versionProperty().get().getVersion();
}
private String getVersion(String id) {
@@ -100,13 +101,14 @@ public class InstallersPage extends Control implements WizardPage {
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (controller.getSettings().containsKey(libraryId)) {
library.libraryVersion.set(getVersion(libraryId));
library.removable.set(true);
library.versionProperty().set(new InstallerItem.InstalledState(getVersion(libraryId), false, false));
} else {
library.libraryVersion.set(null);
library.removable.set(false);
library.versionProperty().set(null);
}
}
if (!isNameModifiedByUser) {
setTxtNameWithLoaders();
}
}
@Override
@@ -128,6 +130,49 @@ public class InstallersPage extends Control implements WizardPage {
return new InstallersPageSkin(this);
}
private void setTxtNameWithLoaders() {
StringBuilder nameBuilder = new StringBuilder(group.getGame().versionProperty().get().getVersion());
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId().replace(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), "");
if (!controller.getSettings().containsKey(libraryId)) {
continue;
}
LibraryAnalyzer.LibraryType libraryType = LibraryAnalyzer.LibraryType.fromPatchId(libraryId);
if (libraryType != null) {
String loaderName;
switch (libraryType) {
case FORGE:
loaderName = i18n("install.installer.forge");
break;
case NEO_FORGE:
loaderName = i18n("install.installer.neoforge");
break;
case FABRIC:
loaderName = i18n("install.installer.fabric");
break;
case LITELOADER:
loaderName = i18n("install.installer.liteloader");
break;
case QUILT:
loaderName = i18n("install.installer.quilt");
break;
case OPTIFINE:
loaderName = i18n("install.installer.optifine");
break;
default:
continue;
}
nameBuilder.append("-").append(loaderName);
}
}
txtName.setText(nameBuilder.toString());
isNameModifiedByUser = false;
}
protected static class InstallersPageSkin extends SkinBase<InstallersPage> {
/**
@@ -148,7 +193,7 @@ public class InstallersPage extends Control implements WizardPage {
versionNamePane.setAlignment(Pos.CENTER_LEFT);
control.txtName.setMaxWidth(300);
versionNamePane.getChildren().setAll(new Label(i18n("archive.name")), control.txtName);
versionNamePane.getChildren().setAll(new Label(i18n("version.name")), control.txtName);
root.setTop(versionNamePane);
}
@@ -31,6 +31,7 @@ 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.WebPage;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
@@ -42,10 +43,9 @@ import java.io.File;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LocalModpackPage extends ModpackPage {
@@ -108,7 +108,6 @@ public final class LocalModpackPage extends ModpackPage {
hideSpinner();
lblName.setText(selectedFile.getName());
installAsVersion.set(false);
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
// trim: https://github.com/HMCL-dev/HMCL/issues/962
@@ -121,7 +120,7 @@ public final class LocalModpackPage extends ModpackPage {
controller.getSettings().put(MODPACK_MANUALLY_CREATED, true);
} else if (exception != null) {
LOG.log(Level.WARNING, "Failed to read modpack manifest", exception);
LOG.warning("Failed to read modpack manifest", exception);
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
Platform.runLater(controller::onEnd);
} else {
@@ -131,8 +130,6 @@ public final class LocalModpackPage extends ModpackPage {
lblVersion.setText(manifest.getVersion());
lblAuthor.setText(manifest.getAuthor());
lblModpackLocation.setText(selectedFile.getAbsolutePath());
if (!name.isPresent()) {
// trim: https://github.com/HMCL-dev/HMCL/issues/962
txtModpackName.setText(manifest.getName().trim());
@@ -154,9 +151,8 @@ public final class LocalModpackPage extends ModpackPage {
}
protected void onDescribe() {
if (manifest != null) {
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
}
if (manifest != null)
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
}
public static final String MODPACK_FILE = "MODPACK_FILE";
@@ -22,7 +22,6 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
protected final Label lblName;
protected final Label lblVersion;
protected final Label lblAuthor;
protected final Label lblModpackLocation;
protected final JFXTextField txtModpackName;
protected final JFXButton btnInstall;
@@ -37,18 +36,9 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
ComponentList componentList = new ComponentList();
{
BorderPane locationPane = new BorderPane();
{
locationPane.setLeft(new Label(i18n("modpack.task.install.will")));
lblModpackLocation = new Label();
BorderPane.setAlignment(lblModpackLocation, Pos.CENTER_RIGHT);
locationPane.setCenter(lblModpackLocation);
}
BorderPane archiveNamePane = new BorderPane();
{
Label label = new Label(i18n("archive.name"));
Label label = new Label(i18n("archive.file.name"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
archiveNamePane.setLeft(label);
@@ -99,7 +89,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
}
componentList.getContent().setAll(
locationPane, archiveNamePane, modpackNamePane, versionPane, authorPane, descriptionPane);
archiveNamePane, modpackNamePane, versionPane, authorPane, descriptionPane);
}
borderPane.getChildren().setAll(componentList);
@@ -22,7 +22,7 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WebPage;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
@@ -43,7 +43,6 @@ public final class RemoteModpackPage extends ModpackPage {
manifest = tryCast(controller.getSettings().get(MODPACK_SERVER_MANIFEST), ServerModpackManifest.class)
.orElseThrow(() -> new IllegalStateException("MODPACK_SERVER_MANIFEST should exist"));
lblModpackLocation.setText(manifest.getFileApi());
try {
controller.getSettings().put(MODPACK_MANIFEST, manifest.toModpack(null));
@@ -84,7 +83,7 @@ public final class RemoteModpackPage extends ModpackPage {
}
protected void onDescribe() {
FXUtils.showWebDialog(i18n("modpack.description"), manifest.getDescription());
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
}
public static final String MODPACK_SERVER_MANIFEST = "MODPACK_SERVER_MANIFEST";
@@ -57,11 +57,10 @@ import org.jackhuang.hmcl.util.Holder;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.wrap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -232,7 +231,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
}
});
} else {
LOG.log(Level.WARNING, "Failed to fetch versions list", exception);
LOG.warning("Failed to fetch versions list", exception);
Platform.runLater(() -> {
if (versionList != currentVersionList) return;
root.setContent(failedPane, ContainerAnimations.FADE.getAnimationProducer());
@@ -17,18 +17,30 @@
*/
package org.jackhuang.hmcl.ui.main;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
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.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.IconedTwoLineListItem;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class AboutPage extends StackPane {
public final class AboutPage extends StackPane {
public AboutPage() {
ComponentList about = new ComponentList();
@@ -48,126 +60,9 @@ public class AboutPage extends StackPane {
about.getContent().setAll(launcher, author);
}
ComponentList thanks = new ComponentList();
{
IconedTwoLineListItem yushijinhun = new IconedTwoLineListItem();
yushijinhun.setImage(FXUtils.newBuiltinImage("/assets/img/yushijinhun.png"));
yushijinhun.setTitle("yushijinhun");
yushijinhun.setSubtitle(i18n("about.thanks_to.yushijinhun.statement"));
yushijinhun.setExternalLink("https://yushi.moe/");
ComponentList thanks = loadIconedTwoLineList("/assets/about/thanks.json");
IconedTwoLineListItem bangbang93 = new IconedTwoLineListItem();
bangbang93.setImage(FXUtils.newBuiltinImage("/assets/img/bangbang93.png"));
bangbang93.setTitle("bangbang93");
bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement"));
bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/");
IconedTwoLineListItem glavo = new IconedTwoLineListItem();
glavo.setImage(FXUtils.newBuiltinImage("/assets/img/glavo.png"));
glavo.setTitle("Glavo");
glavo.setSubtitle(i18n("about.thanks_to.glavo.statement"));
glavo.setExternalLink("https://github.com/Glavo");
IconedTwoLineListItem zekerzhayard = new IconedTwoLineListItem();
zekerzhayard.setImage(FXUtils.newBuiltinImage("/assets/img/zekerzhayard.png"));
zekerzhayard.setTitle("ZekerZhayard");
zekerzhayard.setSubtitle(i18n("about.thanks_to.zekerzhayard.statement"));
zekerzhayard.setExternalLink("https://github.com/ZekerZhayard");
IconedTwoLineListItem zkitefly = new IconedTwoLineListItem();
zkitefly.setImage(FXUtils.newBuiltinImage("/assets/img/zkitefly.png"));
zkitefly.setTitle("Zkitefly");
zkitefly.setSubtitle(i18n("about.thanks_to.zkitefly.statement"));
zkitefly.setExternalLink("https://github.com/zkitefly");
IconedTwoLineListItem burningtnt = new IconedTwoLineListItem();
burningtnt.setImage(FXUtils.newBuiltinImage("/assets/img/burningtnt.png"));
burningtnt.setTitle("Burning_TNT");
burningtnt.setSubtitle(i18n("about.thanks_to.burningtnt.statement"));
burningtnt.setExternalLink("https://github.com/burningtnt");
IconedTwoLineListItem shulkerSakura = new IconedTwoLineListItem();
shulkerSakura.setTitle("ShulkerSakura");
shulkerSakura.setImage(FXUtils.newBuiltinImage("/assets/img/ShulkerSakura.png"));
shulkerSakura.setSubtitle(i18n("about.thanks_to.shulkersakura.statement"));
shulkerSakura.setExternalLink("https://github.com/ShulkerSakura");
IconedTwoLineListItem gamerteam = new IconedTwoLineListItem();
gamerteam.setTitle("gamerteam");
gamerteam.setImage(FXUtils.newBuiltinImage("/assets/img/gamerteam.png"));
gamerteam.setSubtitle(i18n("about.thanks_to.gamerteam.statement"));
gamerteam.setExternalLink("http://www.zhaisoul.com/");
IconedTwoLineListItem redLnn = new IconedTwoLineListItem();
redLnn.setTitle("Red_lnn");
redLnn.setImage(FXUtils.newBuiltinImage("/assets/img/red_lnn.png"));
redLnn.setSubtitle(i18n("about.thanks_to.red_lnn.statement"));
IconedTwoLineListItem mcmod = new IconedTwoLineListItem();
mcmod.setImage(FXUtils.newBuiltinImage("/assets/img/mcmod.png"));
mcmod.setTitle(i18n("about.thanks_to.mcmod"));
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
mcmod.setExternalLink("https://www.mcmod.cn/");
IconedTwoLineListItem contributors = new IconedTwoLineListItem();
contributors.setImage(FXUtils.newBuiltinImage("/assets/img/github.png"));
contributors.setTitle(i18n("about.thanks_to.contributors"));
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
contributors.setExternalLink("https://github.com/HMCL-dev/HMCL/graphs/contributors");
IconedTwoLineListItem users = new IconedTwoLineListItem();
users.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
users.setTitle(i18n("about.thanks_to.users"));
users.setSubtitle(i18n("about.thanks_to.users.statement"));
users.setExternalLink("https://docs.hmcl.net/groups.html");
thanks.getContent().setAll(yushijinhun, bangbang93, glavo, zekerzhayard, zkitefly, burningtnt, mcmod, shulkerSakura, gamerteam, redLnn, contributors, users);
}
ComponentList dep = new ComponentList();
{
IconedTwoLineListItem javafx = new IconedTwoLineListItem();
javafx.setTitle("JavaFX");
javafx.setSubtitle("Copyright © 2013, 2024, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
javafx.setExternalLink("https://openjfx.io/");
IconedTwoLineListItem jfoenix = new IconedTwoLineListItem();
jfoenix.setTitle("JFoenix");
jfoenix.setSubtitle("Copyright © 2016 JFoenix.\nLicensed under the MIT License.");
jfoenix.setExternalLink("https://github.com/sshahine/JFoenix");
IconedTwoLineListItem gson = new IconedTwoLineListItem();
gson.setTitle("Gson");
gson.setSubtitle("Copyright © 2008 Google Inc.\nLicensed under the Apache 2.0 License.");
gson.setExternalLink("https://github.com/google/gson");
IconedTwoLineListItem xz = new IconedTwoLineListItem();
xz.setTitle("XZ for Java");
xz.setSubtitle("Lasse Collin, Igor Pavlov, and/or Brett Okken.\nPublic Domain.");
xz.setExternalLink("https://xz.tukaani.org/xz-for-java/");
IconedTwoLineListItem fxgson = new IconedTwoLineListItem();
fxgson.setTitle("fx-gson");
fxgson.setSubtitle("Copyright © 2016 Joffrey Bion.\nLicensed under the MIT License.");
fxgson.setExternalLink("https://github.com/joffrey-bion/fx-gson");
IconedTwoLineListItem constantPoolScanner = new IconedTwoLineListItem();
constantPoolScanner.setTitle("Constant Pool Scanner");
constantPoolScanner.setSubtitle("Copyright © 1997-2010 Oracle and/or its affiliates.\nLicensed under the GPL 2 or the CDDL.");
constantPoolScanner.setExternalLink("https://github.com/jenkinsci/constant-pool-scanner");
IconedTwoLineListItem openNBT = new IconedTwoLineListItem();
openNBT.setTitle("OpenNBT");
openNBT.setSubtitle("Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.");
openNBT.setExternalLink("https://github.com/GeyserMC/OpenNBT");
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
minecraftJFXSkin.setTitle("minecraft-jfx-skin");
minecraftJFXSkin.setSubtitle("Copyright © 2016 InfinityStudio.\nLicensed under the GPL 3.");
minecraftJFXSkin.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, minecraftJFXSkin);
}
ComponentList deps = loadIconedTwoLineList("/assets/about/deps.json");
ComponentList legal = new ComponentList();
{
@@ -199,7 +94,7 @@ public class AboutPage extends StackPane {
thanks,
ComponentList.createComponentListTitle(i18n("about.dependency")),
dep,
deps,
ComponentList.createComponentListTitle(i18n("about.legal")),
legal
@@ -211,4 +106,49 @@ public class AboutPage extends StackPane {
FXUtils.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
private static ComponentList loadIconedTwoLineList(String path) {
ComponentList componentList = new ComponentList();
InputStream input = FXUtils.class.getResourceAsStream(path);
if (input == null) {
LOG.warning("Resources not found: " + path);
return componentList;
}
try (Reader reader = new InputStreamReader(input)) {
JsonArray array = JsonUtils.GSON.fromJson(reader, JsonArray.class);
for (JsonElement element : array) {
JsonObject obj = element.getAsJsonObject();
IconedTwoLineListItem item = new IconedTwoLineListItem();
if (obj.has("image")) {
String image = obj.get("image").getAsString();
item.setImage(image.startsWith("/")
? FXUtils.newBuiltinImage(image)
: new Image(image));
}
if (obj.has("title"))
item.setTitle(obj.get("title").getAsString());
else if (obj.has("titleLocalized"))
item.setTitle(i18n(obj.get("titleLocalized").getAsString()));
if (obj.has("subtitle"))
item.setSubtitle(obj.get("subtitle").getAsString());
else if (obj.has("subtitleLocalized"))
item.setSubtitle(i18n(obj.get("subtitleLocalized").getAsString()));
if (obj.has("externalLink"))
item.setExternalLink(obj.get("externalLink").getAsString());
componentList.getContent().add(item);
}
} catch (IOException | JsonParseException e) {
LOG.warning("Failed to load list: " + path, e);
}
return componentList;
}
}
@@ -59,13 +59,7 @@ public class FeedbackPage extends SpinnerPane {
discord.setSubtitle(i18n("feedback.discord.statement"));
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
IconedTwoLineListItem kookapp = new IconedTwoLineListItem();
kookapp.setImage(FXUtils.newBuiltinImage("/assets/img/kookapp.png"));
kookapp.setTitle(i18n("feedback.kookapp"));
kookapp.setSubtitle(i18n("feedback.kookapp.statement"));
kookapp.setExternalLink("https://kook.top/Kx7n3t");
community.getContent().setAll(users, github, discord, kookapp);
community.getContent().setAll(users, github, discord);
}
content.getChildren().addAll(
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
@@ -34,6 +33,7 @@ import org.jackhuang.hmcl.util.io.HttpRequest;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class HelpPage extends SpinnerPane {
@@ -63,8 +63,7 @@ public class HelpPage extends SpinnerPane {
private void loadHelp() {
showSpinner();
Task.<List<HelpCategory>>supplyAsync(() -> HttpRequest.GET("https://docs.hmcl.net/index.json").getJson(new TypeToken<List<HelpCategory>>() {
}.getType()))
Task.supplyAsync(() -> HttpRequest.GET("https://docs.hmcl.net/index.json").getJson(listTypeOf(HelpCategory.class)))
.thenAcceptAsync(Schedulers.javafx(), helpCategories -> {
for (HelpCategory category : helpCategories) {
ComponentList categoryPane = new ComponentList();
@@ -0,0 +1,423 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.java.JavaDistribution;
import org.jackhuang.hmcl.download.java.JavaPackageType;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.download.java.disco.*;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.Controllers;
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.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.platform.Platform;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CancellationException;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
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;
/**
* @author Glavo
*/
public final class JavaDownloadDialog extends StackPane {
public static Runnable showDialogAction(DownloadProvider downloadProvider) {
Platform platform = Platform.SYSTEM_PLATFORM;
List<GameJavaVersion> supportedVersions = GameJavaVersion.getSupportedVersions(platform);
EnumSet<DiscoJavaDistribution> distributions = EnumSet.noneOf(DiscoJavaDistribution.class);
for (DiscoJavaDistribution distribution : DiscoJavaDistribution.values()) {
if (distribution.isSupport(platform)) {
distributions.add(distribution);
}
}
return supportedVersions.isEmpty() && distributions.isEmpty()
? null
: () -> Controllers.dialog(new JavaDownloadDialog(downloadProvider, platform, supportedVersions, distributions));
}
private final DownloadProvider downloadProvider;
private final Platform platform;
private final List<GameJavaVersion> supportedGameJavaVersions;
private final EnumSet<DiscoJavaDistribution> distributions;
private JavaDownloadDialog(DownloadProvider downloadProvider, Platform platform, List<GameJavaVersion> supportedGameJavaVersions, EnumSet<DiscoJavaDistribution> distributions) {
this.downloadProvider = downloadProvider;
this.platform = platform;
this.supportedGameJavaVersions = supportedGameJavaVersions;
this.distributions = distributions;
if (!supportedGameJavaVersions.isEmpty()) {
this.getChildren().add(new DownloadMojangJava());
} else {
this.getChildren().add(new DownloadDiscoJava());
}
}
private final class DownloadMojangJava extends DialogPane {
private final ToggleGroup toggleGroup = new ToggleGroup();
DownloadMojangJava() {
setTitle(i18n("java.download"));
validProperty().bind(toggleGroup.selectedToggleProperty().isNotNull());
VBox vbox = new VBox(16);
Label prompt = new Label(i18n("java.download.prompt"));
vbox.getChildren().add(prompt);
for (GameJavaVersion version : supportedGameJavaVersions) {
JFXRadioButton button = new JFXRadioButton("Java " + version.getMajorVersion());
button.setUserData(version);
vbox.getChildren().add(button);
toggleGroup.getToggles().add(button);
}
setBody(vbox);
if (!distributions.isEmpty()) {
JFXHyperlink more = new JFXHyperlink(i18n("java.download.more"));
more.setOnAction(event -> JavaDownloadDialog.this.getChildren().setAll(new DownloadDiscoJava()));
setActions(warningLabel, more, acceptPane, cancelButton);
} else
setActions(warningLabel, acceptPane, cancelButton);
}
private Task<Void> downloadTask(GameJavaVersion javaVersion) {
return JavaManager.getDownloadJavaTask(downloadProvider, platform, javaVersion).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
}
});
}
@Override
protected void onAccept() {
fireEvent(new DialogCloseEvent());
GameJavaVersion javaVersion = (GameJavaVersion) toggleGroup.getSelectedToggle().getUserData();
if (javaVersion == null)
return;
if (JavaManager.REPOSITORY.isInstalled(platform, javaVersion))
Controllers.confirm(i18n("download.java.override"), null, () -> {
Controllers.taskDialog(Task.supplyAsync(() -> JavaManager.REPOSITORY.getJavaExecutable(platform, javaVersion))
.thenComposeAsync(Schedulers.javafx(), realPath -> {
if (realPath != null) {
JavaManager.removeJava(realPath);
}
return downloadTask(javaVersion);
}), i18n("download.java"), TaskCancellationAction.NORMAL);
}, null);
else
Controllers.taskDialog(downloadTask(javaVersion), i18n("download.java"), TaskCancellationAction.NORMAL);
}
}
private final class DownloadDiscoJava extends JFXDialogLayout {
private boolean isLTS(int major) {
if (major <= 8) {
return true;
}
if (major < 21) {
return major == 11 || major == 17;
}
return major % 4 == 1;
}
private final JFXComboBox<DiscoJavaDistribution> distributionBox;
private final JFXComboBox<DiscoJavaRemoteVersion> remoteVersionBox;
private final JFXComboBox<JavaPackageType> packageTypeBox;
private final Label warningLabel = new Label();
private final JFXButton downloadButton;
private final StackPane downloadButtonPane = new StackPane();
private final DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider();
private final ObjectProperty<DiscoJavaVersionList> currentDiscoJavaVersionList = new SimpleObjectProperty<>();
private final Map<Pair<DiscoJavaDistribution, JavaPackageType>, DiscoJavaVersionList> javaVersionLists = new HashMap<>();
private boolean changingDistribution = false;
DownloadDiscoJava() {
assert !distributions.isEmpty();
this.distributionBox = new JFXComboBox<>();
this.distributionBox.setConverter(FXUtils.stringConverter(JavaDistribution::getDisplayName));
this.remoteVersionBox = new JFXComboBox<>();
this.remoteVersionBox.setConverter(FXUtils.stringConverter(JavaRemoteVersion::getDistributionVersion));
this.packageTypeBox = new JFXComboBox<>();
this.downloadButton = new JFXButton(i18n("download"));
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()));
cancelButton.getStyleClass().add("dialog-cancel");
onEscPressed(this, cancelButton::fire);
GridPane body = new GridPane();
body.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
body.setVgap(8);
body.setHgap(16);
body.addRow(0, new Label(i18n("java.download.distribution")), distributionBox);
body.addRow(1, new Label(i18n("java.download.version")), remoteVersionBox);
body.addRow(2, new Label(i18n("java.download.packageType")), packageTypeBox);
distributionBox.setItems(FXCollections.observableList(new ArrayList<>(distributions)));
ChangeListener<DiscoJavaVersionList.Status> updateStatusListener = (observable, oldValue, newValue) -> updateStatus(newValue);
this.currentDiscoJavaVersionList.addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.status.removeListener(updateStatusListener);
}
if (newValue != null) {
newValue.status.addListener(updateStatusListener);
updateStatus(newValue.status.get());
} else {
updateStatus(null);
}
});
packageTypeBox.getSelectionModel().selectedItemProperty().addListener(ignored -> updateVersions());
FXUtils.onChangeAndOperate(distributionBox.getSelectionModel().selectedItemProperty(), distribution -> {
if (distribution != null) {
changingDistribution = true;
packageTypeBox.setItems(FXCollections.observableList(new ArrayList<>(distribution.getSupportedPackageTypes())));
packageTypeBox.getSelectionModel().select(0);
changingDistribution = false;
updateVersions();
packageTypeBox.setDisable(false);
remoteVersionBox.setDisable(false);
} else {
packageTypeBox.setItems(null);
updateVersions();
remoteVersionBox.setItems(null);
packageTypeBox.setDisable(true);
remoteVersionBox.setDisable(true);
}
});
setHeading(new Label(i18n("java.download")));
setBody(body);
setActions(warningLabel, downloadButtonPane, cancelButton);
}
private void updateStatus(DiscoJavaVersionList.Status status) {
if (status == DiscoJavaVersionList.Status.LOADING) {
downloadButtonPane.getChildren().setAll(new JFXSpinner());
remoteVersionBox.setDisable(true);
warningLabel.setText(null);
} else {
downloadButtonPane.getChildren().setAll(downloadButton);
if (status == DiscoJavaVersionList.Status.SUCCESS || status == null) {
remoteVersionBox.setDisable(false);
warningLabel.setText(null);
} else if (status == DiscoJavaVersionList.Status.FAILED) {
remoteVersionBox.setDisable(true);
warningLabel.setText(i18n("java.download.load_list.failed"));
}
}
}
private void onDownload() {
fireEvent(new DialogCloseEvent());
DiscoJavaDistribution distribution = distributionBox.getSelectionModel().getSelectedItem();
DiscoJavaRemoteVersion version = remoteVersionBox.getSelectionModel().getSelectedItem();
if (version == null)
return;
Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().getPkgInfoUri()))
.setExecutor(Schedulers.io())
.thenComposeAsync(json -> {
DiscoResult<DiscoRemoteFileInfo> result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoRemoteFileInfo.class));
if (result.getResult().size() != 1)
throw new IOException("Illegal result: " + json);
DiscoRemoteFileInfo fileInfo = result.getResult().get(0);
if (!fileInfo.getChecksumType().equals("sha1") && !fileInfo.getChecksumType().equals("sha256"))
throw new IOException("Unsupported checksum type: " + fileInfo.getChecksumType());
if (StringUtils.isBlank(fileInfo.getDirectDownloadUri()))
throw new IOException("Missing download URI: " + json);
File targetFile = File.createTempFile("hmcl-java-", "." + version.getArchiveType());
targetFile.deleteOnExit();
Task<FileDownloadTask.IntegrityCheck> getIntegrityCheck;
if (StringUtils.isNotBlank(fileInfo.getChecksum()))
getIntegrityCheck = Task.completed(new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), fileInfo.getChecksum()));
else if (StringUtils.isNotBlank(fileInfo.getChecksumUri()))
getIntegrityCheck = new GetTask(downloadProvider.injectURLWithCandidates(fileInfo.getChecksumUri()))
.thenApplyAsync(checksum ->
new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), checksum.trim()));
else
throw new IOException("Unable to get checksum for file");
return getIntegrityCheck
.thenComposeAsync(integrityCheck ->
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
targetFile, integrityCheck).setName(fileInfo.getFileName()))
.thenSupplyAsync(targetFile::toPath);
})
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
String javaVersion = version.getJavaVersion();
JavaInfo info = new JavaInfo(platform, javaVersion, distribution.getVendor());
Map<String, Object> updateInfo = new LinkedHashMap<>();
updateInfo.put("type", "disco");
updateInfo.put("info", version);
int idx = javaVersion.indexOf('+');
if (idx > 0) {
javaVersion = javaVersion.substring(0, idx);
}
String defaultName = distribution.getApiParameter() + "-" + javaVersion;
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller ->
new JavaInstallPage(controller::onFinish, info, version, updateInfo, defaultName, result)));
} else {
LOG.warning("Failed to download java", exception);
Throwable resolvedException = resolveException(exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
}
})), i18n("java.download"), TaskCancellationAction.NORMAL);
}
private void updateVersions() {
if (changingDistribution) return;
DiscoJavaDistribution distribution = distributionBox.getSelectionModel().getSelectedItem();
if (distribution == null) {
this.currentDiscoJavaVersionList.set(null);
return;
}
JavaPackageType packageType = packageTypeBox.getSelectionModel().getSelectedItem();
DiscoJavaVersionList list = javaVersionLists.computeIfAbsent(Pair.pair(distribution, packageType), pair -> {
DiscoJavaVersionList res = new DiscoJavaVersionList();
new DiscoFetchJavaListTask(downloadProvider, distribution, platform, packageType).setExecutor(Schedulers.io()).thenApplyAsync(versions -> {
if (versions.isEmpty()) return Collections.<DiscoJavaRemoteVersion>emptyList();
int lastLTS = -1;
for (int v : versions.keySet()) {
if (isLTS(v)) {
lastLTS = v;
}
}
ArrayList<DiscoJavaRemoteVersion> remoteVersions = new ArrayList<>();
for (Map.Entry<Integer, DiscoJavaRemoteVersion> entry : versions.entrySet()) {
int v = entry.getKey();
if (v >= lastLTS || isLTS(v) || v == 16) {
remoteVersions.add(entry.getValue());
}
}
Collections.reverse(remoteVersions);
return remoteVersions;
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
res.status.set(DiscoJavaVersionList.Status.SUCCESS);
res.versions.setAll(result);
selectLTS(res);
} else {
LOG.warning("Failed to load java list", exception);
res.status.set(DiscoJavaVersionList.Status.FAILED);
}
})).start();
return res;
});
this.currentDiscoJavaVersionList.set(list);
this.remoteVersionBox.setItems(list.versions);
selectLTS(list);
}
private void selectLTS(DiscoJavaVersionList list) {
if (remoteVersionBox.getItems() == list.versions) {
for (int i = 0; i < list.versions.size(); i++) {
JavaRemoteVersion item = list.versions.get(i);
if (item.getJdkVersion() == GameJavaVersion.LATEST.getMajorVersion()) {
remoteVersionBox.getSelectionModel().select(i);
break;
}
}
}
}
}
private static final class DiscoJavaVersionList {
enum Status {
LOADING, SUCCESS, FAILED
}
final ObservableList<DiscoJavaRemoteVersion> versions = FXCollections.observableArrayList();
final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
}
}
@@ -0,0 +1,190 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
import org.jackhuang.hmcl.download.java.disco.DiscoJavaDistribution;
import org.jackhuang.hmcl.download.java.disco.DiscoJavaRemoteVersion;
import org.jackhuang.hmcl.java.HMCLJavaRepository;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.wizard.WizardSinglePage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
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 JavaInstallPage extends WizardSinglePage {
private static final Pattern NAME_PATTERN = Pattern.compile("[a-zA-Z0-9.\\-_]+");
private final Path file;
private final JavaInfo info;
private final JavaRemoteVersion remoteVersion;
private final Map<String, Object> update;
private final StringProperty nameProperty = new SimpleStringProperty();
public JavaInstallPage(Runnable onFinish, JavaInfo info, JavaRemoteVersion remoteVersion, Map<String, Object> update, String defaultName, Path file) {
super(onFinish);
this.info = info;
this.remoteVersion = remoteVersion;
this.update = update;
this.file = file;
this.nameProperty.set(defaultName);
}
@Override
protected SkinBase<?> createDefaultSkin() {
return new Skin(this);
}
@Override
protected Object finish() {
Task<JavaRuntime> installTask = JavaManager.getInstallJavaTask(info.getPlatform(), nameProperty.get(), update, file);
return remoteVersion == null ? installTask : installTask.whenComplete(exception -> {
try {
Files.delete(file);
} catch (IOException e) {
LOG.warning("Failed to delete file: " + file, e);
}
});
}
@Override
public String getTitle() {
return i18n("java.install");
}
private static final class Skin extends SkinBase<JavaInstallPage> {
private final ComponentList componentList = new ComponentList();
private final JFXTextField nameField;
private final Set<String> usedNames = new HashSet<>();
Skin(JavaInstallPage control) {
super(control);
VBox borderPane = new VBox();
borderPane.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(borderPane, 500);
{
BorderPane namePane = new BorderPane();
{
Label label = new Label(i18n("java.install.name"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
namePane.setLeft(label);
nameField = new JFXTextField();
nameField.textProperty().bindBidirectional(control.nameProperty);
FXUtils.setLimitWidth(nameField, 200);
BorderPane.setAlignment(nameField, Pos.CENTER_RIGHT);
BorderPane.setMargin(nameField, new Insets(0, 0, 12, 0));
namePane.setRight(nameField);
nameField.setValidators(
new RequiredValidator(),
new Validator(i18n("java.install.warning.invalid_character"),
text -> !text.startsWith(HMCLJavaRepository.MOJANG_JAVA_PREFIX) && NAME_PATTERN.matcher(text).matches()),
new Validator(i18n("java.install.failed.exists"), text -> !usedNames.contains(text))
);
String defaultName = control.nameProperty.get();
if (JavaManager.REPOSITORY.isInstalled(control.info.getPlatform(), defaultName)) {
usedNames.add(defaultName);
}
nameField.textProperty().addListener(o -> nameField.validate());
nameField.validate();
componentList.getContent().add(namePane);
}
String vendor = JavaInfo.normalizeVendor(control.info.getVendor());
if (vendor != null)
addInfo(i18n("java.info.vendor"), vendor);
if (control.remoteVersion instanceof DiscoJavaRemoteVersion) {
String distributionName = ((DiscoJavaRemoteVersion) control.remoteVersion).getDistribution();
DiscoJavaDistribution distribution = DiscoJavaDistribution.of(distributionName);
addInfo(i18n("java.info.disco.distribution"), distribution != null ? distribution.getDisplayName() : distributionName);
} else
addInfo(i18n("java.install.archive"), control.file.toAbsolutePath().toString());
addInfo(i18n("java.info.version"), control.info.getVersion());
addInfo(i18n("java.info.architecture"), control.info.getPlatform().getArchitecture().getDisplayName());
BorderPane installPane = new BorderPane();
{
JFXButton installButton = FXUtils.newRaisedButton(i18n("button.install"));
installButton.setOnAction(e -> {
String name = control.nameProperty.get();
if (JavaManager.REPOSITORY.isInstalled(control.info.getPlatform(), name)) {
Controllers.dialog(i18n("java.install.failed.exists"), null, MessageDialogPane.MessageType.WARNING);
usedNames.add(name);
nameField.validate();
} else
control.onFinish.run();
});
installButton.disableProperty().bind(nameField.activeValidatorProperty().isNotNull());
installPane.setRight(installButton);
componentList.getContent().add(installPane);
}
}
borderPane.getChildren().setAll(componentList);
this.getChildren().setAll(borderPane);
}
private void addInfo(String name, String value) {
BorderPane pane = new BorderPane();
pane.setLeft(new Label(name));
Label valueLabel = FXUtils.truncatedLabel(value, 60);
BorderPane.setAlignment(valueLabel, Pos.CENTER_RIGHT);
pane.setCenter(valueLabel);
this.componentList.getContent().add(pane);
}
}
}
@@ -0,0 +1,322 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.java.JavaInfo;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.tree.TarFileTree;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaManagementPage extends ListPageBase<JavaManagementPage.JavaItem> {
@SuppressWarnings("FieldCanBeLocal")
private final ChangeListener<Collection<JavaRuntime>> listener;
private final Runnable onInstallJava;
public JavaManagementPage() {
this.listener = FXUtils.onWeakChangeAndOperate(JavaManager.getAllJavaProperty(), this::loadJava);
if (Platform.SYSTEM_PLATFORM.equals(OperatingSystem.LINUX, Architecture.LOONGARCH64_OW)) {
onInstallJava = () -> FXUtils.openLink("https://www.loongnix.cn/zh/api/java/");
} else {
onInstallJava = JavaDownloadDialog.showDialogAction(DownloadProviders.getDownloadProvider());
}
FXUtils.applyDragListener(this, it -> {
String name = it.getName();
return it.isDirectory() || name.endsWith(".zip") || name.endsWith(".tar.gz") || name.equals(OperatingSystem.CURRENT_OS.getJavaExecutable());
}, files -> {
for (File file : files) {
if (file.isDirectory()) {
onAddJavaHome(file.toPath());
} else {
String fileName = file.getName();
if (fileName.equals(OperatingSystem.CURRENT_OS.getJavaExecutable())) {
onAddJavaBinary(file.toPath());
} else if (fileName.endsWith(".zip") || fileName.endsWith(".tar.gz")) {
onInstallArchive(file.toPath());
} else {
throw new AssertionError("Unreachable code");
}
}
}
});
}
@Override
protected Skin<?> createDefaultSkin() {
return new JavaPageSkin(this);
}
void onAddJava() {
FileChooser chooser = new FileChooser();
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
chooser.setTitle(i18n("settings.game.java_directory.choose"));
File file = chooser.showOpenDialog(Controllers.getStage());
if (file != null) {
JavaManager.getAddJavaTask(file.toPath()).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
}
void onShowRestoreJavaPage() {
Controllers.navigate(new JavaRestorePage(ConfigHolder.globalConfig().getDisabledJava()));
}
private void onAddJavaBinary(Path file) {
JavaManager.getAddJavaTask(file).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
private void onAddJavaHome(Path file) {
Task.composeAsync(() -> {
Path releaseFile = file.resolve("release");
if (Files.notExists(releaseFile))
throw new IOException("Missing release file " + releaseFile);
return JavaManager.getAddJavaTask(file.resolve("bin").resolve(OperatingSystem.CURRENT_OS.getJavaExecutable()));
}).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
private void onInstallArchive(Path file) {
Task.supplyAsync(() -> {
try (ArchiveFileTree<?, ?> tree = TarFileTree.open(file)) {
JavaInfo info = JavaInfo.fromArchive(tree);
if (!JavaManager.isCompatible(info.getPlatform()))
throw new UnsupportedPlatformException(info.getPlatform().toString());
return Pair.pair(tree.getRoot().getSubDirs().keySet().iterator().next(), info);
}
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller ->
new JavaInstallPage(controller::onFinish, result.getValue(), null, null, result.getKey(), file)));
} else {
if (exception instanceof UnsupportedPlatformException) {
Controllers.dialog(i18n("java.install.failed.unsupported_platform"), null, MessageDialogPane.MessageType.WARNING);
} else {
Controllers.dialog(i18n("java.install.failed.invalid"), null, MessageDialogPane.MessageType.WARNING);
}
}
}).start();
}
// FXThread
private void loadJava(Collection<JavaRuntime> javaRuntimes) {
if (javaRuntimes != null) {
List<JavaItem> items = new ArrayList<>();
for (JavaRuntime java : javaRuntimes) {
items.add(new JavaItem(java));
}
this.setItems(FXCollections.observableList(items));
this.setLoading(false);
} else
this.setLoading(true);
}
static final class JavaItem extends Control {
private final JavaRuntime java;
public JavaItem(JavaRuntime java) {
this.java = java;
}
public JavaRuntime getJava() {
return java;
}
public void onReveal() {
Path target;
Path parent = java.getBinary().getParent();
if (parent != null
&& parent.getParent() != null
&& parent.getFileName() != null
&& parent.getFileName().toString().equals("bin")
&& Files.exists(parent.getParent().resolve("release"))) {
target = parent.getParent();
} else {
target = java.getBinary();
}
FXUtils.showFileInExplorer(target);
}
public void onRemove() {
if (java.isManaged()) {
Controllers.taskDialog(JavaManager.getUninstallJavaTask(java), i18n("java.uninstall"), TaskCancellationAction.NORMAL);
} else {
String path = java.getBinary().toString();
ConfigHolder.globalConfig().getUserJava().remove(path);
ConfigHolder.globalConfig().getDisabledJava().add(path);
try {
JavaManager.removeJava(java);
} catch (InterruptedException ignored) {
}
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new JavaRuntimeItemSkin(this);
}
}
private static final class JavaRuntimeItemSkin extends SkinBase<JavaItem> {
JavaRuntimeItemSkin(JavaItem control) {
super(control);
JavaRuntime java = control.getJava();
String vendor = JavaInfo.normalizeVendor(java.getVendor());
BorderPane root = new BorderPane();
HBox center = new HBox();
center.setMouseTransparent(true);
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
TwoLineListItem item = new TwoLineListItem();
item.setTitle((java.isJDK() ? "JDK" : "JRE") + " " + java.getVersion());
item.setSubtitle(java.getBinary().toString());
item.getTags().add(i18n("java.info.architecture") + ": " + java.getArchitecture().getDisplayName());
if (vendor != null)
item.getTags().add(i18n("java.info.vendor") + ": " + vendor);
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(item);
root.setCenter(center);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = new JFXButton();
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
revealButton.setOnAction(e -> control.onReveal());
FXUtils.installFastTooltip(revealButton, i18n("java.reveal"));
JFXButton removeButton = new JFXButton();
removeButton.getStyleClass().add("toggle-icon4");
removeButton.setOnAction(e -> Controllers.confirm(
java.isManaged() ? i18n("java.uninstall.confirm") : i18n("java.disable.confirm"),
i18n("message.warning"),
control::onRemove,
null
));
if (java.isManaged()) {
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
FXUtils.installFastTooltip(removeButton, i18n("java.uninstall"));
if (JavaRuntime.CURRENT_JAVA != null && java.getBinary().equals(JavaRuntime.CURRENT_JAVA.getBinary()))
removeButton.setDisable(true);
} else {
removeButton.setGraphic(FXUtils.limitingSize(SVG.CLOSE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
FXUtils.installFastTooltip(removeButton, i18n("java.disable"));
}
right.getChildren().setAll(revealButton, removeButton);
}
root.setRight(right);
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
getChildren().setAll(new RipplerContainer(root));
}
}
private static final class JavaPageSkin extends ToolbarListPageSkin<JavaManagementPage> {
JavaPageSkin(JavaManagementPage skinnable) {
super(skinnable);
}
@Override
protected List<Node> initializeToolbar(JavaManagementPage skinnable) {
ArrayList<Node> res = new ArrayList<>(4);
res.add(createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, JavaManager::refresh));
if (skinnable.onInstallJava != null) {
res.add(createToolbarButton2(i18n("java.download"), SVG.DOWNLOAD_OUTLINE, skinnable.onInstallJava));
}
res.add(createToolbarButton2(i18n("java.add"), SVG.PLUS, skinnable::onAddJava));
JFXButton disableJava = createToolbarButton2(i18n("java.disabled.management"), SVG.VIEW_LIST, skinnable::onShowRestoreJavaPage);
disableJava.disableProperty().bind(Bindings.isEmpty(ConfigHolder.globalConfig().getDisabledJava()));
res.add(disableJava);
return res;
}
}
}
@@ -0,0 +1,206 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class JavaRestorePage extends ListPageBase<JavaRestorePage.DisabledJavaItem> implements DecoratorPage {
private final ObjectProperty<State> state = new SimpleObjectProperty<>(State.fromTitle(i18n("java.disabled.management")));
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener listener;
public JavaRestorePage(ObservableSet<String> disabledJava) {
this.listener = o -> {
ArrayList<DisabledJavaItem> result = new ArrayList<>(disabledJava.size());
for (String path : disabledJava) {
Path realPath = null;
try {
realPath = Paths.get(path).toRealPath();
} catch (IOException ignored) {
}
result.add(new DisabledJavaItem(disabledJava, path, realPath));
}
result.sort((a, b) -> {
if (a.realPath == null && b.realPath != null)
return -1;
if (a.realPath != null && b.realPath == null)
return 1;
return a.path.compareTo(b.path);
});
this.setItems(FXCollections.observableList(result));
};
disabledJava.addListener(new WeakInvalidationListener(listener));
listener.invalidated(disabledJava);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JavaRestorePageSkin(this);
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
static final class DisabledJavaItem extends Control {
final ObservableSet<String> disabledJava;
final String path;
final Path realPath;
DisabledJavaItem(ObservableSet<String> disabledJava, String path, Path realPath) {
this.disabledJava = disabledJava;
this.path = path;
this.realPath = realPath;
}
@Override
protected Skin<?> createDefaultSkin() {
return new DisabledJavaItemSkin(this);
}
void onReveal() {
if (realPath != null) {
Path target;
Path parent = realPath.getParent();
if (parent != null
&& parent.getParent() != null
&& parent.getFileName() != null
&& parent.getFileName().toString().equals("bin")
&& Files.exists(parent.getParent().resolve("release"))) {
target = parent.getParent();
} else {
target = realPath;
}
FXUtils.showFileInExplorer(target);
}
}
void onRestore() {
disabledJava.remove(path);
JavaManager.getAddJavaTask(realPath).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to add java", exception);
Controllers.dialog(i18n("java.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
void onRemove() {
disabledJava.remove(path);
}
}
private static final class DisabledJavaItemSkin extends SkinBase<DisabledJavaItem> {
DisabledJavaItemSkin(DisabledJavaItem skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
Label label = new Label(skinnable.path);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
root.setCenter(label);
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
{
JFXButton revealButton = new JFXButton();
revealButton.getStyleClass().add("toggle-icon4");
revealButton.setGraphic(FXUtils.limitingSize(SVG.FOLDER_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
revealButton.setOnAction(e -> skinnable.onReveal());
FXUtils.installFastTooltip(revealButton, i18n("java.reveal"));
if (skinnable.realPath == null) {
revealButton.setDisable(true);
JFXButton removeButton = new JFXButton();
removeButton.getStyleClass().add("toggle-icon4");
removeButton.setGraphic(FXUtils.limitingSize(SVG.DELETE_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
removeButton.setOnAction(e -> skinnable.onRemove());
FXUtils.installFastTooltip(removeButton, i18n("java.disabled.management.remove"));
right.getChildren().setAll(revealButton, removeButton);
} else {
JFXButton restoreButton = new JFXButton();
restoreButton.getStyleClass().add("toggle-icon4");
restoreButton.setGraphic(FXUtils.limitingSize(SVG.RESTORE.createIcon(Theme.blackFill(), 24, 24), 24, 24));
restoreButton.setOnAction(e -> skinnable.onRestore());
FXUtils.installFastTooltip(restoreButton, i18n("java.disabled.management.restore"));
right.getChildren().setAll(revealButton, restoreButton);
}
}
root.setRight(right);
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
this.getChildren().setAll(new RipplerContainer(root));
}
}
private static final class JavaRestorePageSkin extends ToolbarListPageSkin<JavaRestorePage> {
JavaRestorePageSkin(JavaRestorePage skinnable) {
super(skinnable);
}
@Override
protected List<Node> initializeToolbar(JavaRestorePage skinnable) {
return Collections.emptyList();
}
}
}
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.PageAware;
import org.jackhuang.hmcl.ui.construct.TabControl;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@@ -40,6 +41,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("settings")));
private final TabHeader tab;
private final TabHeader.Tab<VersionSettingsPage> gameTab = new TabHeader.Tab<>("versionSettingsPage");
private final TabControl.Tab<JavaManagementPage> javaManagementTab = new TabControl.Tab<>("javaManagementPage");
private final TabHeader.Tab<SettingsPage> settingsTab = new TabHeader.Tab<>("settingsPage");
private final TabHeader.Tab<PersonalizationPage> personalizationTab = new TabHeader.Tab<>("personalizationPage");
private final TabHeader.Tab<DownloadSettingsPage> downloadTab = new TabHeader.Tab<>("downloadSettingsPage");
@@ -50,13 +52,14 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
public LauncherSettingsPage() {
gameTab.setNodeSupplier(() -> new VersionSettingsPage(true));
javaManagementTab.setNodeSupplier(JavaManagementPage::new);
settingsTab.setNodeSupplier(SettingsPage::new);
personalizationTab.setNodeSupplier(PersonalizationPage::new);
downloadTab.setNodeSupplier(DownloadSettingsPage::new);
helpTab.setNodeSupplier(HelpPage::new);
feedbackTab.setNodeSupplier(FeedbackPage::new);
aboutTab.setNodeSupplier(AboutPage::new);
tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
tab = new TabHeader(gameTab, javaManagementTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
tab.select(gameTab);
gameTab.initializeIfNeeded();
@@ -74,6 +77,12 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
runInFX(() -> FXUtils.installFastTooltip(settingsItem, i18n("settings.type.global.manage")));
settingsItem.setOnAction(e -> tab.select(gameTab));
})
.addNavigationDrawerItem(javaItem -> {
javaItem.setTitle(i18n("java.management"));
javaItem.setLeftGraphic(wrap(SVG.WRENCH_OUTLINE));
javaItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(javaManagementTab));
javaItem.setOnAction(e -> tab.select(javaManagementTab));
})
.startCategory(i18n("launcher"))
.addNavigationDrawerItem(settingsItem -> {
settingsItem.setTitle(i18n("settings.launcher.general"));
@@ -237,10 +237,13 @@ public final class MainPage extends StackPane implements DecoratorPage {
doAnimation(show);
if (show && getLatestVersion() != null && !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) {
Controllers.dialog("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO, () -> {
config().setPromptedVersion(getLatestVersion().getVersion());
onUpgrade();
});
Controllers.dialog(new MessageDialogPane.Builder("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO)
.addAction(i18n("button.view"), () -> {
config().setPromptedVersion(getLatestVersion().getVersion());
onUpgrade();
})
.addCancel(null)
.build());
}
}
@@ -54,12 +54,11 @@ import java.time.Instant;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
@@ -102,7 +101,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
try {
Controllers.navigate(new NBTEditorPage(file));
} catch (Throwable e) {
LOG.log(Level.WARNING, "Fail to open nbt file", e);
LOG.warning("Fail to open nbt file", e);
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
@@ -31,24 +31,23 @@ import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
@@ -130,11 +129,11 @@ public final class SettingsPage extends SettingsView {
Path logFile = Paths.get("hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
LOG.info("Exporting logs to " + logFile);
try {
Files.write(logFile, Logging.getRawLogs());
try (OutputStream output = Files.newOutputStream(logFile)) {
LOG.exportLogs(output);
} catch (IOException e) {
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
LOG.log(Level.WARNING, "Failed to export logs", e);
LOG.warning("Failed to export logs", e);
return;
}
@@ -30,6 +30,7 @@ import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.scene.text.TextAlignment;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -177,7 +178,7 @@ public abstract class SettingsView extends StackPane {
languagePane.setLeft(left);
cboLanguage = new JFXComboBox<>();
cboLanguage.setConverter(stringConverter(locale -> locale.getName(I18n.getCurrentLocale().getResourceBundle())));
cboLanguage.setConverter(stringConverter(I18n::getName));
FXUtils.setLimitWidth(cboLanguage, 300);
languagePane.setRight(cboLanguage);
@@ -191,10 +192,19 @@ public abstract class SettingsView extends StackPane {
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
debugPane.setLeft(left);
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
openLogFolderButton.setOnMouseClicked(e -> openLogFolder());
openLogFolderButton.getStyleClass().add("jfx-button-border");
JFXButton logButton = new JFXButton(i18n("settings.launcher.launcher_log.export"));
logButton.setOnMouseClicked(e -> onExportLogs());
logButton.getStyleClass().add("jfx-button-border");
debugPane.setRight(logButton);
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, logButton);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
settingsPane.getContent().add(debugPane);
}
@@ -205,6 +215,10 @@ public abstract class SettingsView extends StackPane {
}
}
public void openLogFolder() {
FXUtils.openFolder(Metadata.HMCL_DIRECTORY.resolve("logs").toFile());
}
protected abstract void onUpdate();
protected abstract void onExportLogs();
@@ -18,10 +18,9 @@ import org.jackhuang.hmcl.util.StringUtils;
import java.io.*;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class NBTEditorPage extends BorderPane implements DecoratorPage {
@@ -53,7 +52,7 @@ public class NBTEditorPage extends BorderPane implements DecoratorPage {
try {
save();
} catch (IOException ex) {
LOG.log(Level.WARNING, "Failed to save NBT file", ex);
LOG.warning("Failed to save NBT file", ex);
Controllers.dialog(i18n("nbt.save.failed") + "\n\n" + StringUtils.getStackTrace(ex));
}
});
@@ -73,7 +72,7 @@ public class NBTEditorPage extends BorderPane implements DecoratorPage {
}, Schedulers.javafx())
.handleAsync((result, e) -> {
if (e != null) {
LOG.log(Level.WARNING, "Fail to open nbt file", e);
LOG.warning("Fail to open nbt file", e);
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire);
}
return null;
@@ -29,7 +29,6 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -38,8 +37,8 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements DecoratorPage {
@@ -71,7 +70,7 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
zip.loadFromZip();
zip.installTo(worldDir);
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + datapack, e);
LOG.warning("Unable to parse datapack file " + datapack, e);
}
}
@@ -117,7 +116,7 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
datapack.deletePack(pack);
} catch (IOException e) {
// Fail to remove mods if the game is running or the datapack is absent.
Logging.LOG.warning("Failed to delete datapack " + pack);
LOG.warning("Failed to delete datapack " + pack);
}
});
}
@@ -43,7 +43,6 @@ import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
@@ -52,6 +51,7 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.AggregatedObservableList;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
@@ -64,10 +64,10 @@ import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
@@ -87,7 +87,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
protected final ListProperty<String> downloadSources = new SimpleListProperty<>(this, "downloadSources", FXCollections.observableArrayList());
protected final StringProperty downloadSource = new SimpleStringProperty();
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private TaskExecutor executor;
private int searchID = 0;
protected RemoteModRepository repository;
private Runnable retrySearch;
@@ -163,11 +163,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
setLoading(true);
setFailed(false);
if (executor != null && !executor.isCancelled()) {
executor.cancel();
}
executor = Task.supplyAsync(() -> {
int currentSearchID = searchID = searchID + 1;
Task.supplyAsync(() -> {
Profile.ProfileVersion version = this.version.get();
if (StringUtils.isBlank(version.getVersion())) {
return userGameVersion;
@@ -176,9 +173,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
? version.getProfile().getRepository().getGameVersion(version.getVersion()).orElse("")
: "";
}
}).thenApplyAsync(gameVersion ->
repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)
).whenComplete(Schedulers.javafx(), (result, exception) -> {
}).thenApplyAsync(gameVersion -> repository.search(gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteModRepository.SortOrder.DESC)).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (searchID != currentSearchID) {
return;
}
setLoading(false);
if (exception == null) {
items.setAll(result.getResults().collect(Collectors.toList()));
@@ -196,7 +195,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
return i18n("curse.category." + category);
}
protected String getLocalizedCategoryIndent(ModDownloadListPageSkin.CategoryIndented category) {
private String getLocalizedCategoryIndent(ModDownloadListPageSkin.CategoryIndented category) {
return StringUtils.repeats(' ', category.indent * 4) +
(category.getCategory() == null
? i18n("curse.category.0")
@@ -344,13 +343,14 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
sortComboBox.getSelectionModel().select(0);
searchPane.addRow(rowIndex++, new Label(i18n("mods.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane);
StringProperty previousSearchFilter = new SimpleStringProperty(this, "Previous Seach Filter", "");
IntegerProperty filterID = new SimpleIntegerProperty(this, "Filter ID", 0);
IntegerProperty currentFilterID = new SimpleIntegerProperty(this, "Current Filter ID", -1);
EventHandler<ActionEvent> searchAction = e -> {
if (!previousSearchFilter.get().equals(nameField.getText())) {
if (currentFilterID.get() != filterID.get()) {
control.pageOffset.set(0);
}
currentFilterID.set(filterID.get());
previousSearchFilter.set(nameField.getText());
getSkinnable().search(gameVersionField.getSelectionModel().getSelectedItem(),
Optional.ofNullable(categoryComboBox.getSelectionModel().getSelectedItem())
.map(CategoryIndented::getCategory)
@@ -360,60 +360,93 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
sortComboBox.getSelectionModel().getSelectedItem());
};
control.listenerHolder.add(FXUtils.observeWeak(
() -> filterID.set(filterID.get() + 1),
control.downloadSource,
gameVersionField.getSelectionModel().selectedItemProperty(),
categoryComboBox.getSelectionModel().selectedItemProperty(),
nameField.textProperty(),
sortComboBox.getSelectionModel().selectedItemProperty()
));
HBox actionsBox = new HBox(8);
GridPane.setColumnSpan(actionsBox, 4);
actionsBox.setAlignment(Pos.CENTER);
{
AggregatedObservableList<Node> actions = new AggregatedObservableList<>();
Holder<Runnable> changeButton = new Holder<>();
JFXButton firstPageButton = FXUtils.newBorderButton(i18n("search.first_page"));
firstPageButton.setOnAction(event -> {
control.pageOffset.set(0);
changeButton.value.run();
searchAction.handle(event);
});
firstPageButton.setDisable(true);
control.pageCount.addListener((observable, oldValue, newValue) -> firstPageButton.setDisable(control.pageCount.get() == -1));
JFXButton previousPageButton = FXUtils.newBorderButton(i18n("search.previous_page"));
previousPageButton.setOnAction(event -> {
if (control.pageOffset.get() > 0) {
control.pageOffset.set(control.pageOffset.get() - 1);
int pageOffset = control.pageOffset.get();
if (pageOffset > 0) {
control.pageOffset.set(pageOffset - 1);
changeButton.value.run();
searchAction.handle(event);
}
});
previousPageButton.setDisable(true);
control.pageOffset.addListener((observable, oldValue, newValue) -> previousPageButton.setDisable(
control.pageCount.get() == -1 || control.pageOffset.get() == 0
));
Label pageOffset = new Label(i18n("search.page_n", 0, "-"));
control.pageOffset.addListener((observable, oldValue, newValue) -> pageOffset.setText(i18n(
"search.page_n", control.pageOffset.get() + 1, control.pageCount.get() == -1 ? "-" : control.pageCount.getValue().toString()
)));
control.pageCount.addListener((observable, oldValue, newValue) -> pageOffset.setText(i18n(
"search.page_n", control.pageOffset.get() + 1, control.pageCount.get() == -1 ? "-" : control.pageCount.getValue().toString()
)));
Label pageDescription = new Label();
pageDescription.textProperty().bind(Bindings.createStringBinding(() -> {
int pageCount = control.pageCount.get();
return i18n("search.page_n", control.pageOffset.get() + 1, pageCount == -1 ? "-" : String.valueOf(pageCount));
}, control.pageOffset, control.pageCount));
JFXButton nextPageButton = FXUtils.newBorderButton(i18n("search.next_page"));
nextPageButton.setOnAction(event -> {
control.pageOffset.set(control.pageOffset.get() + 1);
searchAction.handle(event);
int nv = control.pageOffset.get() + 1;
if (nv < control.pageCount.get()) {
control.pageOffset.set(nv);
changeButton.value.run();
searchAction.handle(event);
}
});
nextPageButton.setDisable(true);
control.pageOffset.addListener((observable, oldValue, newValue) -> nextPageButton.setDisable(
control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1
));
control.pageCount.addListener((observable, oldValue, newValue) -> nextPageButton.setDisable(
control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1
));
JFXButton lastPageButton = FXUtils.newBorderButton(i18n("search.last_page"));
lastPageButton.setOnAction(event -> {
control.pageOffset.set(control.pageCount.get() - 1);
changeButton.value.run();
searchAction.handle(event);
});
firstPageButton.setDisable(true);
previousPageButton.setDisable(true);
lastPageButton.setDisable(true);
control.pageCount.addListener((observable, oldValue, newValue) -> lastPageButton.setDisable(control.pageCount.get() == -1 || control.pageOffset.get() >= control.pageCount.get() - 1));
nextPageButton.setDisable(true);
changeButton.value = () -> {
int pageOffset = control.pageOffset.get();
int pageCount = control.pageCount.get();
boolean disablePrevious = pageOffset == 0;
firstPageButton.setDisable(disablePrevious);
previousPageButton.setDisable(disablePrevious);
boolean disableNext = pageOffset == pageCount - 1;
nextPageButton.setDisable(disableNext);
lastPageButton.setDisable(disableNext || pageCount == -1);
};
FXUtils.onChange(control.pageCount, pageCountN -> {
int pageCount = pageCountN.intValue();
if (pageCount != -1) {
if (control.pageOffset.get() + 1 >= pageCount) {
control.pageOffset.set(pageCount - 1);
}
}
changeButton.value.run();
});
Pane placeholder = new Pane();
HBox.setHgrow(placeholder, Priority.SOMETIMES);
@@ -421,13 +454,14 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
JFXButton searchButton = FXUtils.newRaisedButton(i18n("search"));
searchButton.setOnAction(searchAction);
actions.appendList(FXCollections.observableArrayList(firstPageButton, previousPageButton, pageOffset, nextPageButton, lastPageButton, placeholder, searchButton));
actions.appendList(FXCollections.observableArrayList(firstPageButton, previousPageButton, pageDescription, nextPageButton, lastPageButton, placeholder, searchButton));
actions.appendList(control.actions);
Bindings.bindContent(actionsBox.getChildren(), actions.getAggregatedList());
}
searchPane.addRow(rowIndex++, actionsBox);
FXUtils.onChange(control.downloadSource, v -> searchAction.handle(null));
nameField.setOnAction(searchAction);
gameVersionField.setOnAction(searchAction);
categoryComboBox.setOnAction(searchAction);
@@ -477,7 +511,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
protected void updateControl(RemoteMod dataItem, boolean empty) {
if (empty) return;
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(dataItem.getSlug());
content.setTitle(mod != null && I18n.getCurrentLocale().getLocale() == Locale.CHINA ? mod.getDisplayName() : dataItem.getTitle());
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : dataItem.getTitle());
content.setSubtitle(dataItem.getDescription());
content.getTags().setAll(dataItem.getCategories().stream()
.map(category -> getSkinnable().getLocalizedCategory(category))
@@ -94,22 +94,11 @@ public class DownloadPage extends Control implements DecoratorPage {
}
private void loadModVersions() {
File versionJar = StringUtils.isNotBlank(version.getVersion())
? version.getProfile().getRepository().getVersionJar(version.getVersion())
: null;
setLoading(true);
setFailed(false);
Task.supplyAsync(() -> {
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
// if (StringUtils.isNotBlank(version.getVersion())) {
// Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
// if (gameVersion.isPresent()) {
// return sortVersions(
// .filter(file -> file.getGameVersions().contains(gameVersion.get())));
// }
// }
return sortVersions(versions);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
@@ -240,7 +229,7 @@ public class DownloadPage extends Control implements DecoratorPage {
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
ModTranslations.Mod mod = getSkinnable().translations.getModByCurseForgeId(getSkinnable().addon.getSlug());
content.setTitle(mod != null && I18n.getCurrentLocale().getLocale() == Locale.CHINA ? mod.getDisplayName() : getSkinnable().addon.getTitle());
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : getSkinnable().addon.getTitle());
content.setSubtitle(getSkinnable().addon.getDescription());
content.getTags().setAll(getSkinnable().addon.getCategories().stream()
.map(category -> getSkinnable().page.getLocalizedCategory(category))
@@ -253,14 +242,6 @@ public class DownloadPage extends Control implements DecoratorPage {
descriptionPane.getChildren().add(openMcmodButton);
openMcmodButton.setMinWidth(Region.USE_PREF_SIZE);
runInFX(() -> FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod")));
// if (StringUtils.isNotBlank(getSkinnable().mod.getMcbbs())) {
// JFXHyperlink openMcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
// openMcbbsButton.setExternalLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs()));
// descriptionPane.getChildren().add(openMcbbsButton);
// openMcbbsButton.setMinWidth(Region.USE_PREF_SIZE);
// runInFX(() -> FXUtils.installFastTooltip(openMcbbsButton, i18n("mods.mcbbs")));
// }
}
JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage());
@@ -294,28 +275,44 @@ public class DownloadPage extends Control implements DecoratorPage {
if (control.version.getProfile() != null && control.version.getVersion() != null) {
HMCLGameRepository repository = control.version.getProfile().getRepository();
Version game = repository.getResolvedPreservingPatchesVersion(control.version.getVersion());
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(game, repository.getGameVersion(game).orElse(null));
libraryAnalyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT).ifPresent(currentGameVersion -> {
Set<ModLoaderType> currentGameModLoaders = libraryAnalyzer.getModLoaders();
if (control.versions.containsKey(currentGameVersion)) {
control.versions.get(currentGameVersion).stream()
.filter(version1 -> version1.getLoaders().isEmpty() || version1.getLoaders().stream().anyMatch(currentGameModLoaders::contains))
.findFirst()
.ifPresent(value -> list.getContent().addAll(
ComponentList.createComponentListTitle(i18n("mods.download.recommend", currentGameVersion)),
new ModItem(value, control)
));
String gameVersion = repository.getGameVersion(game).orElse(null);
if (gameVersion != null) {
List<RemoteMod.Version> modVersions = control.versions.get(gameVersion);
if (modVersions != null && !modVersions.isEmpty()) {
Set<ModLoaderType> targetLoaders = LibraryAnalyzer.analyze(game, gameVersion).getModLoaders();
resolve:
for (RemoteMod.Version modVersion : modVersions) {
for (ModLoaderType loader : modVersion.getLoaders()) {
if (targetLoaders.contains(loader)) {
list.getContent().addAll(
ComponentList.createComponentListTitle(i18n("mods.download.recommend", gameVersion)),
new ModItem(modVersion, control)
);
break resolve;
}
}
}
}
});
}
}
for (String gameVersion : control.versions.keys().stream()
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
.collect(Collectors.toList())) {
ComponentList sublist = new ComponentList(() ->
control.versions.get(gameVersion).stream()
.map(version -> new ModItem(version, control))
.collect(Collectors.toList()));
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
if (versions == null || versions.isEmpty()) {
continue;
}
ComponentList sublist = new ComponentList(() -> {
ArrayList<ModItem> items = new ArrayList<>(versions.size());
for (RemoteMod.Version v: versions) {
items.add(new ModItem(v, control));
}
return items;
});
sublist.getStyleClass().add("no-padding");
sublist.setTitle("Minecraft " + gameVersion);
@@ -352,15 +349,21 @@ public class DownloadPage extends Control implements DecoratorPage {
container.setOnMouseClicked(e -> Controllers.navigate(new DownloadPage(page, addon, version, callback)));
getChildren().setAll(container);
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
content.setTitle(mod != null && I18n.getCurrentLocale().getLocale() == Locale.CHINA ? mod.getDisplayName() : addon.getTitle());
content.setSubtitle(addon.getDescription());
content.getTags().setAll(addon.getCategories().stream()
.map(page::getLocalizedCategory)
.collect(Collectors.toList()));
if (addon != RemoteMod.BROKEN) {
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.getTitle());
content.setSubtitle(addon.getDescription());
content.getTags().setAll(addon.getCategories().stream()
.map(page::getLocalizedCategory)
.collect(Collectors.toList()));
if (StringUtils.isNotBlank(addon.getIconUrl())) {
imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true));
if (StringUtils.isNotBlank(addon.getIconUrl())) {
imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true));
}
} else {
content.setTitle(i18n("mods.broken_dependency.title"));
content.setSubtitle(i18n("mods.broken_dependency.desc"));
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@8x.png", 40, 40, true, true));
}
}
}
@@ -378,8 +381,6 @@ public class DownloadPage extends Control implements DecoratorPage {
{
StackPane graphicPane = new StackPane();
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
content.setTitle(dataItem.getName());
@@ -387,11 +388,16 @@ public class DownloadPage extends Control implements DecoratorPage {
switch (dataItem.getVersionType()) {
case Alpha:
content.getTags().add(i18n("version.game.snapshot"));
graphicPane.getChildren().setAll(SVG.ALPHA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
case Beta:
content.getTags().add(i18n("version.game.snapshot"));
graphicPane.getChildren().setAll(SVG.BETA_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
case Release:
content.getTags().add(i18n("version.game.release"));
graphicPane.getChildren().setAll(SVG.RELEASE_CIRCLE_OUTLINE.createIcon(Theme.blackFill(), 24, 24));
break;
}
@@ -454,7 +460,7 @@ public class DownloadPage extends Control implements DecoratorPage {
this.setBody(box);
JFXButton downloadButton = new JFXButton(i18n("download"));
JFXButton downloadButton = new JFXButton(i18n("mods.install"));
downloadButton.getStyleClass().add("dialog-accept");
downloadButton.setOnAction(e -> {
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
@@ -463,7 +469,7 @@ public class DownloadPage extends Control implements DecoratorPage {
selfPage.download(version);
});
JFXButton saveAsButton = new JFXButton(i18n("button.save_as"));
JFXButton saveAsButton = new JFXButton(i18n("mods.save_as"));
saveAsButton.getStyleClass().add("dialog-accept");
saveAsButton.setOnAction(e -> {
if (!spinnerPane.isLoading() && spinnerPane.getFailedReason() == null) {
@@ -35,12 +35,11 @@ import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT;
import static org.jackhuang.hmcl.util.Lang.handleUncaught;
import static org.jackhuang.hmcl.util.Lang.threadPool;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameItem extends Control {
@@ -84,7 +83,7 @@ public class GameItem extends Control {
if (config == null) return;
tag.set(config.getVersion());
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Failed to read modpack configuration from " + version, e);
LOG.warning("Failed to read modpack configuration from " + version, e);
}
}, Platform::runLater)
.exceptionally(handleUncaught);
@@ -0,0 +1,126 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.game.LocalizedRemoteModRepository;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.util.MissingResourceException;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.MOD, CurseForgeRemoteModRepository.MODS, ModrinthRemoteModRepository.MODS);
}
public static DownloadListPage ofCurseForgeMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.MOD, CurseForgeRemoteModRepository.MODS, null);
}
public static DownloadListPage ofModrinthMod(DownloadPage.DownloadCallback callback, boolean versionSelection) {
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.MOD, null, ModrinthRemoteModRepository.MODS);
}
public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.MODPACK, CurseForgeRemoteModRepository.MODPACKS, ModrinthRemoteModRepository.MODPACKS);
}
public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.RESOURCE_PACK, CurseForgeRemoteModRepository.RESOURCE_PACKS, ModrinthRemoteModRepository.RESOURCE_PACKS);
}
private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection, RemoteModRepository.Type type, CurseForgeRemoteModRepository curseForge, ModrinthRemoteModRepository modrinth) {
super(null, callback, versionSelection);
repository = new Repository(type, curseForge, modrinth);
supportChinese.set(true);
downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
if (curseForge != null) {
downloadSource.set("mods.curseforge");
} else if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else {
throw new AssertionError("Should not be here.");
}
}
private class Repository extends LocalizedRemoteModRepository {
private final RemoteModRepository.Type type;
private final CurseForgeRemoteModRepository curseForge;
private final ModrinthRemoteModRepository modrinth;
public Repository(Type type, CurseForgeRemoteModRepository curseForge, ModrinthRemoteModRepository modrinth) {
this.type = type;
this.curseForge = curseForge;
this.modrinth = modrinth;
}
@Override
protected RemoteModRepository getBackedRemoteModRepository() {
if ("mods.modrinth".equals(downloadSource.get())) {
return modrinth;
} else {
return curseForge;
}
}
@Override
protected SortType getBackedRemoteModRepositorySortOrder() {
if ("mods.modrinth".equals(downloadSource.get())) {
return SortType.NAME;
} else {
return SortType.POPULARITY;
}
}
@Override
public Type getType() {
return type;
}
}
@Override
protected String getLocalizedCategory(String category) {
if (category.isEmpty()) {
return "";
}
String key = ("mods.modrinth".equals(downloadSource.get()) ? "modrinth" : "curse") + ".category." + category;
try {
return I18n.getResourceBundle().getString(key);
} catch (MissingResourceException e) {
LOG.warning("Cannot find key " + key + " in resource bundle", e);
return category;
}
}
@Override
protected String getLocalizedOfficialPage() {
if ("mods.modrinth".equals(downloadSource.get())) {
return i18n("mods.modrinth");
} else {
return i18n("mods.curseforge");
}
}
}
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -39,7 +38,6 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -74,35 +72,42 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
return LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(versionId), gameVersion);
}).thenAcceptAsync(analyzer -> {
Function<String, Runnable> removeAction = libraryId -> () -> {
profile.getDependency().removeLibraryAsync(version, libraryId)
itemsProperty().clear();
InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup(gameVersion, InstallerItem.Style.LIST_ITEM);
// Conventional libraries: game, fabric, forge, neoforge, liteloader, optifine
for (InstallerItem item : group.getLibraries()) {
String libraryId = item.getLibraryId();
// Skip fabric-api and quilt-api
if (libraryId.contains("fabric-api") || libraryId.contains("quilt-api")) {
continue;
}
String libraryVersion = analyzer.getVersion(libraryId).orElse(null);
if (libraryVersion != null) {
item.versionProperty().set(new InstallerItem.InstalledState(
libraryVersion,
analyzer.getLibraryStatus(libraryId) != LibraryAnalyzer.LibraryMark.LibraryStatus.CLEAR,
false
));
} else {
item.versionProperty().set(null);
}
item.installActionProperty().set(e -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
});
item.removeActionProperty().set(e -> profile.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(profile.getRepository()::saveAsync)
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
.start();
};
.start());
itemsProperty().clear();
InstallerItem.InstallerItemGroup group = new InstallerItem.InstallerItemGroup(gameVersion);
// Conventional libraries: game, fabric, forge, neoforge, liteloader, optifine
for (InstallerItem installerItem : group.getLibraries()) {
String libraryId = installerItem.getLibraryId();
String libraryVersion = analyzer.getVersion(libraryId).orElse(null);
installerItem.libraryVersion.set(libraryVersion);
installerItem.upgradable.set(libraryVersion != null);
installerItem.installable.set(true);
installerItem.action.set(e -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
});
boolean removable = !LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId().equals(libraryId) && libraryVersion != null;
installerItem.removable.set(removable);
if (removable) {
Runnable action = removeAction.apply(libraryId);
installerItem.removeAction.set(e -> action.run());
}
itemsProperty().add(installerItem);
itemsProperty().add(item);
}
// other third-party libraries which are unable to manage.
@@ -114,21 +119,13 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
if (LibraryAnalyzer.LibraryType.fromPatchId(libraryId) != null)
continue;
Runnable action = removeAction.apply(libraryId);
InstallerItem installerItem = new InstallerItem(libraryId);
installerItem.libraryVersion.set(libraryVersion);
installerItem.installable.set(false);
installerItem.upgradable.bind(installerItem.installable);
installerItem.removable.set(true);
installerItem.removeAction.set(e -> action.run());
if (libraryVersion != null && Lang.test(() -> profile.getDependency().getVersionList(libraryId))) {
installerItem.installable.set(true);
installerItem.action.set(e -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
});
}
InstallerItem installerItem = new InstallerItem(libraryId, InstallerItem.Style.LIST_ITEM);
installerItem.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
installerItem.removeActionProperty().set(e -> profile.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(profile.getRepository()::saveAsync)
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
.start());
itemsProperty().add(installerItem);
}
@@ -175,7 +172,8 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
@Override
protected List<Node> initializeToolbar(InstallerListPage skinnable) {
return Collections.singletonList(
createToolbarButton2(i18n("install.installer.install_offline"), SVG.PLUS, skinnable::installOffline));
createToolbarButton2(i18n("install.installer.install_offline"), SVG.PLUS, skinnable::installOffline)
);
}
}
}
@@ -1,84 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.game.LocalizedRemoteModRepository;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModDownloadListPage extends DownloadListPage {
public ModDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection) {
super(null, callback, versionSelection);
repository = new Repository();
supportChinese.set(true);
downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
if (CurseForgeRemoteModRepository.isAvailable())
downloadSource.set("mods.curseforge");
else
downloadSource.set("mods.modrinth");
}
private class Repository extends LocalizedRemoteModRepository {
@Override
protected RemoteModRepository getBackedRemoteModRepository() {
if ("mods.modrinth".equals(downloadSource.get())) {
return ModrinthRemoteModRepository.MODS;
} else {
return CurseForgeRemoteModRepository.MODS;
}
}
@Override
protected SortType getBackedRemoteModRepositorySortOrder() {
if ("mods.modrinth".equals(downloadSource.get())) {
return SortType.NAME;
} else {
return SortType.POPULARITY;
}
}
@Override
public Type getType() {
return Type.MOD;
}
}
@Override
protected String getLocalizedCategory(String category) {
if ("mods.modrinth".equals(downloadSource.get())) {
return i18n("modrinth.category." + category);
} else {
return i18n("curse.category." + category);
}
}
@Override
protected String getLocalizedOfficialPage() {
if ("mods.modrinth".equals(downloadSource.get())) {
return i18n("mods.modrinth");
} else {
return i18n("mods.curseforge");
}
}
}
@@ -36,7 +36,6 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PageAware;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -45,10 +44,10 @@ import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.VersionLoadable, PageAware {
@@ -65,7 +64,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
try {
modManager.addMod(it.toPath());
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse mod file " + it, e);
LOG.warning("Unable to parse mod file " + it, e);
}
});
loadMods(modManager);
@@ -135,7 +134,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
modManager.addMod(file.toPath());
succeeded.add(file.getName());
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Unable to add mod " + file, e);
LOG.warning("Unable to add mod " + file, e);
failed.add(file.getName());
// Actually addMod will not throw exceptions because FileChooser has already filtered files.
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import com.jfoenix.controls.datamodels.treetable.RecursiveTreeObject;
import javafx.animation.PauseTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.collections.ListChangeListener;
@@ -33,7 +34,11 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.mod.*;
import javafx.util.Duration;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.setting.Profile;
@@ -46,32 +51,32 @@ 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.Holder;
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;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
import java.util.Optional;
import java.util.*;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.StringUtils.isNotBlank;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
class ModListPageSkin extends SkinBase<ModListPage> {
@@ -110,7 +115,12 @@ class ModListPageSkin extends SkinBase<ModListPage> {
searchField = new JFXTextField();
searchField.setPromptText(i18n("search"));
HBox.setHgrow(searchField, Priority.ALWAYS);
searchField.setOnAction(e -> search());
PauseTransition pause = new PauseTransition(Duration.millis(100));
pause.setOnFinished(e -> search());
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
pause.setRate(1);
pause.playFromStart();
});
JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE,
() -> {
@@ -218,7 +228,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
Pattern pattern = Pattern.compile(queryString.substring("regex:".length()));
predicate = s -> pattern.matcher(s).find();
} catch (Throwable e) {
LOG.log(Level.WARNING, "Illegal regular expression", e);
LOG.warning("Illegal regular expression", e);
return;
}
} else {
@@ -253,7 +263,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
StringBuilder message = new StringBuilder(localModFile.getFileName());
if (isNotBlank(localModFile.getGameVersion()))
message.append(", ").append(i18n("archive.game_version")).append(": ").append(localModFile.getGameVersion());
message.append(", ").append(i18n("game.version")).append(": ").append(localModFile.getGameVersion());
if (isNotBlank(localModFile.getAuthors()))
message.append(", ").append(i18n("archive.author")).append(": ").append(localModFile.getAuthors());
this.message = message.toString();
@@ -290,25 +300,65 @@ class ModListPageSkin extends SkinBase<ModListPage> {
titleContainer.setSpacing(8);
ImageView imageView = new ImageView();
if (StringUtils.isNotBlank(modInfo.getModInfo().getLogoPath())) {
Task.supplyAsync(() -> {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modInfo.getModInfo().getFile())) {
Path iconPath = fs.getPath(modInfo.getModInfo().getLogoPath());
Task.supplyAsync(() -> {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modInfo.getModInfo().getFile())) {
String logoPath = modInfo.getModInfo().getLogoPath();
if (StringUtils.isNotBlank(logoPath)) {
Path iconPath = fs.getPath(logoPath);
if (Files.exists(iconPath)) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
Files.copy(iconPath, stream);
return new ByteArrayInputStream(stream.toByteArray());
try (InputStream stream = Files.newInputStream(iconPath)) {
Image image = new Image(stream, 40, 40, true, true);
if (!image.isError() && image.getWidth() == image.getHeight())
return image;
} catch (Throwable e) {
LOG.warning("Failed to load image " + logoPath, e);
}
}
}
return null;
}).whenComplete(Schedulers.javafx(), (stream, exception) -> {
if (stream != null) {
imageView.setImage(new Image(stream, 40, 40, true, true));
} else {
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 40, 40, true, true));
List<String> defaultPaths = new ArrayList<>(Arrays.asList(
"icon.png",
"logo.png",
"mod_logo.png",
"pack.png",
"logoFile.png"
));
String id = modInfo.getModInfo().getId();
if (StringUtils.isNotBlank(id)) {
defaultPaths.addAll(Arrays.asList(
"assets/" + id + "/icon.png",
"assets/" + id.replace("-", "") + "/icon.png",
id + ".png",
id + "-logo.png",
id + "-icon.png",
id + "_logo.png",
id + "_icon.png"
));
}
}).start();
}
for (String path : defaultPaths) {
Path iconPath = fs.getPath(path);
if (Files.exists(iconPath)) {
try (InputStream stream = Files.newInputStream(iconPath)) {
Image image = new Image(stream, 40, 40, true, true);
if (!image.isError() && image.getWidth() == image.getHeight())
return image;
}
}
}
} catch (Exception e) {
LOG.warning("Failed to load icon", e);
}
return null;
}).whenComplete(Schedulers.javafx(), (image, exception) -> {
if (image != null) {
imageView.setImage(image);
} else {
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/command.png", 40, 40, true, true));
}
}).start();
TwoLineListItem title = new TwoLineListItem();
title.setTitle(modInfo.getModInfo().getName());
@@ -324,18 +374,16 @@ class ModListPageSkin extends SkinBase<ModListPage> {
setBody(description);
if (StringUtils.isNotBlank(modInfo.getModInfo().getId())) {
Lang.<Pair<String, RemoteModRepository>>immutableListOf(
for (Pair<String, ? extends RemoteModRepository> item : Arrays.asList(
pair("mods.curseforge", CurseForgeRemoteModRepository.MODS),
pair("mods.modrinth", ModrinthRemoteModRepository.MODS)
).forEach(item -> {
String text = item.getKey();
RemoteModRepository remoteModRepository = item.getValue();
JFXHyperlink button = new JFXHyperlink(i18n(text));
)) {
RemoteModRepository repository = item.getValue();
JFXHyperlink button = new JFXHyperlink(i18n(item.getKey()));
Task.runAsync(() -> {
Optional<RemoteMod.Version> versionOptional = remoteModRepository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile());
Optional<RemoteMod.Version> versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile());
if (versionOptional.isPresent()) {
RemoteMod remoteMod = remoteModRepository.getModById(versionOptional.get().getModid());
RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid());
FXUtils.runInFX(() -> {
for (ModLoaderType modLoaderType : versionOptional.get().getLoaders()) {
String loaderName;
@@ -358,15 +406,16 @@ class ModListPageSkin extends SkinBase<ModListPage> {
default:
continue;
}
if (!title.getTags().contains(loaderName)) {
title.getTags().add(loaderName);
List<String> tags = title.getTags();
if (!tags.contains(loaderName)) {
tags.add(loaderName);
}
}
button.setOnAction(e -> {
fireEvent(new DialogCloseEvent());
Controllers.navigate(new DownloadPage(
new DownloadListPage(remoteModRepository),
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
remoteMod,
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getVersionId()),
null
@@ -378,7 +427,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
}).start();
button.setDisable(true);
getActions().add(button);
});
}
}
if (StringUtils.isNotBlank(modInfo.getModInfo().getUrl())) {
@@ -391,15 +440,6 @@ class ModListPageSkin extends SkinBase<ModListPage> {
getActions().add(officialPageButton);
}
// if (modInfo.getMod() != null && StringUtils.isNotBlank(modInfo.getMod().getMcbbs())) {
// JFXHyperlink mcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
// mcbbsButton.setOnAction(e -> {
// fireEvent(new DialogCloseEvent());
// FXUtils.openLink(ModManager.getMcbbsUrl(modInfo.getMod().getMcbbs()));
// });
// getActions().add(mcbbsButton);
// }
if (modInfo.getMod() == null || StringUtils.isBlank(modInfo.getMod().getMcmod())) {
JFXHyperlink searchButton = new JFXHyperlink(i18n("mods.mcmod.search"));
searchButton.setOnAction(e -> {
@@ -490,10 +530,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
content.getTags().add(i18n("install.installer.quilt"));
break;
}
if (dataItem.getMod() != null) {
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA) {
content.getTags().add(dataItem.getMod().getDisplayName());
}
if (dataItem.getMod() != null && I18n.isUseChinese()) {
content.getTags().add(dataItem.getMod().getDisplayName());
}
content.setSubtitle(dataItem.getSubtitle());
if (booleanProperty != null) {
@@ -24,10 +24,9 @@ import org.jackhuang.hmcl.util.io.IOUtils;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
@@ -127,7 +126,7 @@ public enum ModTranslations {
mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
return true;
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load " + resourceName, e);
LOG.warning("Failed to load " + resourceName, e);
return false;
}
}
@@ -198,10 +197,9 @@ public enum ModTranslations {
return true;
}
public static class Mod {
public static final class Mod {
private final String curseforge;
private final String mcmod;
private final String mcbbs;
private final List<String> modIds;
private final String name;
private final String subname;
@@ -209,23 +207,21 @@ public enum ModTranslations {
public Mod(String line) {
String[] items = line.split(";", -1);
if (items.length != 7) {
throw new IllegalArgumentException("Illegal mod data line, 7 items expected " + line);
if (items.length != 6) {
throw new IllegalArgumentException("Illegal mod data line, 6 items expected " + line);
}
curseforge = items[0];
mcmod = items[1];
mcbbs = items[2];
modIds = Collections.unmodifiableList(Arrays.asList(items[3].split(",")));
name = items[4];
subname = items[5];
abbr = items[6];
modIds = Collections.unmodifiableList(Arrays.asList(items[2].split(",")));
name = items[3];
subname = items[4];
abbr = items[5];
}
public Mod(String curseforge, String mcmod, String mcbbs, List<String> modIds, String name, String subname, String abbr) {
public Mod(String curseforge, String mcmod, List<String> modIds, String name, String subname, String abbr) {
this.curseforge = curseforge;
this.mcmod = mcmod;
this.mcbbs = mcbbs;
this.modIds = modIds;
this.name = name;
this.subname = subname;
@@ -252,10 +248,6 @@ public enum ModTranslations {
return mcmod;
}
public String getMcbbs() {
return mcbbs;
}
public List<String> getModIds() {
return modIds;
}
@@ -1,84 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 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 org.jackhuang.hmcl.game.LocalizedRemoteModRepository;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ModpackDownloadListPage extends DownloadListPage {
public ModpackDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection) {
super(null, callback, versionSelection);
repository = new Repository();
supportChinese.set(true);
downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
if (CurseForgeRemoteModRepository.isAvailable())
downloadSource.set("mods.curseforge");
else
downloadSource.set("mods.modrinth");
}
private class Repository extends LocalizedRemoteModRepository {
@Override
protected RemoteModRepository getBackedRemoteModRepository() {
if ("mods.modrinth".equals(downloadSource.get())) {
return ModrinthRemoteModRepository.MODPACKS;
} else {
return CurseForgeRemoteModRepository.MODPACKS;
}
}
@Override
protected SortType getBackedRemoteModRepositorySortOrder() {
if ("mods.modrinth".equals(downloadSource.get())) {
return SortType.NAME;
} else {
return SortType.POPULARITY;
}
}
@Override
public Type getType() {
return Type.MODPACK;
}
}
@Override
protected String getLocalizedCategory(String category) {
if ("mods.modrinth".equals(downloadSource.get())) {
return i18n("modrinth.category." + category);
} else {
return i18n("curse.category." + category);
}
}
@Override
protected String getLocalizedOfficialPage() {
if ("mods.modrinth".equals(downloadSource.get())) {
return i18n("mods.modrinth");
} else {
return i18n("mods.curseforge");
}
}
}
@@ -1,97 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.game.LocalizedRemoteModRepository;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.util.MissingResourceException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ResourcePackDownloadListPage extends DownloadListPage {
public ResourcePackDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection) {
super(null, callback, versionSelection);
repository = new Repository();
supportChinese.set(true);
downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
if (CurseForgeRemoteModRepository.isAvailable())
downloadSource.set("mods.curseforge");
else
downloadSource.set("mods.modrinth");
}
private class Repository extends LocalizedRemoteModRepository {
@Override
protected RemoteModRepository getBackedRemoteModRepository() {
if ("mods.modrinth".equals(downloadSource.get())) {
return ModrinthRemoteModRepository.RESOURCE_PACKS;
} else {
return CurseForgeRemoteModRepository.RESOURCE_PACKS;
}
}
@Override
protected SortType getBackedRemoteModRepositorySortOrder() {
if ("mods.modrinth".equals(downloadSource.get())) {
return SortType.NAME;
} else {
return SortType.POPULARITY;
}
}
@Override
public Type getType() {
return Type.MOD;
}
}
@Override
protected String getLocalizedCategory(String category) {
String key;
if ("mods.modrinth".equals(downloadSource.get())) {
key = "modrinth.category." + category;
} else {
key = "curse.category." + category;
}
try {
return I18n.getResourceBundle().getString(key);
} catch (MissingResourceException e) {
LOG.log(Level.WARNING, "Cannot find key " + key + " in resource bundle", e);
return category;
}
}
@Override
protected String getLocalizedOfficialPage() {
if ("mods.modrinth".equals(downloadSource.get())) {
return i18n("mods.modrinth");
} else {
return i18n("mods.curseforge");
}
}
}
@@ -31,13 +31,11 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class VersionIconDialog extends DialogPane {
@@ -74,20 +72,19 @@ public class VersionIconDialog extends DialogPane {
private void exploreIcon() {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png", "*.jpg", "*.bmp", "*.gif"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile != null) {
File iconFile = profile.getRepository().getVersionIconFile(versionId);
try {
FileUtils.copyFile(selectedFile, iconFile);
profile.getRepository().setVersionIconFile(versionId, selectedFile);
if (vs != null) {
vs.setVersionIcon(VersionIconType.DEFAULT);
}
onAccept();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to copy icon file from " + selectedFile + " to " + iconFile, e);
} catch (IOException | IllegalArgumentException e) {
LOG.error("Failed to set icon file: " + selectedFile, e);
}
}
}
@@ -33,6 +33,7 @@ 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.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -55,8 +56,8 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final TabHeader tab;
private final TabHeader.Tab<VersionSettingsPage> versionSettingsTab = new TabHeader.Tab<>("versionSettingsTab");
private final TabHeader.Tab<ModListPage> modListTab = new TabHeader.Tab<>("modListTab");
private final TabHeader.Tab<InstallerListPage> installerListTab = new TabHeader.Tab<>("installerListTab");
private final TabHeader.Tab<ModListPage> modListTab = new TabHeader.Tab<>("modListTab");
private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList");
private final TransitionPane transitionPane = new TransitionPane();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
@@ -67,11 +68,11 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
{
versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false)));
modListTab.setNodeSupplier(loadVersionFor(ModListPage::new));
installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new));
modListTab.setNodeSupplier(loadVersionFor(ModListPage::new));
worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new));
tab = new TabHeader(versionSettingsTab, modListTab, installerListTab, worldListTab);
tab = new TabHeader(versionSettingsTab, installerListTab, modListTab, worldListTab);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
@@ -127,10 +128,10 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
if (versionSettingsTab.isInitialized())
versionSettingsTab.getNode().loadVersion(profile, version);
if (modListTab.isInitialized())
modListTab.getNode().loadVersion(profile, version);
if (installerListTab.isInitialized())
installerListTab.getNode().loadVersion(profile, version);
if (modListTab.isInitialized())
modListTab.getNode().loadVersion(profile, version);
if (worldListTab.isInitialized())
worldListTab.getNode().loadVersion(profile, version);
currentVersionUpgradable.set(profile.getRepository().isModpack(version));
@@ -164,9 +165,11 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
private void clearAssets() {
File baseDirectory = getProfile().getRepository().getBaseDirectory();
FileUtils.deleteDirectoryQuietly(new File(baseDirectory, "assets"));
FileUtils.deleteDirectoryQuietly(new File(baseDirectory, "resources"));
HMCLGameRepository baseDirectory = getProfile().getRepository();
FileUtils.deleteDirectoryQuietly(new File(baseDirectory.getBaseDirectory(), "assets"));
if (version.get() != null) {
FileUtils.deleteDirectoryQuietly(new File(baseDirectory.getRunDirectory(version.get().getVersion()), "resources"));
}
}
private void clearJunkFiles() {
@@ -244,15 +247,6 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
runInFX(() -> FXUtils.installFastTooltip(versionSettingsItem, i18n("settings.game")));
versionSettingsItem.setOnAction(e -> control.tab.select(control.versionSettingsTab));
AdvancedListItem modListItem = new AdvancedListItem();
modListItem.getStyleClass().add("navigation-drawer-item");
modListItem.setTitle(i18n("mods.manage"));
modListItem.setLeftGraphic(wrap(SVG.PUZZLE));
modListItem.setActionButtonVisible(false);
modListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.modListTab));
runInFX(() -> FXUtils.installFastTooltip(modListItem, i18n("mods.manage")));
modListItem.setOnAction(e -> control.tab.select(control.modListTab));
AdvancedListItem installerListItem = new AdvancedListItem();
installerListItem.getStyleClass().add("navigation-drawer-item");
installerListItem.setTitle(i18n("settings.tabs.installers"));
@@ -262,6 +256,15 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
runInFX(() -> FXUtils.installFastTooltip(installerListItem, i18n("settings.tabs.installers")));
installerListItem.setOnAction(e -> control.tab.select(control.installerListTab));
AdvancedListItem modListItem = new AdvancedListItem();
modListItem.getStyleClass().add("navigation-drawer-item");
modListItem.setTitle(i18n("mods.manage"));
modListItem.setLeftGraphic(wrap(SVG.PUZZLE));
modListItem.setActionButtonVisible(false);
modListItem.activeProperty().bind(control.tab.getSelectionModel().selectedItemProperty().isEqualTo(control.modListTab));
runInFX(() -> FXUtils.installFastTooltip(modListItem, i18n("mods.manage")));
modListItem.setOnAction(e -> control.tab.select(control.modListTab));
AdvancedListItem worldListItem = new AdvancedListItem();
worldListItem.getStyleClass().add("navigation-drawer-item");
worldListItem.setTitle(i18n("world.manage"));
@@ -273,8 +276,8 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
AdvancedListBox sideBar = new AdvancedListBox()
.add(versionSettingsItem)
.add(modListItem)
.add(installerListItem)
.add(modListItem)
.add(worldListItem);
VBox.setVgrow(sideBar, Priority.ALWAYS);
@@ -22,18 +22,22 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Toggle;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ProcessPriority;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.setting.*;
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.WeakListenerHolder;
@@ -44,19 +48,13 @@ import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -71,7 +69,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private Profile profile;
private WeakListenerHolder listenerHolder;
private String versionId;
private boolean javaItemsLoaded;
private final VBox rootPane;
private final JFXTextField txtWidth;
@@ -82,9 +79,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final JFXCheckBox chkAutoAllocate;
private final JFXCheckBox chkFullscreen;
private final ComponentSublist javaSublist;
private final MultiFileItem<Pair<JavaVersionType, JavaVersion>> javaItem;
private final MultiFileItem.Option<Pair<JavaVersionType, JavaVersion>> javaAutoDeterminedOption;
private final MultiFileItem.FileOption<Pair<JavaVersionType, JavaVersion>> javaCustomOption;
private final MultiFileItem<Pair<JavaVersionType, JavaRuntime>> javaItem;
private final MultiFileItem.Option<Pair<JavaVersionType, JavaRuntime>> javaAutoDeterminedOption;
private final MultiFileItem.StringOption<Pair<JavaVersionType, JavaRuntime>> javaVersionOption;
private final MultiFileItem.FileOption<Pair<JavaVersionType, JavaRuntime>> javaCustomOption;
private final ComponentSublist gameDirSublist;
private final MultiFileItem<GameDirectoryType> gameDirItem;
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
@@ -92,9 +91,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final OptionToggleButton showLogsPane;
private final ImagePickerItem iconPickerItem;
private final ChangeListener<Collection<JavaRuntime>> javaListChangeListener;
private final InvalidationListener specificSettingsListener;
private final InvalidationListener javaListener = any -> initJavaSubtitle();
private boolean updatingJavaSetting = false;
private boolean updatingSelectedJava = false;
private final StringProperty selectedVersion = new SimpleStringProperty();
private final BooleanProperty navigateToSpecificSettings = new SimpleBooleanProperty(false);
@@ -178,14 +179,42 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
javaSublist.setTitle(i18n("settings.game.java_directory"));
javaSublist.setHasSubtitle(true);
javaAutoDeterminedOption = new MultiFileItem.Option<>(i18n("settings.game.java_directory.auto"), pair(JavaVersionType.AUTO, null));
javaCustomOption = new MultiFileItem.FileOption<Pair<JavaVersionType, JavaVersion>>(i18n("settings.custom"), pair(JavaVersionType.CUSTOM, null))
javaVersionOption = new MultiFileItem.StringOption<>(i18n("settings.game.java_directory.version"), pair(JavaVersionType.VERSION, null));
javaVersionOption.setValidators(new NumberValidator(true));
FXUtils.setLimitWidth(javaVersionOption.getCustomField(), 40);
javaCustomOption = new MultiFileItem.FileOption<Pair<JavaVersionType, JavaRuntime>>(i18n("settings.custom"), pair(JavaVersionType.CUSTOM, null))
.setChooserTitle(i18n("settings.game.java_directory.choose"));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
javaCustomOption.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
javaListChangeListener = FXUtils.onWeakChangeAndOperate(JavaManager.getAllJavaProperty(), allJava -> {
List<MultiFileItem.Option<Pair<JavaVersionType, JavaRuntime>>> options = new ArrayList<>();
options.add(javaAutoDeterminedOption);
options.add(javaVersionOption);
if (allJava != null) {
boolean isX86 = Architecture.SYSTEM_ARCH.isX86() && allJava.stream().allMatch(java -> java.getArchitecture().isX86());
for (JavaRuntime java : allJava) {
options.add(new MultiFileItem.Option<>(
i18n("settings.game.java_directory.template",
java.getVersion(),
isX86 ? i18n("settings.game.java_directory.bit", java.getBits().getBit())
: java.getPlatform().getArchitecture().getDisplayName()),
pair(JavaVersionType.DETECTED, java))
.setSubtitle(java.getBinary().toString()));
}
}
options.add(javaCustomOption);
javaItem.loadChildren(options);
initializeSelectedJava();
});
gameDirItem = new MultiFileItem<>();
gameDirSublist = new ComponentSublist();
gameDirSublist.getContent().add(gameDirItem);
gameDirSublist.setTitle(i18n("settings.game.working_directory"));
gameDirSublist.setHasSubtitle(true);
gameDirSublist.setHasSubtitle(versionId != null);
gameDirItem.disableProperty().bind(modpack);
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
.setChooserTitle(i18n("settings.game.working_directory.choose"))
@@ -428,31 +457,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
private void initialize() {
memoryStatus.set(OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID));
Task.supplyAsync(JavaVersion::getJavas).thenAcceptAsync(Schedulers.javafx(), list -> {
boolean isX86 = Architecture.SYSTEM_ARCH.isX86() && list.stream().allMatch(java -> java.getArchitecture().isX86());
List<MultiFileItem.Option<Pair<JavaVersionType, JavaVersion>>> options = list.stream()
.map(javaVersion -> new MultiFileItem.Option<>(
i18n("settings.game.java_directory.template",
javaVersion.getVersion(),
isX86 ? i18n("settings.game.java_directory.bit", javaVersion.getBits().getBit())
: javaVersion.getPlatform().getArchitecture().getDisplayName()),
pair(JavaVersionType.DETECTED, javaVersion))
.setSubtitle(javaVersion.getBinary().toString()))
.collect(Collectors.toList());
options.add(0, javaAutoDeterminedOption);
options.add(javaCustomOption);
javaItem.loadChildren(options);
javaItemsLoaded = true;
initializeSelectedJava();
}).start();
javaItem.setSelectedData(null);
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
javaCustomOption.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe"));
memoryStatus.set(OperatingSystem.getPhysicalMemoryStatus());
enableSpecificSettings.addListener((a, b, newValue) -> {
if (versionId == null) return;
@@ -471,7 +476,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
@Override
@SuppressWarnings("unchecked")
public void loadVersion(Profile profile, String versionId) {
this.profile = profile;
this.versionId = versionId;
@@ -516,8 +520,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
FXUtils.unbindEnum(cboProcessPriority);
lastVersionSetting.usesGlobalProperty().removeListener(specificSettingsListener);
lastVersionSetting.javaVersionTypeProperty().removeListener(javaListener);
lastVersionSetting.javaDirProperty().removeListener(javaListener);
lastVersionSetting.javaProperty().removeListener(javaListener);
lastVersionSetting.defaultJavaPathPropertyProperty().removeListener(javaListener);
lastVersionSetting.javaVersionProperty().removeListener(javaListener);
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirSublist.subtitleProperty().unbind();
@@ -530,6 +536,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
// unbind data fields
javaItem.setToggleSelectedListener(null);
javaVersionOption.valueProperty().unbind();
// bind new data fields
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
@@ -550,18 +557,42 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
enableSpecificSettings.set(!versionSetting.isUsesGlobal());
javaItem.setToggleSelectedListener(newValue -> {
if (javaItem.getSelectedData() == null || updatingSelectedJava)
return;
updatingJavaSetting = true;
if (javaVersionOption.isSelected()) {
javaVersionOption.valueProperty().bindBidirectional(versionSetting.javaVersionProperty());
} else {
javaVersionOption.valueProperty().unbind();
javaVersionOption.setValue("");
}
if (javaCustomOption.isSelected()) {
versionSetting.setUsesCustomJavaDir();
} else if (javaAutoDeterminedOption.isSelected()) {
versionSetting.setJavaAutoSelected();
} else if (javaVersionOption.isSelected()) {
if (versionSetting.getJavaVersionType() != JavaVersionType.VERSION)
versionSetting.setJavaVersion("");
versionSetting.setJavaVersionType(JavaVersionType.VERSION);
versionSetting.setDefaultJavaPath(null);
} else {
versionSetting.setJavaVersion(((Pair<JavaVersionType, JavaVersion>) newValue.getUserData()).getValue());
@SuppressWarnings("unchecked")
JavaRuntime java = ((Pair<JavaVersionType, JavaRuntime>) newValue.getUserData()).getValue();
versionSetting.setJavaVersionType(JavaVersionType.DETECTED);
versionSetting.setJavaVersion(java.getVersion());
versionSetting.setDefaultJavaPath(java.getBinary().toString());
}
updatingJavaSetting = false;
});
versionSetting.javaVersionTypeProperty().addListener(javaListener);
versionSetting.javaDirProperty().addListener(javaListener);
versionSetting.defaultJavaPathPropertyProperty().addListener(javaListener);
versionSetting.javaProperty().addListener(javaListener);
versionSetting.javaVersionProperty().addListener(javaListener);
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
@@ -575,52 +606,101 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
}
private void initializeSelectedJava() {
if (lastVersionSetting == null
|| !javaItemsLoaded /* JREs are still being loaded */) {
if (lastVersionSetting == null || updatingJavaSetting)
return;
}
if (lastVersionSetting.isUsesCustomJavaDir()) {
javaCustomOption.setSelected(true);
} else if (lastVersionSetting.isJavaAutoSelected()) {
javaAutoDeterminedOption.setSelected(true);
} else {
// javaLoading.set(true);
lastVersionSetting.getJavaVersion(null, null)
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> {
javaItem.setSelectedData(pair(JavaVersionType.DETECTED, javaVersion));
// javaLoading.set(false);
}).start();
updatingSelectedJava = true;
switch (lastVersionSetting.getJavaVersionType()) {
case CUSTOM:
javaCustomOption.setSelected(true);
break;
case VERSION:
javaVersionOption.setSelected(true);
javaVersionOption.setValue(lastVersionSetting.getJavaVersion());
break;
case AUTO:
javaAutoDeterminedOption.setSelected(true);
break;
default:
Toggle toggle = null;
if (JavaManager.isInitialized()) {
try {
JavaRuntime java = lastVersionSetting.getJava(null, null);
if (java != null) {
for (Toggle t : javaItem.getGroup().getToggles()) {
if (t.getUserData() != null) {
@SuppressWarnings("unchecked")
Pair<JavaVersionType, JavaRuntime> userData = (Pair<JavaVersionType, JavaRuntime>) t.getUserData();
if (userData.getValue() != null && java.getBinary().equals(userData.getValue().getBinary())) {
toggle = t;
break;
}
}
}
}
} catch (InterruptedException ignored) {
}
}
if (toggle != null) {
toggle.setSelected(true);
} else {
Toggle selectedToggle = javaItem.getGroup().getSelectedToggle();
if (selectedToggle != null) {
selectedToggle.setSelected(false);
}
}
break;
}
updatingSelectedJava = false;
}
private void initJavaSubtitle() {
FXUtils.checkFxUserThread();
initializeSelectedJava();
VersionSetting versionSetting = lastVersionSetting;
if (versionSetting == null)
if (lastVersionSetting == null)
return;
Profile profile = this.profile;
initializeSelectedJava();
HMCLGameRepository repository = this.profile.getRepository();
String versionId = this.versionId;
boolean autoSelected = versionSetting.isJavaAutoSelected();
JavaVersionType javaVersionType = lastVersionSetting.getJavaVersionType();
boolean autoSelected = javaVersionType == JavaVersionType.AUTO || javaVersionType == JavaVersionType.VERSION;
if (autoSelected && versionId == null) {
if (versionId == null && autoSelected) {
javaSublist.setSubtitle(i18n("settings.game.java_directory.auto"));
return;
}
Task.composeAsync(Schedulers.javafx(), () -> {
Pair<JavaVersionType, JavaRuntime> selectedData = javaItem.getSelectedData();
if (selectedData != null && selectedData.getValue() != null) {
javaSublist.setSubtitle(selectedData.getValue().getBinary().toString());
return;
}
if (JavaManager.isInitialized()) {
GameVersionNumber gameVersionNumber;
Version version;
if (versionId == null) {
return versionSetting.getJavaVersion(GameVersionNumber.unknown(), null);
gameVersionNumber = GameVersionNumber.unknown();
version = null;
} else {
return versionSetting.getJavaVersion(
GameVersionNumber.asGameVersion(profile.getRepository().getGameVersion(versionId)),
profile.getRepository().getVersion(versionId));
gameVersionNumber = GameVersionNumber.asGameVersion(repository.getGameVersion(versionId));
version = repository.getVersion(versionId);
}
}).thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion)
.map(JavaVersion::getBinary).map(Path::toString).orElseGet(() ->
autoSelected ? i18n("settings.game.java_directory.auto.not_found") : i18n("settings.game.java_directory.invalid"))))
.start();
try {
JavaRuntime java = lastVersionSetting.getJava(gameVersionNumber, version);
if (java != null) {
javaSublist.setSubtitle(java.getBinary().toString());
} else {
javaSublist.setSubtitle(autoSelected ? i18n("settings.game.java_directory.auto.not_found") : i18n("settings.game.java_directory.invalid"));
}
return;
} catch (InterruptedException ignored) {
}
}
javaSublist.setSubtitle("");
}
private void editSpecificSettings() {
@@ -642,9 +722,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
if (versionId == null)
return;
File iconFile = profile.getRepository().getVersionIconFile(versionId);
if (iconFile.exists())
iconFile.delete();
profile.getRepository().deleteIconFile(versionId);
VersionSetting localVersionSetting = profile.getRepository().getLocalVersionSettingOrCreate(versionId);
if (localVersionSetting != null) {
localVersionSetting.setVersionIcon(VersionIconType.DEFAULT);
@@ -665,10 +743,4 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
private enum JavaVersionType {
DETECTED,
CUSTOM,
AUTO,
}
}
@@ -41,7 +41,6 @@ import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -55,8 +54,8 @@ import java.nio.file.Path;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Versions {
@@ -84,7 +83,7 @@ public final class Versions {
} catch (IOException e) {
Controllers.dialog(
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed"), MessageDialogPane.MessageType.ERROR);
i18n("download.failed.no_code", file.getFile().getUrl()), MessageDialogPane.MessageType.ERROR);
return;
}
Controllers.taskDialog(
@@ -97,7 +96,7 @@ public final class Versions {
} else {
Controllers.dialog(
i18n("install.failed.downloading.detail", file.getFile().getUrl()) + "\n" + StringUtils.getStackTrace(e),
i18n("download.failed"), MessageDialogPane.MessageType.ERROR);
i18n("download.failed.no_code", file.getFile().getUrl()), MessageDialogPane.MessageType.ERROR);
}
}).executor(true),
i18n("message.downloading"),
@@ -184,7 +183,7 @@ public final class Versions {
try {
profile.getRepository().clean(id);
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to clean game directory", e);
LOG.warning("Unable to clean game directory", e);
}
}
@@ -27,9 +27,8 @@ import java.text.DecimalFormat;
import java.time.Instant;
import java.util.Arrays;
import java.util.Locale;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -421,7 +420,7 @@ public final class WorldInfoPage extends StackPane implements DecoratorPage {
try {
this.world.writeLevelDat(levelDat);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to save level.dat of world " + world.getWorldName(), e);
LOG.warning("Failed to save level.dat of world " + world.getWorldName(), e);
}
}
@@ -28,7 +28,6 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
@@ -39,10 +38,10 @@ import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class WorldListPage extends ListPageBase<WorldListItem> implements VersionPage.VersionLoadable {
@@ -139,7 +138,7 @@ public class WorldListPage extends ListPageBase<WorldListItem> implements Versio
}).start();
}, world.getWorldName());
}, e -> {
Logging.LOG.log(Level.WARNING, "Unable to parse world file " + zipFile, e);
LOG.warning("Unable to parse world file " + zipFile, e);
Controllers.dialog(i18n("world.import.invalid"));
}).start();
}
@@ -19,10 +19,11 @@ package org.jackhuang.hmcl.ui.wizard;
import javafx.scene.Node;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Logging;
import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class WizardController implements Navigation {
private final WizardDisplayer displayer;
private WizardProvider provider = null;
@@ -71,7 +72,7 @@ public class WizardController implements Navigation {
displayer.onStart();
Logging.LOG.info("Navigating to " + page + ", pages: " + pages);
LOG.info("Navigating to " + page + ", pages: " + pages);
displayer.navigateTo(page, NavigationDirection.START);
}
@@ -90,7 +91,7 @@ public class WizardController implements Navigation {
if (page instanceof WizardPage)
((WizardPage) page).onNavigate(settings);
Logging.LOG.info("Navigating to " + page + ", pages: " + pages);
LOG.info("Navigating to " + page + ", pages: " + pages);
displayer.navigateTo(page, NavigationDirection.NEXT);
}
@@ -113,7 +114,7 @@ public class WizardController implements Navigation {
if (prevPage instanceof WizardPage)
((WizardPage) prevPage).onNavigate(settings);
Logging.LOG.info("Navigating to " + prevPage + ", pages: " + pages);
LOG.info("Navigating to " + prevPage + ", pages: " + pages);
displayer.navigateTo(prevPage, NavigationDirection.PREVIOUS);
}
@@ -23,7 +23,8 @@ public interface WizardPage {
default void onNavigate(Map<String, Object> settings) {
}
void cleanup(Map<String, Object> settings);
default void cleanup(Map<String, Object> settings) {
}
String getTitle();
}
Loaded 100 of 302 files, more files were not shown because too many files have changed in this diff. Show more