Compare commits

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

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

---------

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

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

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

---------

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

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

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

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

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

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

---------

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

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

---------

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

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

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

Co-authored-by: 辞庐 <109708109+CiiLu@users.noreply.github.com>
2025-08-22 20:36:18 +08:00
Glavo f9dd7a1e64 修复 InstallersPage 意外地显示滚动条的问题 (#4097)
This reverts commit c669026312.
2025-07-19 22:36:30 +08:00
Glavo cbb83d4f15 修复无法使用部分日志字体的问题 (#4096) 2025-07-19 14:56:31 +08:00
Glavo bab863287d Update versions.txt to 1.21.8 (#4085) 2025-07-18 00:08:44 +08:00
Glavo e4c479754b Bump HMCLauncher to 3.6.0.6 (#4086) 2025-07-16 18:50:57 +08:00
Glavo 8ebf58e8fa Fix #4072: 修复无法使用部分中文字体的问题 (#4074) 2025-07-09 19:39:28 +08:00
辞庐 064b39d753 修复添加游戏文件夹页面缺少灰色背景的问题 (#4065) 2025-07-06 15:46:08 +08:00
Glavo c669026312 Fix #4067: 修复使用 JavaFX 25 启动时下载界面布局错位的问题 (#4069) 2025-07-05 22:29:07 +08:00
Glavo d9b95b28b2 Update PLATFORM.md (#4062) 2025-07-04 17:06:41 +08:00
Glavo d492884927 使用 Java 8 运行时禁止更新 (#4032) 2025-07-02 23:44:27 +08:00
3gf8jv4dv cf4fde5592 chore(locale): add shader category translation for russian and spanish (#4042) 2025-07-01 07:58:40 +08:00
Glavo fd959c41c3 Update versions.txt to 1.21.7 (#4035) 2025-07-01 07:52:54 +08:00
3gf8jv4dv e34037c055 chore(locale): add world management translation for russian and spanish (#4041)
Some terminology is consistent with Minecraft Wiki.
2025-06-29 07:13:44 +08:00
3gf8jv4dv 417c804f48 chore(locale): add tip translation of deprecated java ver for ru and es (#4043)
and fix a previous typo.
2025-06-29 07:12:34 +08:00
Glavo ea6eced136 更新 mod_data.txt (#4040) 2025-06-29 04:07:48 +08:00
Glavo 432465ab46 修复部分 Linux 环境中未正确选择默认字体的问题 (#4036) 2025-06-27 17:23:27 +08:00
Burning_TNT 03c95cd726 Fix: 压制 DiscoJavaDistribution 编译警告 (#4038)
* Fix: Suppress compile-time warning.

* Fix: typo

* Adjust F*** -> Fucking.
2025-06-27 17:16:40 +08:00
Burning_TNT 730febca8c Fix: MultiMC allows libraries with the same artifact descriptor string. (#4016) 2025-06-21 21:18:53 +08:00
Kieran Moy bc41499d1c fix typo (#4015) 2025-06-19 17:19:09 +08:00
3gf8jv4dv 60fae5cc88 chore: update versions.txt to 1.21.6 (#4011) 2025-06-18 23:58:12 +08:00
Glavo c11837baa4 修复光影下载页面中标签名称未翻译的问题 (#4007) 2025-06-17 09:07:47 +08:00
Glavo aaf1218536 修复 macOS 上无法读取 CPU 信息的问题 (#4009) 2025-06-16 11:48:05 +08:00
Glavo 88637b33bc 更新 GROUPS_URL (#4004) 2025-06-15 09:34:24 +08:00
Burning_TNT 462eea3539 Fix #3997: 模组搜索返回结果为空时下载界面异常 (#3998) 2025-06-14 21:23:39 +08:00
Burning_TNT d624086594 解析 MultiMC 整合包中未声明但在依赖链上的组件 (#4003) 2025-06-14 21:23:21 +08:00
Burning_TNT fa8b3a952b Fix #3999: 自动安装更改 MC 版本后无法点击安装键 (#4001) 2025-06-14 21:12:59 +08:00
Burning_TNT c70a6c3372 移除漏洞反馈 Issue 模板中的系统信息 (#4000) 2025-06-14 21:12:33 +08:00
Glavo 0616c37de7 创建世界管理页面 (#3991) 2025-06-13 14:28:51 +08:00
Burning_TNT b72056f7fe Fix #3994: 修复 Controller 未能读取 JAVA_VERSION_TIP 的问题 (#3995) 2025-06-12 20:49:55 +08:00
Glavo 3ebe8dce44 修复在 schematics 文件夹不存在的情况下,打开投影管理页面会抛出异常的问题 (#3996) 2025-06-12 20:46:04 +08:00
Glavo a9c28573b8 Bump HMCLauncher to 3.6.0.4 (#3992) 2025-06-12 12:38:45 +08:00
9bead7e34e 支持下载光影 (#2794)
* Clean up duplicated codes in DownloadPage.

* Remove useless codes. Provide support for downloading shaderpacks from Modrinth.

* Fix.

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

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

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

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

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

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

* Update Icon

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-06-11 19:30:47 +08:00
3gf8jv4dvandGlavo d0b36ffa15 fix(i18n): localize MiB in VersionSettingsPage (#3984)
For Slavic languages (Russian, Ukrainian, etc.) they need translations.

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-06-11 01:43:05 +08:00
辞庐andGlavo ea081333e7 自动安装页面隐藏 mcbbs 项 (#3836)
* update

* CheckStyle

* Update InstallerListPage.java

CheckStyle

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-06-11 01:25:59 +08:00
3gf8jv4dv 2b99da430f chore(locale): add folder.schematics to spanish and russian (#3983) 2025-06-11 01:16:20 +08:00
Wulian233andGlavo f1ea316bad feat: 更新日志中仅显示本次更新内容 (#3974)
* 更新日志中仅显示本次更新内容

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-06-11 00:57:30 +08:00
3gf8jv4dv 9565c3bdfe fix(locale): fix units of information in russian (#3985) 2025-06-11 00:32:28 +08:00
3gf8jv4dv 710fd0f075 fix(locale): fix units of information in japanese (#3986) 2025-06-11 00:29:23 +08:00
Burning_TNT 7d7dbb9eb5 重构 MultiMC 整合包兼容功能 (#3547)
Fix #3540
Fix #3706
2025-06-08 20:59:44 +08:00
3gf8jv4dv 88a7243b32 chore(locale): clean up chinese text style (#3811)
- Clean up most of the punctuation to bring it up to standard.
- Fine-tune some of the text wording.
2025-06-08 20:29:43 +08:00
Glavo a0e9a20782 优化 JavaVersionConstraint (#3981) 2025-06-08 20:21:05 +08:00
Glavo 8889cca878 使用独立线程保存设置 (#3929) 2025-06-08 12:33:13 +08:00
Glavo bc911b95a0 Bump HMCLauncher to 3.6.0.3 (#3979) 2025-06-08 10:01:54 +08:00
3gf8jv4dv a46991bd95 chore(i18n): unify reveal button i18n (#3965)
- Unify i18n for related buttons in other pages
to `reveal.in_file_manager`.
- Add translation of `reveal.in_file_manager` for
Spanish, Japanese, and Russian.
2025-06-08 09:49:32 +08:00
3gf8jv4dv 395823c2b5 chore(locale): revise settings.launcher.launcher_log.reveal (#3966)
For various languages. Add translation for Japanese.
2025-06-08 09:48:19 +08:00
3gf8jv4dv 9769cf3996 chore(locale): add translation of "ban query" to spanish and russian (#3970)
and revise english.
2025-06-08 09:46:41 +08:00
Glavo ee652f6f31 优化原理图管理功能 (#3978) 2025-06-08 09:45:50 +08:00
Glavo d317844b28 修复 Java 管理页面无法添加 IKVM 的问题 (#3968) 2025-06-08 09:45:23 +08:00
3gf8jv4dv d9d32eab1d fix(ui): refine PromptDialogPane (#3693)
- Limit the dialog width to avoid too long a width
- Avoid accidental blank line in the information box (That blue bg box)
2025-06-06 21:56:40 +08:00
3gf8jv4dv 24bb630afa fix(locale): correct zh-TW translation of "add" (#3964)
Another one of the previous mistakes.

Glossary:
- en-US: add
- zh-CN: 添加
- zh-TW: 新增
2025-06-06 21:54:34 +08:00
3gf8jv4dv 2359ad9ed7 chore(locale): add schematic translation to spanish and russian (#3972)
and revise english.

The relevant terminology is consistent with the Litematica mod.
2025-06-06 21:05:58 +08:00
辞庐andGlavo 17abfc1a55 添加Xbox封禁相关提示 (#3860)
---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-31 22:53:17 +08:00
Glavo caa98c8956 新增“原理图管理”界面 (#3951) 2025-05-31 21:53:16 +08:00
General_K1ng 6124b6711f 在切换目录时确保主页版本列表切换菜单正确更新 (#3949) 2025-05-31 21:48:24 +08:00
Glavo 3b3cc54cb5 Bump HMCLauncher to 3.6.0.2 (#3953) 2025-05-31 16:36:23 +08:00
Glavo 45a40ee675 优化 Linux 平台硬件检测 (#3946) 2025-05-31 11:38:28 +08:00
Glavo 4a75f1d462 修复通过 fc-match 获取字体时未正确处理 TTC 文件的问题 (#3950) 2025-05-30 22:33:19 +08:00
Glavo e0805fc25f Fix #3940: 修复未正确解析 IPv6 服务器地址的问题 (#3942) 2025-05-29 19:34:53 +08:00
General_K1ng f223d2bc41 fix(ui): 修复版本列表为空时显示的问题 (#3944) 2025-05-29 19:26:42 +08:00
3gf8jv4dv 5ab75b6a84 chore(locale): add translations of new text for spanish and russian (#3941) 2025-05-26 21:47:44 +08:00
Glavoand3gf8jv4dv 362969db95 使用 Java 8 启动启动器时弹出弃用警告 (#3938)
* 使用 Java 8 启动启动器时弹出弃用警告

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

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

* 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>

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-05-26 09:48:55 +08:00
Glavo fb37e3b0df 修复导出启动器时未正确从 .hmcl/background 中查找壁纸的问题 (#3936) 2025-05-25 13:16:40 +08:00
Glavo ad312a61fe 清理高通骁龙 SoC 名称 (#3935) 2025-05-25 13:05:54 +08:00
Glavo 58a4700a46 创建 WindowsVersion 工具类 (#3932) 2025-05-23 21:11:35 +08:00
Glavo 68c8a30bd7 导出整合包时将启动器配置集中存放在 .hmcl 文件夹中 (#3930) 2025-05-23 18:08:57 +08:00
Glavo bb37e3fafa Use StringUtils.isASCII to check if a string contains non-ASCII characters (#3931) 2025-05-23 00:25:28 +08:00
Glavo 942f7b703e 清理 CPU 名称 (#3928) 2025-05-21 14:22:27 +08:00
Burning_TNTandGlavo 267da46050 将日志 caller 信息中的 'org.jackhuang.hmcl.' 缩短为 '@.' (#3919)
* Shorten 'org.jackhuang.hmcl.' in logs to @.

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-21 00:08:41 +08:00
Glavo 7c40dca3ea Bump JavaFX to 17.0.15/24.0.1 (#3927) 2025-05-20 23:55:24 +08:00
Glavo 48e3f79965 将 OS X 重命名为 macOS (#3878) 2025-05-20 19:34:35 +08:00
Glavo c5f56eb5c4 清理 OperatingSystem (#3925) 2025-05-20 19:03:48 +08:00
Burning_TNT 0900ad3e5a Fix #3757: 切换游戏文件夹后,全局游戏设置应重新读取设置 (#3818) 2025-05-20 13:41:25 +08:00
Glavo d347b0c6cb 优化系统版本检测 (#3924) 2025-05-20 13:33:57 +08:00
Glavo 2f3bf61d94 调用 PowerShell 时添加 NoProfile 参数 (#3923) 2025-05-20 12:54:04 +08:00
3gf8jv4dv 5479607597 fix(i18n): refine tasklistpane i18n (#3600)
The current TaskListPane may not look different for Chinese users, but
the grammar is not quite right for English users.

My previous series of PRs did not address this aspect due to the
associated hard coding of the i18n key.

I believe that revisions in this aspect can further improve the
experience of Indo-European speakers.

All languages except Japanese are ready for this PR.
2025-05-20 11:53:01 +08:00
Glavo 4291ae5f16 在 Linux 平台通过 fc-match 查询默认字体 (#3922) 2025-05-20 00:25:56 +08:00
e74yp8andGlavo 1ac677365b Fix #3865 游戏版本名称在游戏版本更换时报红 (#3874)
---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-19 12:00:16 +08:00
Glavo b079e50916 修复启动器在部分 Linux 发行版上无法正确识别用户地区的问题 (#3921) 2025-05-19 11:06:45 +08:00
e74yp8 f986df9654 Fix #3896: 下载界面对于多行信息显示错误 (#3901) 2025-05-18 23:11:34 +08:00
RainChanandGlavo c576c48f9d Fix #3852: 修复无法一键更新整合包的问题 (#3888)
* Fix #3852 修复无法一键更新整合包

* 删除未使用的import

* fix checkstyle

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-17 07:54:13 +08:00
Glavo 5bb035c884 在非 x86 平台检测 CPU 物理核心数 (#3917) 2025-05-17 00:20:17 +08:00
Glavo 973b7a9716 启动时在日志中记录 CPU 信息 (#3914) 2025-05-16 14:50:43 +08:00
Glavo 6e05b5ee58 使用 JNA 查询注册表 (#3913) 2025-05-14 22:18:02 +08:00
Glavo 76ed9353bd 重构 Theme (#3910)
* update

* update

* update

* update

* update

* update
2025-05-14 14:18:50 +08:00
Glavo 9c0f823705 优化代理设置 (#3876) 2025-05-13 16:39:43 +08:00
e74yp8 b69602b735 Fix #3218: 修复复制游戏版本里的“复制存档”选项失效问题 (#3868) 2025-05-12 23:21:20 +08:00
3gf8jv4dv 7cdf4504d1 fix(locale): add i18n key curse.category.7669 (#3906) 2025-05-11 23:07:29 +08:00
Burning_TNT d95efc4cdc Use org.intellij.lang.annotations.Language (#3907) 2025-05-11 23:07:06 +08:00
Glavo 48ca2d6ee6 在 SystemInfo 中采集内存信息 (#3903) 2025-05-10 12:06:11 +08:00
Glavo 1da98d6d67 启动时在日志中记录当前平台的所有 GPU (#3893) 2025-05-09 10:08:08 +08:00
Glavo 06a89dd6a9 尝试从 HMCL.jar 所在文件夹加载默认字体 (#3899) 2025-05-08 22:36:33 +08:00
3gf8jv4dv 4eee79da17 chore(locale): revise invalid username prompt for spanish and russian (#3897) 2025-05-06 21:48:41 +08:00
Glavo 9788338b65 更新“关于”页面 (#3895) 2025-05-06 21:48:05 +08:00
3gf8jv4dv fb8da249b6 chore(locale): add/revise prompt about create hmcl dir failure (#3879) 2025-05-04 23:14:53 +08:00
Glavo 2d8b1df465 在 Windows 平台通过 GeoID 检测用户所处国家/区域 (#3891) 2025-05-04 23:13:40 +08:00
Glavo 1ab7ab0750 使用 JNA 调用本机 API (#3890) 2025-05-04 22:31:50 +08:00
3gf8jv4dv f283254ca0 fix(locale): revise version.empty translation (#3812)
Revise `.launch` and `.hint` to make text match the actual situation.
2025-05-04 09:31:35 +08:00
Wulian233andGlavo 2ea96fdaa7 Fix #3864 游戏内容下载的分页按钮不重置 (#3866)
* Fix #3864

* Update DownloadListPage.java

* Update DownloadListPage.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-05-03 04:45:47 +08:00
Glavo b32d3c354a Move authlib-injector.jar to .hmcl/dependencies/universal (#3886) 2025-05-03 04:45:25 +08:00
e74yp8 cd57b163a3 Fix #3851: 当用户名为中文名时,离线账户 - LittleSkin 换肤 功能失效 (#3883) 2025-05-02 22:21:55 +08:00
Glavo cc5484f9ed Fix #3881: 打开启动器时应当创建 HMCL_GLOBAL_DIRECTORY (#3882) 2025-05-01 11:59:34 +08:00
Glavo bfa9c1a697 修复启动器未压缩日志的问题 (#3877) 2025-04-30 14:56:03 +08:00
Glavo 9a37d412a3 使用工作路径下的 .hmcl 文件夹存放所有配置文件 (#3875) 2025-04-30 14:50:51 +08:00
Glavo dc5ebcf64d 使用 libs.versions.toml 管理依赖版本 (#3873) 2025-04-29 13:41:45 +08:00
Glavo 038f56b048 Bump Shadow Plugin to 8.3.6 (#3871) 2025-04-29 10:13:02 +08:00
Glavo 528f79e008 使用 Java 11 构建 buildSrc (#3870) 2025-04-29 10:04:29 +08:00
Glavo 75d0ec34f5 将构建项目所需的最低 JDK 版本升级至 11 (#3805) 2025-04-29 09:52:06 +08:00
Glavo d70712a18f Fix #3546: 修复游戏崩溃窗口乱码的问题 (#3867) 2025-04-27 22:15:30 +08:00
Glavo 05c5b53c52 自动探测日志文件编码 (#3863) 2025-04-27 20:39:49 +08:00
Glavo e98c7dfef1 Fix #3400: 逐行过滤日志中的 token (#3856) 2025-04-27 15:41:09 +08:00
3gf8jv4dv 8ba0e16dd9 chore(locale): add new translations for russian and spanish (#3838)
For relevant text of offline account restrictions.
2025-04-26 16:40:12 +08:00
Glavo 77397f6722 优化离线账户中包含非 ASCII 字符时的提示 (#3853) 2025-04-26 13:19:38 +08:00
Glavo c9ba789b7c Bump Gradle to 8.14 (#3857) 2025-04-26 00:05:20 +08:00
Glavo 91135b961e 右键主页“启动游戏”按钮时展开版本列表菜单 (#3854) 2025-04-25 00:31:52 +08:00
3gf8jv4dvandGlavo 435790aa03 fix(i18n): show installable translations according to different styles (#3624)
* fix(i18n): show installable translations according to different styles

when install a new instance (安装新游戏): Do not install (不安装)
on Edit Instance → Loaders (自动安装页面): Not installed (未安装)

resolves https://github.com/HMCL-dev/HMCL/issues/3297

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-25 00:14:45 +08:00
Glavo c252d7027e 更新 Issue 模板 (#3847) 2025-04-22 23:59:24 +08:00
Glavo 9d0c8da310 拆分 HMCLauncher 至独立项目 (#3840) 2025-04-20 11:52:29 +08:00
Burning_TNTandGlavo 84b3312ebb Fix #3825: 修复 NeoForge 愚人节版本无法正常解析的问题 (#3826)
* Fix #2825.

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-19 11:49:42 +08:00
Burning_TNTandGlavo f351d1d133 Close #3829: 将模组/整合包/资源包的默认下载源调整为 Modrinth (#3830)
* Close #3829: Use Modrinth as default download source.

* Update HMCLLocalizedDownloadListPage.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-18 22:49:02 +08:00
Glavo 5ca1120223 Bump jsoup to 1.19.1 (#3835) 2025-04-18 22:42:10 +08:00
Glavo 6aa4019221 Bump Gson to 2.13.0 (#3834) 2025-04-18 22:35:05 +08:00
Glavo 73fb8bda10 优化默认 JVM 参数 (#3824) 2025-04-15 03:46:33 +08:00
Glavo cc16f84992 默认限制离线账户功能 (#3823) 2025-04-14 20:34:01 +08:00
辞庐 b4945a150e 统一导出和导入整合包页面样式 (#3822) 2025-04-13 22:34:22 +08:00
Wulian233andGlavo 8bcbe74d82 优化整合包导出类型选择页面 (#3800)
* 优化整合包导出类型页面

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-12 22:10:28 +08:00
Glavo 69c2429e92 Bump mesa-loader-windows to 25.0.3 (#3819) 2025-04-12 20:36:05 +08:00
Glavo 88e83dc44a 修复自定义背景图片路径包含无效字符时启动器崩溃的问题 (#3817) 2025-04-12 17:51:28 +08:00
Burning_TNT 87c2126e5a Fix #3471: 模组管理内“安装到当前版本”与“下载到本地文件夹”相同 (#3772) 2025-04-12 17:39:58 +08:00
Liyan Zhao abf1ec3eb8 反序列化账户信息时优先将数字解析为 Long (#3080) 2025-04-12 13:01:49 +08:00
Burning_TNT 139fb41149 Fix #3147: 修复 LiteLoader 下载相关问题 (#3776)
* Fix #3147

* Fix: Use 'release' classifier on snapshot versions. Speed up loading time.

* Use https.
2025-04-12 12:07:26 +08:00
Glavo 6c986921d6 Delete unused constants (#3816) 2025-04-12 12:05:24 +08:00
3gf8jv4dv b8e6d6fc96 fix(locale): adjust the wording of the unofficial hmcl build prompts (#3813)
The display condition for `download.curseforge.unavailable` is whether
HMCL has a CurseForge API secret built in, not its update channel.
2025-04-12 11:39:40 +08:00
3gf8jv4dv 75e4aee8aa fix(locale): improve world backup ui i18n (#3815) 2025-04-12 11:38:50 +08:00
Glavo abda9f50e1 Fix #3807: 修复 Fabric Metadata 无效时 FabricInstallTask 抛出 NPE 的问题 (#3814) 2025-04-12 02:13:14 +08:00
Burning_TNT c33ef5170b Fix #3644: 修复下载 - 模组界面无法正常翻页、Category 会在切换下载源时出错的问题 (#3768) 2025-04-09 11:35:56 +08:00
Glavo a3cb871928 修复 RISC-V 平台游戏识别 CPU 名称出错的问题 (#3806) 2025-04-09 01:27:38 +08:00
Glavo 62e965c6fe 使用 Kala Compress 替代 commons-compress (#3481) 2025-04-08 17:36:36 +08:00
Glavo 99a031cea4 Gradle run task 应使用 HMCL_JAVA_HOME 所指向的 Java (#3804) 2025-04-08 12:29:14 +08:00
Zkitefly f97b9b9382 Update unlisted-versions.json (#3799) 2025-04-06 11:29:17 +08:00
Zkitefly a8929560df Update PURCHASE_URL (#3797) 2025-04-05 21:04:55 +08:00
Glavo 22c8c59b5d 优化 CacheRepository (#3794) 2025-04-05 06:08:02 +08:00
ZkiteflyandGlavo 78b043c19c 添加 unlisted-versions-of-minecraft (#3247)
* 添加 unlisted-versions-of-minecraft

* Update BMCLAPIDownloadProvider.java

* Update VersionsPage.java

* Update VersionsPage.java

* Update BMCLAPIDownloadProvider.java

* update name

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-04 22:49:31 +08:00
3gf8jv4dv fd51fb7d26 chore(locale): adjust the use of quotation marks in spanish (#3685)
Replace the quotation marks with guillemets.
2025-04-04 22:38:05 +08:00
Burning_TNT 43c59dad9c Fix: There are two progress bars when updating HMCL (#3793) 2025-04-04 20:54:08 +08:00
Zkitefly 81b0547610 更新链接为 Metadata 中的常量,优化代码可维护性 (#3674)
* 更新链接为 Metadata 中的常量,优化代码可维护性

* update
2025-04-04 18:47:42 +08:00
997a16486b 优化下载窗口标题 (#3771)
* update

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

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

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

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

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

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

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

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

* Apply suggestions from code review

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

* update

* update

* update

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-04 00:47:37 +08:00
Wulian233andGlavo 36ebdab698 折叠高级设置中基于特定系统不可用的选项 (#3734)
* 隐藏高级设置中基于特定系统不可用的选项

* 不支持的系统使用折叠选项

* 完成~待审

* 修改文本描述

* 修正翻译

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-03 23:08:34 +08:00
Glavo a6471bca09 Add 25w14craftmine to versions.txt (#3781) 2025-04-02 00:35:05 +08:00
Burning_TNT cb6320ffae Fix #3627: 修复离线账户切换模型时左侧 3D 预览不会即时更新的问题 (#3769) 2025-04-01 15:43:01 +08:00
Burning_TNT 7cf520cb48 Fix #3589: 修复 Windows 平台部分语言编码下无法打开 Minecraft 中文 Wiki 的问题 (#3770) 2025-04-01 15:40:05 +08:00
Minecraft-温迪andGlavo 7dc922d4bd Fix #3777: 修复使用 Rosetta 2 转译运行时无法正确识别系统架构的问题 (#3778)
* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-04-01 15:31:09 +08:00
Glavo 8e57729349 Update versions.txt (#3744) 2025-03-26 22:55:44 +08:00
Glavo 66ed87eb1e 修复 cslApi 为 null 时启动器崩溃的问题 (#3762) 2025-03-24 02:52:17 +08:00
ShulkerSakuraandGlavo 27ffc8eec5 Add MacOS Style Icon (#3755)
* Add MacOS Style Icon

* MacOS Style Alt Icon

* Scaled Mac Style Icon

* Delete unused icons

* Rename icon

* update

* Optimize icon file

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-03-22 19:13:32 +08:00
Glavo e2f50c92e9 修复无法解析部分 Java 版本号的问题 (#3759)
* 修复无法解析部分 Java 版本号的问题

* update

* update
2025-03-22 12:17:13 +08:00
Glavo e8ff42c4b2 修复取消导出整合包时弹出报错对话框的问题 (#3752)
* 修复取消导出整合包时弹出报错对话框的问题

* Update
2025-03-21 11:01:37 +08:00
Glavo 44ca2cdb55 更新主页公告 (#3751)
* 更新主页公告

* update

* update
2025-03-20 21:25:30 +08:00
Glavo 77bbee02f9 消除 JEP 498 引入的弃用警告 (#3750)
* 消除 JEP 498 引入的弃用警告

* Fix checkstyle
2025-03-20 16:59:46 +08:00
Glavo e4c7b137f1 Add --enable-native-access for javafx.graphics (#3749)
* Add --enable-native-access for javafx.graphics

* fix checkstyle

* update
2025-03-20 16:21:17 +08:00
Glavo efd088e014 在日志中记录编码信息 (#3747)
* 在日志中记录 Code Page

* update

* update
2025-03-20 10:02:16 +08:00
Glavo 2ae266491c 更新关于页面头像 (#3745) 2025-03-18 20:11:07 +08:00
Glavo 0912613cfc 优化 ScrollUtils (#3432)
* 优化 ScrollUtils

* update
2025-03-18 13:31:47 +08:00
Glavo d9e3816b8e 优化账户列表页面 (#3736) 2025-03-13 22:18:23 +08:00
GlavoandZkitefly bdaf72fe2f 修复 ModrinthRemoteModRepository::getRemoteVersionByLocalFile 未捕获 NoSuchFileException 的问题 (#3735)
---------

Co-authored-by: Zkitefly <2573874409@qq.com>
2025-03-11 21:58:18 +08:00
Glavo 7efbbefdcf HTMLRenderer 应当避免插入过多换行符 (#3733) 2025-03-10 22:14:46 +08:00
Glavo ad71216bfc 为部分页面添加半透明背景遮罩 (#3730) 2025-03-09 13:02:00 +08:00
Wulian233 57903da09b Close #3728: 为更新日志对话框添加平滑滚动 (#3729) 2025-03-08 22:35:51 +08:00
3gf8jv4dv cf56c8035b fix: add scrollbar for GameCrashWindow (#3723)
Shows a scroll bar when needed to prevent user misinterpretation
when there is too much information.
2025-03-08 16:32:54 +08:00
Glavo dbf519b435 Fix #3720: 修复备份文件路径被错误地设置为源世界的问题 (#3722) 2025-03-08 16:18:49 +08:00
Glavo 696b9bd7ef Fix #3694: 显示对话框时隐藏焦点 (#3717) 2025-03-08 08:47:19 +08:00
3gf8jv4dv ec5236368a chore(locale): add new translations of world parts for es and ru (#3714) 2025-03-07 21:39:39 +08:00
3gf8jv4dv 8d3794d828 fix(i18n): split game.version to make different scenarios appropriate (#3709)
- `game.version` indicates the currently used game instance
in the game crash window, not the game version.

- Change the i18n key previously used in ModListPageSkin to
`mods.game.version`, to be consistent with the world section.
2025-03-07 21:06:34 +08:00
3gf8jv4dv d1b59ef553 fix(locale): revise spanish (#3713)
Fix some mistakes. :\
2025-03-07 20:20:04 +08:00
3gf8jv4dv 79f29e143e fix(locale): clean up language typos (#3707) 2025-03-07 19:35:05 +08:00
Glavo e7ffb0b271 优化世界管理页面 (#3711)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update
2025-03-07 19:19:04 +08:00
3gf8jv4dv cda2f6fb82 chore(locale): improve spanish translation (#3712)
- Add translation of the `game.crash` part.
- Improve further the translation of the remaining parts.
2025-03-07 17:34:40 +08:00
3gf8jv4dv 114aa4ce94 chore(locale): add missing translations for spanish (#3710)
Several i18n keys were added during the 3.6.12 semester.
This patch adds those translations.
2025-03-07 17:12:34 +08:00
Glavo 0ff194b391 Fix #3485: 修复游戏 Java 路径显示错误的问题 (#3708) 2025-03-06 12:30:13 +08:00
3gf8jv4dvandGlavo 025bde66d6 chore(locale): improve russian translation (game crash part) (#3704)
* chore(locale): improve russian translation (game crash part)

- Re-translate something from English
- Fix and unify the style of text left by different contributors
- Fix some inconsistent/incorrect terminology
- Add missing translations

* Update I18N_ru.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-03-06 12:00:37 +08:00
Glavo 872c1943b7 修复 Javadoc 中的无效链接 (#3703) 2025-03-05 19:00:22 +08:00
Glavo d268d923d7 更新全部 SVG 图标至 Material Symbols (#3687) 2025-03-05 16:39:21 +08:00
3gf8jv4dv d523ccd312 fix(ui&locale): optimize the create offline account dialog (#3695)
- Adjust lblErrorMessage for invalid username to avoid ui truncation
- Improve the wording of warnings
2025-03-05 16:05:02 +08:00
3gf8jv4dv 78a5ade35a fix(locale): fix english translation of game.crash.reason.file_changed (#3699)
My mistake in https://github.com/HMCL-dev/HMCL/pull/3367
2025-03-05 16:02:56 +08:00
Glavo f011a14798 在 DDE 上禁用自绘窗口动画 (#3701) 2025-03-05 16:01:48 +08:00
Glavo f4701c678e Revert "启用高帧率动画 (#3655)" (#3700)
This reverts commit a45b636b1c.
2025-03-05 14:45:02 +08:00
Zkitefly e61fb27c61 更新 PURCHASE_URL (#3675)
* Update YggdrasilService.java

* Update YggdrasilService.java
2025-03-05 02:54:39 +08:00
3gf8jv4dv d2f58a89a2 fix(locale): remove duplicate extensions (#3692)
Oops... Didn't notice this before.

The issue first appeared in https://github.com/HMCL-dev/HMCL/pull/2468
2025-03-03 15:58:53 +08:00
3gf8jv4dvandGlavo 5174e0a67a chore(locale): revise english (#3688)
* chore(locale): revise english

`settings.launcher.launcher_log.reveal`
Considering that HMCL is a cross-platform launcher and not all systems
use the term `Explorer`, this should be changed to a more generic term.

`update.channel.dev.hint` and `update.channel.nightly.hint`
Revise text style.

Misc
Resolve rendering issues on some text editors.

* Update I18N.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-03-03 10:41:03 +08:00
3gf8jv4dv 4225e98d6b chore(locale): improve russian translation (curseforge category) (#3686)
- Add translations
- Some proper nouns (mod name) keep the original name
- Some proper nouns (game content) follow the game and wiki
2025-03-03 02:12:46 +08:00
Glavo 0540f3c9cd 在世界管理界面显示世界图标 (#3689)
* 在世界管理界面显示世界图标

* update

* update
2025-03-03 02:12:07 +08:00
Glavo 4292d47fe8 Bump Gson to 2.12.1 (#3691) 2025-03-02 23:58:07 +08:00
3gf8jv4dv 4e98d80855 chore(locale): improve russian translation (setting, launch, menu, etc) (#3684)
Fully revision, except for the `curse` and `game.crash.reason` parts.

- Re-translate something from English
- Fix some inconsistent terminology
- Change some terminology to make sure to keep up with the game and wiki
- Add some missing translations
- Revise some incorrect/outdated translations
2025-03-02 23:56:55 +08:00
Glavo 578aaafb0a 使用 DDE 桌面时启用窗口动画 (#3682) 2025-03-02 00:28:44 +08:00
3gf8jv4dv 60deea64c8 chore(locale): improve russian translation (mods & nbt) (#3678)
- add `mods.check_updates.empty`
- add nbt translations
2025-03-01 21:05:08 +08:00
3gf8jv4dv 7e1cd4319b chore(locale): improve russian translation (modpack) (#3679)
- Revise some translations based on English
- Change some terminology to make sure to keep up with the game and wiki
- Add some missing translations
2025-03-01 21:04:54 +08:00
Glavo 318da840f9 修复对话框关闭动画缺失的问题 (#3676) 2025-03-01 21:01:51 +08:00
Glavo 0c7046d764 优化 TransitionPane (#3680)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* Fix checkstyle
2025-03-01 20:50:31 +08:00
Glavo bdf4886445 禁止标题栏中返回键获取焦点 (#3677) 2025-03-01 10:20:39 +08:00
Glavo 4a758d045d 使更多标签支持双击复制 (#3673) 2025-03-01 01:47:56 +08:00
Glavo 1059a4b3d4 支持关闭鼠标进入 RipplerContainer 时的渐变动画 (#3670)
* update

* fix checkstyle
2025-02-28 23:56:42 +08:00
Glavo a25eb14da4 优化 TransitionPane (#3668) 2025-02-28 23:41:40 +08:00
Glavo 349b663c83 禁止标题栏按钮获取焦点 (#3666) 2025-02-28 01:54:53 +08:00
Glavo 04781da3a7 清理 FXUtils (#3667) 2025-02-28 01:53:31 +08:00
Glavo 96e1ddff79 Close #3664: 支持双击复制世界信息 (#3665)
* Close #3664: 支持双击复制世界信息

* update

* update
2025-02-27 23:58:20 +08:00
3gf8jv4dv e8502890a4 chore(locale): improve russian translation (game setting / management) (#3661)
- Re-translate something from English
- Fix some inconsistent terminology
- Revise text to fit UI
- Change some terminology to make sure to keep up with the game and wiki
- Add some missing translations
2025-02-27 21:19:31 +08:00
3gf8jv4dv 4d0619a9df chore(locale): improve russian translation (javafx dialog / search) (#3662) 2025-02-27 21:18:57 +08:00
3gf8jv4dv fba14737f9 chore(locale): revise unofficial hint for russian (#3660)
- add `unofficial.hint`
- fix typo in `account.methods.microsoft.snapshot`
2025-02-27 20:56:59 +08:00
3gf8jv4dv 5de98399c5 chore(locale): add wiki translation for russian (#3659) 2025-02-27 20:56:41 +08:00
Glavo 7adfa0e3d1 启动时尝试从 HMCL_DIRECTORY 中加载字体 (#3663)
* 启动时尝试从 HMCL_DIRECTORY 中加载字体

* update

* update
2025-02-27 15:05:51 +08:00
Glavo a45b636b1c 启用高帧率动画 (#3655)
* 启用高帧率动画

* update
2025-02-27 09:51:06 +08:00
Glavo 50c22e1f43 添加最小化和还原动画 (#3657)
* update

* update

* update

* update

* update

* update

* update

* update

* update

* fix checkstyle
2025-02-26 20:39:49 +08:00
Glavo 3b38c22398 修复标题过长时窗口异常的问题 (#3654) 2025-02-26 02:00:27 +08:00
Glavo 618243350e 优化 NBT 查看器 (#3653)
* update

* update

* update
2025-02-26 01:40:36 +08:00
Glavo ff4940e79f 添加关闭启动器时的淡出动画 (#3652)
* 添加关闭启动器时的淡出动画

* update
2025-02-26 01:04:12 +08:00
Glavo 1c163934ee Fix #2048: 当微软账户 Token 过期时应当刷新账户 (#3651) 2025-02-25 20:50:49 +08:00
Glavo c513d00c50 添加启动动画 (#3442)
* update

* update

* update
2025-02-25 18:20:44 +08:00
3gf8jv4dv 2c9549b36b fix(ui): limit width of FileSelector in offline skin pane (#3623)
make the text around the selector not be covered.

resolves https://github.com/HMCL-dev/HMCL/issues/3296
2025-02-25 17:58:31 +08:00
Glavo 3389e0a934 Close #3649: 点击账户卡片空白处时切换账户 (#3650)
* Close #3649: 点击账户卡片空白处时切换账户

* update
2025-02-25 17:36:51 +08:00
3gf8jv4dv 78149449a9 chore(locale): improve russian translation (modrinth category) (#3647)
- Add missing translations
- Fix existing translations
2025-02-25 17:33:07 +08:00
3gf8jv4dv b792b23ad4 chore(locale): improve russian translation (web) (#3648) 2025-02-25 17:32:27 +08:00
3gf8jv4dv 881b0e9127 chore(locale): improve russian translation (account part) (#3642)
- Re-translate some translations based on English
- Fix some inconsistent terminology
  - Such as Account, учетная запись > аккаунт
- Add some missing translations
- Reorder some keys to ensure they match with English
- Adjust terminology related to LittleSkin to ensure consistency with
the terminology used on the site
2025-02-23 10:48:45 +08:00
3gf8jv4dv 77e06c8820 chore(locale): adjust english translation of account.portable (#3643)
Match the style of the account type shown for the account item
on account list page
2025-02-23 10:48:03 +08:00
Glavo bc2405cf12 修复在 macOS 平台无法找到通过 DMG 安装的 Java 的问题 (#3645) 2025-02-23 10:47:25 +08:00
Glavo 72b5428f8b 修复 NeoForge 版本识别错误的问题 (#3638) 2025-02-22 17:28:25 +08:00
Glavo e7e8d45497 禁止整合包复制全局游戏设置 (#3639)
* 禁止整合包复制全局游戏设置

* update
2025-02-22 14:35:54 +08:00
3gf8jv4dv 82dbe18d78 fix(i18n): fix a missing i18n for LittleSkin term (#3637)
The word "LittleSkin" appears twice in OfflineAccountSkinPane, but one
of them is hardcoded. Add i18n support for the hardcoded one.
2025-02-22 08:36:34 +08:00
3gf8jv4dv ecb7ef0867 fix(locale): remove accidental japanese characters from russian (#3636)
This bug first appeared in https://github.com/HMCL-dev/HMCL/pull/2412
2025-02-22 00:50:59 +08:00
Glavo d40742a624 Bump jsoup to 1.18.3 (#3632) 2025-02-19 16:28:46 +08:00
Glavo 6f8002a0cf Fix typo in FXUtils (#3631) 2025-02-19 15:34:08 +08:00
3gf8jv4dv 3d3896c5c4 fix(i18n): add i18n support for steve & alex option in offline skin pane (#3630)
Some Slavic Minecraft communities have translated Steve and Alex,
such as Russian and Ukrainian. They need a translation.

ref:
- https://ru.minecraft.wiki/w/%D0%A1%D0%BA%D0%B8%D0%BD
- https://uk.minecraft.wiki/w/%D0%A1%D0%BA%D1%96%D0%BD
2025-02-19 15:30:34 +08:00
Glavoand3gf8jv4dv a24f8e2ec0 在游戏设置中添加“复制全局游戏设置”选项 (#3628)
* update

* update

* Fix checkstyle

* update

* fix checkstyle

* 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>

* Fix FXUtils.bindEnum

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-02-19 15:29:50 +08:00
Glavo fce08a6923 删除全局游戏设置页面的离线账户更换皮肤提示 (#3629) 2025-02-19 13:38:51 +08:00
Glavo 5fa01c8838 修复在非官方支持的平台上 Minecraft 1.21 与 Sodium 不兼容的问题 (#3622)
* update

* update

* update
2025-02-18 23:58:16 +08:00
Glavo 15a3fb5f59 优化 VersionsPage 搜索框 (#3625)
* update

* update

* update

* update
2025-02-18 23:51:28 +08:00
Glavo d5ab14f476 Fix #3295: 修复离线账户选择 Steve/Alex 皮肤时模型错误的问题 (#3626)
* Fix #3295: 修复离线账户选择 Steve/Alex 皮肤时模型错误的问题

* update
2025-02-18 22:46:52 +08:00
Glavo 554ecfd6d5 Fix #3445: 点击安装整合包时无条件关闭整合包下载对话框 (#3621) 2025-02-17 23:35:58 +08:00
Glavoand3gf8jv4dv c2abe755f4 优化整合包下载对话框 (#3620)
* update

* 优化整合包下载对话框

* update

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

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

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-02-17 22:44:25 +08:00
Glavo c0410c46ff 在日志中记录 PROCESSOR_IDENTIFIER (#3619) 2025-02-16 21:10:58 +08:00
Glavo da93b4c12d 修复无法识别 Intel64 架构的问题 (#3618) 2025-02-16 17:23:14 +08:00
Zkitefly c16d6e8d5f 添加 NeoForge 的 BMCLAPI 代理支持 (#3605) 2025-02-15 16:38:30 +08:00
Glavo 6df73aeb68 修复语言设置中英语选项名称错误的问题 (#3612) 2025-02-15 12:19:09 +08:00
3gf8jv4dv 23d11eca81 fix(locale): adjust time formats for various languages (#3613)
- Change to a standard and commonly used format.
- Rename i18n key as it is used in more than one place.
2025-02-15 12:16:42 +08:00
Glavo 0e307f3b71 简化 AdvancedListBox (#3611) 2025-02-12 21:53:10 +08:00
Glavo 33b791f2b0 更新日期时间格式 (#3610)
* Revert "improv(i18n): refine time formats on ui (#3608)"

This reverts commit 741f65d37c.

* update
2025-02-12 19:37:05 +08:00
3gf8jv4dv de01946ab2 fix(locale): fix a typo in zh-TW language (#3606) 2025-02-12 17:35:03 +08:00
3gf8jv4dv 5526bac70e fix(locale): change Socks to SOCKS (#3607)
ref: https://en.wikipedia.org/wiki/SOCKS
2025-02-12 17:33:31 +08:00
3gf8jv4dv 741f65d37c improv(i18n): refine time formats on ui (#3608)
- unify time formats using DateTimeFormatter, deprecate `world.time`
- change FormatStyle to make the time detail appropriate
- fix incorrect time format in English locale
2025-02-12 17:32:36 +08:00
Haowei Wen ecafc9d6f0 Fix height computation in ComponentListCell 2025-02-11 23:51:50 +08:00
Glavo 8795d522b0 支持临时隐藏预览版提示 (#3603) 2025-02-11 19:00:03 +08:00
Glavo d8e7e0e97e 更新 mod 数据 (#3601)
* 更新 mod 数据

* update

* update

* update
2025-02-10 22:04:48 +08:00
Glavo 8236dfc7e5 Fix #3595: patchNative may throw NPE when gameVersion is null (#3602) 2025-02-10 19:59:25 +08:00
Glavo 8a15b7d274 修复使用自定义字体时未正确设置字重的问题 (#3598) 2025-02-10 00:41:31 +08:00
3gf8jv4dv fce4c13515 fix(locale): fix zh-tw locale for game crash window buttons (#3596) 2025-02-09 09:28:00 +08:00
Glavo 2f36506099 Fix #3508: Java 下载对话框中切换包类型时不应重置版本字段 (#3594)
* Fix #3508: Java 下载对话框中切换包类型时不应重置版本字段

* fix checkstyle
2025-02-08 18:02:46 +08:00
Glavo 5e60eaf595 修复 macOS 平台 HMCLauncher.sh 不会显示 Java 下载页面的问题 (#3549) 2025-01-29 07:35:19 +08:00
Glavo f70fb4ae08 Update README (#3548) 2025-01-29 07:27:17 +08:00
3gf8jv4dv 38adb48051 Fix qq badge in readme (#3544)
ref: https://shields.io/blog/simple-icons-14
2025-01-25 16:38:55 +08:00
Glavo f972c49762 Fix #3539: 安装 MultiMC 整合包后应当设置图标 (#3541) 2025-01-24 09:11:22 +08:00
Glavo 5afeb3d019 优化 HMCLauncher.sh (#3538) 2025-01-23 18:21:29 +08:00
Burning_TNT 38897fbd2a Fix #3063 修复本地模组管理界面水平滚动条显示异常的问题 (#3463) 2025-01-23 13:51:55 +08:00
ZkiteflyandGlavo 0562042218 Update HMCLauncher.sh (#3265)
* Update HMCLauncher.sh

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-01-23 13:17:33 +08:00
Glavo c5e7ee8aef 优化对 Linux RISC-V 64 平台的支持 (#3536) 2025-01-23 10:23:27 +08:00
Glavo 35b288b800 Fix #3529: 修复在 FreeBSD x86-64 平台未正确替换 LWJGL 本地库的问题 (#3531)
* Fix #3529: 修复在 FreeBSD x86-64 平台未正确替换 LWJGL 本地库的问题

* update

* update

* update
2025-01-20 14:03:11 +08:00
Zkitefly a609d32081 更新版权年份至2025 (#3527) 2025-01-16 11:26:31 +08:00
Haowei Wen 72d8b75643 模组列表页中按下 Esc 时取消选中 (#3523)
* 模组列表页中按下 Esc 时取消选中

目前在模组列表页中选中模组后,按下 Esc 是不会有任何反应的,因为 [ListView](https://github.com/openjdk/jfx/blob/5b074c4c2b4f539301813322a6589cec28121553/modules/javafx.controls/src/main/java/com/sun/javafx/scene/control/behavior/ListViewBehavior.java#L108) 会处理这个事件,认为是取消编辑,而不论是否处于编辑模式中。

这个 PR 添加了 `FXUtils.ignoreEvent()` 函数,用于忽略掉对特定事件的处理。然后,我们就可以让 ListView 不处理 Esc 按下事件,然后在模组页添加 Esc 处理逻辑(必须让 ListView 忽略掉这个事件,不然就会被它优先处理)。

理论上所有用了 ListView / JFXListView 的地方都会有这个问题,即当焦点在 ListView 中时,按下 Esc 不会触发正常的事件处理(如返回上一页、关闭对话框)。比如在游戏下载页面中,对某个版本按下右键后,焦点就会转移到其上,此时再按 Esc 就毫无反应。

* 修复 Esc 在版本下载页/模组下载页/数据包页不工作
2025-01-16 11:15:29 +08:00
3gf8jv4dvandyushijinhun 01f9a452f1 Update ISSUE_TEMPLATE [skip ci] (#3446)
* Update ISSUE_TEMPLATE [skip ci]

* Apply suggestions from yushijinhun

Co-authored-by: yushijinhun <yushijinhun@gmail.com>

---------

Co-authored-by: yushijinhun <yushijinhun@gmail.com>
2025-01-06 03:40:49 +08:00
3gf8jv4dvandyushijinhun f2edc8001c Revise Spanish & English (#3465)
* Revise Spanish & English

Overhaul for Spanish. I cannot guarantee it is 100% correct, but I
promise to proofread it. During the overhaul some more mistakes in the
English language were discovered, so let's fix it.

All translations have been revised except for the `game.crash.reason`
series.

* Apply suggestions from yushijinhun

Co-authored-by: yushijinhun <yushijinhun@gmail.com>

---------

Co-authored-by: yushijinhun <yushijinhun@gmail.com>
2025-01-06 03:37:19 +08:00
3gf8jv4dvandyushijinhun d2df515325 Revise README [skip ci] (#3447)
* Revise README [skip ci]

* Apply suggestions from yushijinhun

Co-authored-by: yushijinhun <yushijinhun@gmail.com>

---------

Co-authored-by: yushijinhun <yushijinhun@gmail.com>
2025-01-06 03:32:42 +08:00
3gf8jv4dv 95e39fe87a gha: revise release description [skip ci] (#3474)
unify text style
2025-01-06 03:12:13 +08:00
lijiaxuanand3gf8jv4dv 662bf1a9d4 Update I18N.properties English Translations (#3477)
* Update I18N.properties English Translations

更新了英文语言翻译文件,修复了一些语法错误并且让一些句子更加通顺

* Update I18N.properties

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

* Update I18N.properties

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

* Update I18N.properties

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

* Update I18N.properties

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

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

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

* Update I18N.properties

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

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

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

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

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2025-01-06 02:15:58 +08:00
Zkiteflyand3gf8jv4dv bfda297838 检查安装时的非ASCII字符名称 (#3244)
* 检查安装时的非ASCII字符名称

* i18n

* 添加导出模组列表信息功能

* Revert "添加导出模组列表信息功能"

This reverts commit 6505d1c284.

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

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>

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

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>

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

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>

---------

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>
2025-01-05 01:31:44 +08:00
ZkiteflyandGlavo d80f61e2e7 update getAssetIndex() (#3252)
* update getAssetIndex()

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2025-01-04 21:33:48 +08:00
Zkitefly 1f7b156423 Press ESC in search box to exit search (#3519) 2025-01-04 01:38:11 +08:00
Haowei Wen 56d20a5d08 Fix problems with window resizing in Linux 2025-01-04 01:35:36 +08:00
Haowei Wen b5cea83a8a Workaround for JDK-8344372
https://bugs.openjdk.org/browse/JDK-8344372
2025-01-03 20:15:03 +08:00
Haowei Wen 397edaec5d Press ESC in search box to exit search 2025-01-03 17:08:19 +08:00
Zkitefly 39b8de4824 fix 2025-01-03 17:08:19 +08:00
Zkitefly 5197157a97 添加搜索功能 2025-01-03 17:08:19 +08:00
Glavo d61826ba4c 优化 Release Note (#3516) 2025-01-01 05:37:30 +08:00
Glavo 85886da770 Bump Mesa to 24.3.2 (#3515)
* Bump Mesa to 24.3.2

* update
2025-01-01 05:14:14 +08:00
Haowei Wen 6f53da159a Refine DownloadPage
* Fix cursor is not hand when hovering on ModItem
* Fix clicking on the ModItem in ModVersion dialog would open a new dialog again
* Fix ESC doesn't work in ModVersion dialog
2024-12-31 19:56:38 +08:00
Haowei Wen 78e15d130b Update memory status every 1s, fix #3440
This commit also fixes Lang.getTimer() returns a non-daemon Timer,
preventing Java from exiting after closing the launcher.
2024-12-30 20:14:04 +08:00
Haowei Wen 7d12ef6b09 Fix memory slider indicator is covered by progress bar 2024-12-30 19:39:42 +08:00
Haowei Wen 260f4b6c5d Fix ESC not working in input dialog 2024-12-30 00:17:00 +08:00
Haowei Wen 104534d12f Fix style 2024-12-30 00:07:43 +08:00
Haowei Wen 81b3911dc9 Fix value in text field might not be saved when pressing ESC
https://github.com/HMCL-dev/HMCL/pull/1869#issuecomment-2335027530
2024-12-30 00:02:32 +08:00
Haowei Wen c5b8aabc57 Refactor VersionSetting.clone() & .addListener(), fix javaVersionType is not saved 2024-12-29 22:36:40 +08:00
Haowei Wen f0a064afdd Fix sometimes version specific setting is lost 2024-12-29 22:18:15 +08:00
Haowei Wen 8a4f6af193 Fix NPE due to the order of attaching specificSettingsListener 2024-12-29 21:40:09 +08:00
Haowei Wen e63ee49eaa Remove VersionSetting.global property as it's unused 2024-12-29 20:59:47 +08:00
Haowei Wen d5408c38b3 Copy global settings when specialize version setting 2024-12-29 20:18:58 +08:00
Haowei Wen 5fd52bb808 Move skin cache directory to hmcl dir, close #2970 2024-12-29 18:28:23 +08:00
白铭骢 (Mingcong Bai) cb2b3525b8 fix(DefaultLauncher): also set LIBGL_KOPPER_DRI2=1 for Zink mode (#3503)
The amdgpu DDX is missing support for modifiers, causing Zink to fail.
Disable DRI3 to workaround this issue.

Link: https://gitlab.freedesktop.org/mesa/mesa/-/issues/10093

Signed-off-by: Mingcong Bai <jeffbai@aosc.io>
2024-12-25 19:55:42 +08:00
Glavo 4c6fdf49de Bump JetBrains Annotations to 26.0.1 (#3493) 2024-12-13 00:29:34 +08:00
Burning_TNT 981b55a10e Fix #3456 在更新预览界面没法拖动启动器窗口 (#3464) 2024-12-10 16:20:02 +08:00
Glavo 8460df7d5d 修复 OAuth 登录时轮询过于频繁的问题 (#3490)
* 修复 OAuth 登录时轮询过于频繁的问题

* update
2024-12-09 17:47:42 +08:00
Glavo 727e6746d2 Update versions.txt (#3484)
* Update versions.txt

* update
2024-12-04 12:29:17 +08:00
Glavo 8e0f0e2a38 使用 ONE_PASS_BOX 阴影代替高斯模糊 (#3458)
* 使用 ONE_PASS_BOX 代替高斯模糊

* update

* update

* update
2024-12-02 21:24:58 +08:00
Glavo 3df52b0e8f Fix typo in check-update.yml (#3479) 2024-11-29 20:06:08 +08:00
Glavo 6f0e133bb0 添加 banshanjdk-8 下载地址 (#3476)
* 添加 banshanjdk-8 下载地址

* update

* fix i18n

* fix typo
2024-11-28 20:34:12 +08:00
Glavo 36da64f796 支持加载 WebP 格式的背景图片 (#3457)
* 支持加载 WebP 格式的背景图片

* update

* update

* update

* update
2024-11-28 00:35:08 +08:00
Glavo 4c242d0c80 不再生成 MD5 checksum (#3473) 2024-11-24 14:10:58 +08:00
Glavo 0a230ef6b5 优化 Release Note (#3472) 2024-11-23 19:48:46 +08:00
Burning_TNT c12ae9fdf8 Fix #3460 无法识别奇异的 Forge (Like) 模组 Metadata (#3470) 2024-11-21 21:22:20 +08:00
Glavo 773ef4282c Bump action-gh-release to v2 (#3469) 2024-11-17 22:09:29 +08:00
Burning_TNTandYELANDAOKONG 9edfe29659 实现 #2969: 启动前日志列出mods目录的jar文件 (#3125)
* Support #2969: 启动前日志列出mods目录的jar文件

* Support #2969: 启动前日志列出mods目录jar文件

* Support #2969: 启动前日志中使用字符画列出mods目录模组文件

* Support #2969: 启动前日志中使用字符画列出mods目录模组文件

* Support #2969: 启动前日志中使用字符画列出mods目录模组文件

* Support #2969: 日志列出mods目录时不再过滤文件后缀

* Support #2969: 启动时日志列出mods目录(代码优化)

* Support #2969: 启动时日志列出mods目录(不再列出Zip内容)

* Support #2969

* Fix: checkstyle

---------

Co-authored-by: YELANDAOKONG <yelandaokong@yldk.xyz>
2024-11-16 23:29:53 +08:00
Burning_TNT 5591d92e25 Fix #2991: 导入 PrismLauncher 格式整合包时 $.jvmArgs 参数未正确移除两侧引号 (#3030) 2024-11-15 22:49:30 +08:00
Glavo 65bdb29e71 在 macOS 上使用 JavaFX 19.0.2.1 (#3461) 2024-11-15 18:02:59 +08:00
Glavo 355df211db 在游戏下载列表中添加 Minecraft Wiki 链接 (#3435) 2024-11-14 17:49:27 +08:00
3gf8jv4dv fa66549f53 Revise English (#3453)
- Adjust some translation styles again
- Fix some missed mistakes
2024-11-14 09:59:15 +08:00
3gf8jv4dv 6e48dbd6c6 Fix dialog title when modpack download fails (#3451)
* Fix dialog title when modpack download fails

* Update
2024-11-14 09:58:14 +08:00
Zkitefly e3c7278759 Update check-update.yml (#3452) 2024-11-14 09:42:43 +08:00
3gf8jv4dv dd1829d8ca Check word 其它 → 其他 when checking translation (#3455)
There is some controversy over the use of this word.
In order to ensure consistency in formal texts, we
should unify them to more tried-and-true and commonly
used usages.

By checking at build time, it lets the translator
or proofreader correct the word on his/her own without
any special attention from the reviewer.
2024-11-14 09:27:45 +08:00
3gf8jv4dv 60ac16a3ea Revise Chinese (#3454)
* Revise Chinese

- Simplified Chinese only
- Minor revision

* oops...
2024-11-13 22:14:55 +08:00
Glavo b400a9a3ef 使最小化按钮居中 (#3450)
* 修复最小化按钮未居中的问题

* update
2024-11-12 14:24:15 +08:00
3gf8jv4dv c7cc2d0733 Fix missing i18n when checking for updates fails (#3444)
* Fix missing i18n when checking for updates fails

* Revise translation
2024-11-11 19:20:28 +08:00
Glavo 861223e909 清理 GameJavaVersion (#3441)
* 清理 GameJavaVersion

* update
2024-11-11 02:30:41 +08:00
Glavo 70ecbc734e Fix #3438: 修复 Minecraft 1.20.2+Forge 默认图标错误的问题 (#3439) 2024-11-10 23:19:14 +08:00
ZkiteflyandGlavo 2ca7e7c830 Fix #3200: 启动时不应补全 Modrinth 整合包中已禁用的 mod (#3308)
* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* Update ModrinthCompletionTask.java

* update

* update

* update

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-11-10 21:27:36 +08:00
Wulian 9a0db56ac3 修复 Copyright © 显示为“漏”的bug (#3437)
Fixes #3436

Specify UTF-8 charset when reading `deps.json` file in `AboutPage.java`.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/HMCL-dev/HMCL/issues/3436?shareId=XXXX-XXXX-XXXX-XXXX).
2024-11-10 19:02:59 +08:00
Glavo c1ae5875ba 将 VERSION_ROOT 更新至 3.6 (#3431) 2024-11-10 13:01:50 +08:00
Glavo 1d0ae427bf Update JavaVersionConstraint: 1.20.5 requires Java 21 (#3434) 2024-11-09 19:39:09 +08:00
3gf8jv4dv 3dde2d19e2 Fix extra "=" in zh_CN launch.advice translation (#3430)
Closes #3429
2024-11-08 05:59:02 +08:00
3gf8jv4dv d5ec6d7b85 Revise i18n translation before releases (#3428)
- Fix some English grammar and text style
- Revise `Allow Cheats` to `Allow Commands/Cheats`
  - Due to the change from `Allow Cheats` to `Allow Commands` by Mojang in 24w04a.
  - https://minecraft.wiki/w/Java_Edition_24w04a
2024-11-07 21:37:59 +08:00
Glavo 13856b71a0 更新 HMCLauncher.exe (#3427) 2024-11-05 03:43:18 +08:00
Glavo c6658771ca 修复 HMCLauncher 无法正常识别 Windows on Arm 平台的问题 (#3425)
* update

* update

* update
2024-11-05 00:20:42 +08:00
Glavo 406f36584d 设置 FontComboBox 的最小宽度 (#3424) 2024-11-04 18:55:04 +08:00
Glavo 898750d058 修复对话框按钮自行移动的问题 (#3423)
* 修复对话框按钮自行移动的问题

* update

* update

* update
2024-11-04 18:41:14 +08:00
Glavo 09e9e934d3 Fix #3412: 修复点击前置模组时,模组下载对话框不会关闭的问题 (#3422) 2024-11-04 06:00:45 +08:00
Glavo 3d170267b7 Fix #3416: 修复打开游戏设置页时崩溃的问题 (#3421) 2024-11-04 05:34:28 +08:00
3gf8jv4dv 3ca7e3cf88 Fix incorrect i18n key and line breaks in es & ja (#3404)
* Fix incorrect i18n key and line breaks in es & ja

* fix
2024-11-03 17:03:41 +08:00
3gf8jv4dv 1378d1445a README & PLATFORM docs post-fixes (#3417) 2024-11-02 17:36:41 +08:00
3gf8jv4dv 16d304dbb4 en_US Post-fix (#3413)
* en_US Post-fix

Some minor fixes.

* fix `game.crash.feedback`

* oops...

* oops...

* Bunch of grammar fixes :/

* Another bunch fixes

* Minor fixes

* fix

* fix

* fix
2024-11-02 17:36:20 +08:00
3gf8jv4dv aff5db7759 Change the terminology %AppData% to %APPDATA% (#3420) 2024-11-02 02:27:54 +08:00
Glavo 0697aa9755 重命名包 moe.mickey.minecraft.skin.fx (#3419) 2024-11-02 01:05:52 +08:00
Glavo 995d3bd4b9 不再生成 bss 文件 (#3418) 2024-11-01 20:59:33 +08:00
Glavo 3bf5204e79 优化 Theme (#3414) 2024-11-01 02:12:16 +08:00
3gf8jv4dv 0b1ce6e94a Adjust language selection order (#3411)
Sort alphabetically by language code.
2024-10-31 23:30:44 +08:00
3gf8jv4dv a66ccd7385 Adjust Microsoft login related i18n (#3393)
* Adjust Microsoft login related i18n

- Adjust English and Chinese
- Fix typos and grammatical errors

* fix
2024-10-30 02:26:55 +08:00
3gf8jv4dv 7a3f125802 Adjust custom command hint (zh_CN only) (#3394)
I forgot to change the zh_CN part.
A half-width colon is better.
2024-10-30 02:22:34 +08:00
3gf8jv4dv 4a8dcce3d4 Adjust about page i18n (#3398)
* Adjust about page i18n

- Adjust all languages
- Add missing translations

* fix

* fix
2024-10-30 02:21:54 +08:00
3gf8jv4dv adcc3eb43f Adjust buttons, chat & crash i18n (#3401)
* Adjust buttons, chat & crash i18n

- Add missing translations

* fix

* fix

* fix

* fix ja

* fix es & ja
2024-10-30 02:20:39 +08:00
3gf8jv4dv 8bbd1a7cf4 Remove unused i18n keys in es, ja & ru (#3405) 2024-10-30 02:19:45 +08:00
Zkitefly 467ff26278 模组管理添加右键查看模组详情 (#3232)
* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java

* Update ModListPageSkin.java
2024-10-30 02:19:16 +08:00
Glavo d6a267afa4 优化 FXUtils.installTooltip (#3407)
* update

* Fix checkstyle

* update

* update

* update

* update

* update
2024-10-28 01:52:38 +08:00
Glavo 743e934436 优化“复制”图标 (#3406) 2024-10-28 00:29:29 +08:00
3gf8jv4dv f89ac2231a Adjust Java management i18n (#3402)
- Add missing languages.
2024-10-27 22:07:25 +08:00
3gf8jv4dv 1877a18ebb Fix en language missing a line break (#3403) 2024-10-27 22:07:03 +08:00
Glavo c99f85d9d5 缩小复制图标 (#3397)
* update

* update
2024-10-27 00:21:31 +08:00
Glavo 228942c60a 优化删除账号按钮图标 (#3396) 2024-10-26 23:49:43 +08:00
Glavo fecd567eab 简化 ContainerAnimations (#3395) 2024-10-26 23:34:41 +08:00
3gf8jv4dv 602840519b Change forgot password link (#3392) 2024-10-26 23:32:12 +08:00
959adf1075 更改主页 UI (#3129)
* Change root page UI

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

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>

* Shorten I18N

* update

---------

Co-authored-by: 3gf8jv4dv <158490944+3gf8jv4dv@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-26 21:02:34 +08:00
Glavo 422f681fe6 Bump XZ to 1.10 (#3391) 2024-10-26 20:54:48 +08:00
Glavo 474d298fd3 Bump Gradle to 8.10.2 (#3390) 2024-10-26 13:52:49 +08:00
3gf8jv4dv 4052136217 Adjust sponsor hint i18n (#3387)
- Adjust for all languages.
- For BMCLAPI: Make it clear that this is a service
  for users in Chinese Mainland.
- For HMCL: Since the page that was opened was not
  Afdian but the GitHub repo page, I made adjustments
  based on the actual situation.
- Fix some typos.
2024-10-26 02:51:19 +08:00
3gf8jv4dv 3193e02867 Adjust feedback page i18n (#3389)
- Adjust for all languages.
- Add missing translations to some languages.
2024-10-26 02:49:58 +08:00
Glavo c7122b3fff 优化首页公告 (#3378)
* update

* 添加公告隐藏动画

* update

* update
2024-10-26 02:45:58 +08:00
3gf8jv4dv 8775194d26 Adjust i18n for Linux/FreeBSD-specific settings in Advanced Settings (#3388)
I checked the code and it looked like this was a
Linux/FreeBSD-specific feature, so I adjusted the
translation for that.
2024-10-26 01:14:28 +08:00
3gf8jv4dv b3f27cc96e Adjust homepage hint typesetting (#3382)
- Adjustments made for all languages.
- Remove outdated hidden hint links.
2024-10-25 22:30:12 +08:00
3gf8jv4dv 3313a1ed27 For language selection, remove all region descriptions (#3383)
Currently, HMCL does not provide variants for
languages ​​(e.g. US, UK, etc.). For Chinese, there
are already descriptions of Simplified (简体) and
Traditional (繁體) to distinguish them, so keep them.
2024-10-25 22:05:19 +08:00
3gf8jv4dv da66622994 Adjust Microsoft login hint (zh_CN only) (#3384) 2024-10-25 22:04:48 +08:00
3gf8jv4dv 446cdc32d7 Adjust language file header comments (#3385)
- Update copyright year
- Unify typesetting style
2024-10-25 22:03:52 +08:00
3gf8jv4dv b08d2b0dc0 Remove some unnecessary tooltips (#3386)
* Remove some unnecessary tooltips

These texts are basically fixed, and the problem of
too long texts can be solved by revising the translation
for different languages.

Currently, for both Chinese and English, the i18n
keys involved in this PR will not be partially obscured
due to long text, so their tooltips are removed.

* fix checkstyle
2024-10-25 16:11:16 +08:00
Glavo e051c212d1 支持使用鼠标返回键返回上级 (#3381)
* 支持使用鼠标返回键返回上级

* fix checkstyle
2024-10-25 12:36:07 +08:00
Glavo 7645a600ef 修复部分控件响应任意鼠标按键点击事件的问题 (#3380)
* update

* update

* update

* update

* update

* update

* update

* update

* update
2024-10-25 12:24:57 +08:00
Glavo 9e56b5cd15 清理对 setOnMouseClicked 的误用 (#3379) 2024-10-25 04:08:40 +08:00
3gf8jv4dvandGlavo dcfcd1d8b2 Revise zh_TW part 3 (#3358)
* Revise zh_TW part 3

* Update

* Update

* fix typo

* fix

* fix

* oops...

* fix

* fix main page hint

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

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-25 02:25:26 +08:00
3gf8jv4dv b2dc4b45c2 Revise zh_TW part 2 (#3357)
* Revise zh_TW part 2

* Update

* fix typo

* fix

* fix

* fix

* fix

When choosing a download source, the commas in the BMCLAPI item are changed to half-width.
2024-10-25 01:47:22 +08:00
0314921728 Revise en_US (#3367)
* Revise en_US

* fix

* fix

* Revise

Co-authored-by: Zkitefly <2573874409@qq.com>

* fix some English grammar

* another English grammar fixes

* fix fix fix...

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: Zkitefly <2573874409@qq.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-25 01:28:02 +08:00
Glavo 7d339677e4 调整公告中超链接的风格 (#3377)
* 调整 MessageDialogPane 中超链接的风格

* Fix checkstyle

* fix
2024-10-24 21:50:05 +08:00
3gf8jv4dv c9d259d7f1 zh_CN post-fix (#3376)
- When choosing a download source, the commas in the BMCLAPI item are changed to half-width.
- Remove extra line breaks in the home page hint.
2024-10-24 21:32:37 +08:00
Glavo eae2670855 支持在启动器内上传微软账号皮肤 (#3375)
* update

* Fix clearCache
2024-10-24 20:03:21 +08:00
3gf8jv4dvandGlavo e857f19b18 Revise zh_CN part 4 (#3349)
* Revise zh_CN part 4

* Update

* fix some typo

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

* Revise

Co-authored-by: Glavo <zjx001202@gmail.com>

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-24 14:25:37 +08:00
Glavo a11816884c 创建 @FXThread 注解 (#3374) 2024-10-24 01:01:31 +08:00
3gf8jv4dv f686cb0ceb Update versions.txt (#3373) 2024-10-23 23:00:02 +08:00
Glavo 5aac4b82fc 优化 Java 下载功能 (#3371)
* update

* update

* update

* update
2024-10-23 21:49:35 +08:00
Glavo 99c3ef8a57 优化 Java 下载功能 (#3369)
* update

* update

* update
2024-10-20 22:26:38 +08:00
3gf8jv4dv 0a17aad9d6 Revise readme (#3362) 2024-10-20 21:36:01 +08:00
lijiaxuanand3gf8jv4dv 965995ad20 略微修改了一下英文版的readme.md (#3354)
* Updated English version of readme.md

修改了英文版的readme.md,并未增加或删除内容,只是让一些句子更加自然了一些

* Update README.md

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

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2024-10-20 21:35:27 +08:00
Glavo a5067cb1dd Bump JavaFX to 17.0.13 (#3364) 2024-10-20 21:33:55 +08:00
3gf8jv4dv 1f9c0736aa Capitalize choose player dialog title text (#3368) 2024-10-20 21:33:35 +08:00
Glavo a27671560d 支持下载 OpenJ9 (#3363)
* 支持下载 OpenJ9

* update
2024-10-19 22:59:40 +08:00
Glavo 5c767deaf3 优化 WorldInfoPage (#3361)
* update

* update

* update
2024-10-19 22:25:47 +08:00
3gf8jv4dv c157d4e0d1 Revise zh_TW part 4 (#3359) 2024-10-19 21:48:30 +08:00
3gf8jv4dv 655f42734a Revise zh_TW part 1 (#3356)
* Revise zh_TW part 1

* Update

* Update

* Update
2024-10-19 21:41:54 +08:00
3gf8jv4dv 55b01ab101 Revise zh_CN part 3 (#3348)
* Revise zh_CN part 3

* Update
2024-10-19 21:35:57 +08:00
3gf8jv4dv c6ed42d3e6 Revise zh_CN part 2 (#3346)
* Revise zh_CN part 2

* Update

* Oops...
2024-10-19 21:35:22 +08:00
0562051989 Fix mod update channel i18n (#3331)
* Fix mod update channel i18n

* Update

Co-authored-by: Burning_TNT <pangyl08@163.com>

* update

---------

Co-authored-by: Burning_TNT <pangyl08@163.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-19 21:34:11 +08:00
3gf8jv4dvandBurning_TNT f68e056e47 Revise zh_CN part 1 (#3343)
* Revise zh_CN part 1

* Update

Co-authored-by: Burning_TNT <pangyl08@163.com>

---------

Co-authored-by: Burning_TNT <pangyl08@163.com>
2024-10-19 21:28:03 +08:00
Burning_TNTandGlavo 457490276c Update CallerFinder (#3350)
* Update CallerFinder

* Revert "Fix #2687 (#2688)"

This reverts commit 207b4d0033.

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-18 02:17:47 +08:00
Glavo 49ea9e5da0 fix checkstyle (#3353) 2024-10-17 12:48:17 +08:00
Glavo 9f3fb7aff6 修复无法安装 zip 格式的 Java 压缩包的问题 (#3352) 2024-10-17 04:20:09 +08:00
Glavo 1033389c38 Revert "Bump commons-compress to 1.27.1 (#3344)" (#3351)
This reverts commit fab32a6d51.
2024-10-16 22:15:13 +08:00
Glavo 58e32c4459 当整合包没有描述时,隐藏查看整合包描述按钮 (#3347) 2024-10-15 19:56:32 +08:00
Glavo 936285847c Bump Gson to 2.11.0 (#3345) 2024-10-15 19:10:02 +08:00
Glavo fab32a6d51 Bump commons-compress to 1.27.1 (#3344) 2024-10-15 19:05:22 +08:00
Glavo 5b345affec 优化 Java 下载功能 (#3342)
* update

* update
2024-10-15 18:47:57 +08:00
3gf8jv4dv 32c3b694b3 Revise readme and platform docs [skip ci] (#3054)
* Revise [skip ci]

Squash previous commits.

* Revise [skip ci]

Unify badge style.

* Revise [skip ci]

Change the badge style of QQ group

* Revise [skip ci]

- Add Chinese variants (Traditional, Taiwan)
- Compilation requirements for Java bump to 11+
- Various revisions

* Revise [skip ci]

Fix a link

* Revise [skip ci]

Revert a change

* Revise [skip ci]

* Revise [skip ci]

* Revise [skip ci]
2024-10-15 18:47:38 +08:00
3gf8jv4dv 4dce13855a Fix some i18n key mistake for es, ja & ru (#3341) 2024-10-15 18:26:12 +08:00
Burning_TNTandGlavo 1ce9858893 Support #2992: 检查模组更新界面加入全选/取消全选复选框 (#3032)
Support #2992

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-15 04:06:40 +08:00
Glavo 71c9632d61 GitHub Action 使用 Java 8 构建 HMCL (#3340)
* update

* update
2024-10-15 03:13:35 +08:00
3gf8jv4dv cce17edaa6 Capitalize text above the sidebar line (#3336) 2024-10-15 01:24:44 +08:00
3gf8jv4dvandGlavo deeef8f67a Download source and thread setting titles i18n key (#3338)
* Change the download source and thread setting titles to the appropriate i18n key

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-10-15 01:19:11 +08:00
3gf8jv4dv 25c76a748f Fix alignment of host and port in proxy settings (#3332) 2024-10-15 01:06:21 +08:00
3gf8jv4dv 2109df8145 Fix some link (#3334)
* Fix some link

* Revise
2024-10-15 00:58:40 +08:00
3gf8jv4dv ae01a49cc4 Fix some error message wrap (#3333)
* Fix some error message wrap

* Fix checkstyle
2024-10-15 00:57:23 +08:00
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
Glavo 1187ae4f04 Fix #2938: 修复 DefaultLauncher 会多次添加包装命令的问题 (#2939) 2024-03-16 11:08:17 +08:00
Glavo 8efdc536ee Fix #2934: 修复无法读取远古版本版本号的问题 (#2936)
* update

* update
2024-03-15 20:57:45 +08:00
Glavo 78f080b6cc Fix #2784: 离线账户选择 Steve/Alex 皮肤时强制使用本地 Yggdrasil 服务器 (#2933) 2024-03-15 13:39:50 +08:00
Glavo cc8bcfd924 Fix i18n (#2931)
* Fix i18n

* update
2024-03-15 12:56:29 +08:00
Glavo 806ca3f64a 更新 Modrinth 翻译 (#2929)
* update

* update

* update

* update

* update

* update
2024-03-15 12:24:43 +08:00
lyuxc_ 4a16fa13ee Update ModpackInfoPage.java (#2928) 2024-03-14 21:23:06 +08:00
Glavo b88d7c4fd2 修复 saveETagIndex 可能写入不全的问题 (#2926) 2024-03-14 19:24:30 +08:00
Burning_TNTandGlavo c0838468b1 Close #2911: 下载失败时打印重定向链 (#2912)
* Support #2911.

* update

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-03-14 13:50:25 +08:00
Glavo 90cfe3d0b7 加载网络背景时设置超时 (#2856) 2024-03-13 16:25:55 +08:00
Burning_TNT baf1d22bc4 Fix bugs. (#2920)
* Always sort the results while searching in CurseForge.

* Fix bugs.
2024-03-12 22:14:32 +08:00
Glavo 7ca5744559 允许使用系统属性禁用系统代理 (#2921) 2024-03-12 21:44:08 +08:00
Glavo 57e728991b Fix #1938: 正确处理 map_to_resources (#2919)
* Fix #1938: 正确处理 map_to_resources

* fix checkstyle
2024-03-12 20:01:30 +08:00
Glavo e08ba4d421 不再尝试下载 .pack.xz 文件 (#2917)
* 不再尝试下载 .pack.xz 文件

* update
2024-03-11 20:21:17 +08:00
Burning_TNT ba91c60e95 Always sort the results while searching in CurseForge. (#2916) 2024-03-10 22:45:34 +08:00
389b93178e fix #2796 (#2804)
* fix https://github.com/HMCL-dev/HMCL/issues/2796

* 调整 game.crash.feedback 显示位置

* fix i18n

* 添加导出崩溃信息失败的弹窗

* updata

* 在崩溃窗口的日志窗口添加更多信息

* fix i18n

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

Co-authored-by: 何杰豪 <hejiehao12@126.com>

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

Co-authored-by: 何杰豪 <hejiehao12@126.com>

* Revert "在崩溃窗口的日志窗口添加更多信息"

This reverts commit e8ed43291e.

* updata

* add log

* updat log

* Update GameCrashWindow.java

* 添加更多日志输出

格式:Processing log file: /home/zkitefly/.minecraft/hs_err_pid20074.log, processStartTime: 1709952457315, lastModifiedTime: 1709952504054

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

---------

Co-authored-by: 何杰豪 <hejiehao12@126.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-03-10 22:02:56 +08:00
Glavo 37aaf153f8 更新文本和链接 (#2915)
* update

* update

* update

* update

* update

* update
2024-03-10 20:29:44 +08:00
ShulkerSakuraandGlavo 2249f4ee0e 在未知桌面环境禁用 moveToTrash (#2885)
* Added Optifine Icon

* Updated Optifine Icons

* Updated Optifine Icons

* Added new HMCL logo

* update

* Add ChromeOS Detection

* ChromeOS Detection

* Removed useless import

* Style Checked

* Add private constructor

* Style Check

* Simplification

* update

* update

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-03-09 20:38:48 +08:00
Glavo eadfff349a Revert "auth: Always check token.exp when launch game (#2642)" (#2908)
This reverts commit 8678c0dcf2.
2024-03-09 20:25:59 +08:00
Glavo cb4cd696a0 修复未初始化 fileDownloadProvider 的问题 (#2906) 2024-03-08 16:26:37 +08:00
Glavo c85c074eee 更改语言应在重启后生效 (#2905) 2024-03-08 15:04:40 +08:00
Glavo b26c478d4a 更新 I18N (#2904)
* 更新 I18N

* 更新 I18N
2024-03-08 14:21:05 +08:00
Glavo 2d83de7c31 简化 InvocationDispatcher (#2674)
* 优化 InvocationDispatcher

* update

* Rename type parameter

* update
2024-03-08 10:06:08 +08:00
Glavo 20d93dd085 清理 GlobalConfig (#2903) 2024-03-08 10:01:01 +08:00
Glavo 24fc436870 将首页联机入口替换为入群链接 (#2900) 2024-03-06 18:29:51 +08:00
Glavo 3a6c23aa9d 将部分 KOOK 入口替换为 QQ 群 (#2902) 2024-03-06 14:38:13 +08:00
Glavo 0ee8b2dd82 指引用户加入 QQ 群 (#2897) 2024-03-05 23:57:25 +08:00
Glavo 7cce90bed8 删除 Mojang 登录支持 (#2889)
* 删除 Mojang 登录支持

* update

* update
2024-03-05 15:07:15 +08:00
Glavo c806a5d412 Update gradle.yml (#2895) 2024-03-05 14:22:45 +08:00
Glavo 14e8b9212f 更新 HMCLauncher 图标 (#2887) 2024-03-05 13:19:29 +08:00
Glavo 67e96fb84f 使用 GitHub 构建 HMCLauncher 后构建 HMCL (#2893)
* update

* update

* test

* update

* update

* update
2024-03-05 06:28:51 +08:00
Glavo c39c19f215 仅在必要时运行 GitHub Action (#2892)
* 仅在必要时运行 GitHub Action

* test

* test

* test

* update
2024-03-05 05:23:34 +08:00
Glavo 88d7493870 更新 GitHub Action 脚本 (#2891)
* update

* update
2024-03-05 05:11:21 +08:00
Glavo 404bc88ec8 使用 GitHub Action 构建 HMCLauncher (#2890)
* 使用 GitHub Action 构建 HMCLauncher

* test

* update

* update

* update

* update
2024-03-05 04:59:30 +08:00
Glavo e2355ca2ff 暂时移除赞助页面 (#2888) 2024-03-05 02:27:16 +08:00
Glavo aab271ac75 添加 GameJavaVersion.JAVA_21 (#2886)
* 将 GameJavaVersion 转换为 enum

* update
2024-03-05 00:35:29 +08:00
Glavo 0da0a1f386 默认不进行降级更新 (#2862)
* 默认不进行降级更新

* update
2024-03-04 16:19:30 +08:00
Glavo 59316a866d Fix #2880: 支持安装带有 NeoForge 的 Modrinth 整合包 (#2881) 2024-03-03 20:37:11 +08:00
zkitefly d3087bd763 更新 README (#2877) 2024-03-03 08:53:20 +08:00
Glavo df7b63c491 更新 README (#2872)
* 更新 README

* 更新 README

* update
2024-03-02 01:29:49 +08:00
Glavo dff893ba4b Fix #2870: 导出脚本时预解析 PreLaunchCommand 与 PostExitCommand (#2871)
* Fix #2870: 导出脚本时预解析 PreLaunchCommand 与 PostExitCommand

* fix checkstyle
2024-03-02 00:01:02 +08:00
Glavo c539702a1b 修复下载 Java 不会尝试备用下载源的问题 (#2869)
* 修复下载 Java 不会尝试备用下载源的问题

* Fix checkstyle

* Fix checkstyle
2024-03-01 21:38:04 +08:00
Glavo 6fad149410 更新 Release Note (#2868)
* 更新 Release Note

* update
2024-03-01 17:01:06 +08:00
Glavo a072fb7bea 改进任务栏图标质量 (#2864)
* 改进任务栏图标质量

* update
2024-02-29 02:03:42 +08:00
Glavo 08a3850dcf Fix #2861: MaintainTask 中不应调用 getGameVersion (#2863)
* Fix #2861: MaintainTask 中不应调用 getGameVersion

* update

* fix checkstyle
2024-02-29 00:06:50 +08:00
Glavo 030fba4630 Fix #2855: tokenize 应支持变量插值 (#2858)
* Fix #2855: tokenize 应支持变量插值

* update

* update

* update

* update

* update
2024-02-27 21:12:51 +08:00
Glavo 559aefd6b6 Fix #2854: 修复 LibraryAnalyzer.analyze 无法获得游戏版本的问题 (#2859)
* Fix #2854: 修复 LibraryAnalyzer.analyze 无法获得游戏版本的问题

* delete debug code

* update
2024-02-27 21:12:35 +08:00
Burning_TNT 2ee5d9ceed Fix #2831. (#2857) 2024-02-27 16:35:00 +08:00
zkitefly 20ed42ab8a 移除残留的 MCBBS 按钮 (#2849)
* remove

* fix
2024-02-25 01:20:42 +08:00
Glavo 815b90845a Fix #2850: fix i18n (#2852)
* Fix #2850: fix i18n

* update

* update

* update

* update
2024-02-25 00:56:29 +08:00
zkitefly e9846ea644 zkitefly-add-thanks-to-@ZekerZhayard (#2807)
* zkitefly-add-thanks-to-@ZekerZhayard

* fix

* add image
2024-02-25 00:25:15 +08:00
Glavo 02065094ca 修复使用 Java 19+ 启动游戏时控制台乱码的问题 (#2846) 2024-02-22 23:20:30 +08:00
Burning_TNTandGlavo 9f6124d617 Remove the link of MCBBS in README.md. (#2844)
* Remove the link of MCBBS in README.md.

* Remove MCBBS in sponsor.bmclapi

* Update I18N_zh.properties

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-21 21:54:50 +08:00
Glavo 76229a894c Fix Linux RISC-V 64 Support (#2840) 2024-02-20 02:41:24 +08:00
Glavo 03de1598c8 在 Jenkins CI 上使用腾讯云镜像下载 Gradle Wrapper (#2837)
* 使用腾讯云下载 Gradle Wrapper

* Create config-jenkins.sh

* update script
2024-02-20 01:42:51 +08:00
Glavo 3a13ff0e10 更新 downloadType 的默认值 (#2839) 2024-02-19 22:15:08 +08:00
Glavo 63345f52ce 更新 PUBLISH_URL (#2836)
* 更新 PUBLISH_URL

* update
2024-02-19 17:43:22 +08:00
Burning_TNT 0bff282d3d Fix #2826. (#2827) 2024-02-19 17:28:55 +08:00
Burning_TNTandGlavo a81462425d Fix #2762: 光影应当被下载至 ./shaderpacks 下 (#2770)
* Fix #2762: Shader packs should be downloaded in to the ./shaderpacks directory.

* Make the network requests parallely in order to make CurseCompletionTask faster.

* Support displaying the process.

* Fix: checkstyle

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-19 17:28:07 +08:00
Burning_TNT e9ae43b1f1 Fix 2774: Local NeoForge mods shouldn't be identified as Forge mods. (#2775) 2024-02-19 16:19:48 +08:00
zkitefly dc31f0d5c9 Updata slug (#2801)
* add curse.category.4736

* add

* 调整顺序
2024-02-19 16:12:33 +08:00
Burning_TNTandGlavo 76f11b6a6f Fix #2799 (#2800)
* Fix #2799

* Remove codes in ConfigUpgrader.

* delete collectDFU

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-19 16:07:49 +08:00
zkiteflyandGlavo d6ea2ef1f9 updata-incomplete_forge_installation (#2834)
* updata-incomplete_forge_installation

* Update CrashReportAnalyzer.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-19 15:54:21 +08:00
zkitefly 14db21211f updata Exitwaiter (#2813)
* 添加检测日志

参考自 https://github.com/Hex-Dragon/PCL2/blob/8c8b1066bff6476a14437a14bc16a281e25145bd/Plain%20Craft%20Launcher%202/Modules/Minecraft/ModWatcher.vb#L209-L228

* updata
2024-02-19 15:47:52 +08:00
zkiteflyandGlavo f354f65adb add-forge-found-duplicate-mods (#2828)
* add

* Update CrashReportAnalyzer.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-19 15:46:57 +08:00
zkitefly 83e8ea3735 把“自动选择并发数”选项默认开启 (#2805) 2024-02-19 15:41:28 +08:00
Glavo 0e9e9811eb 更新关于页面图标 (#2835) 2024-02-19 15:40:23 +08:00
huangyuhui 420d77de9f fix: remove thanks to mcbbs. 2024-02-19 15:21:10 +08:00
yuhuihuang 9361719a67 fix: remove mcbbs download source. 2024-02-19 14:48:44 +08:00
Burning_TNT b09a5868d4 Fix: Raw use of parameterized class 'VersionRange'. (#2815) 2024-02-16 17:53:33 +08:00
Burning_TNT 56c48e5595 Fix: English I18N for fatal.javafx.missing is incorrect. (#2811) 2024-02-15 19:56:50 +08:00
Burning_TNT 4d6cb547ea Fix #2809 本地模组列表搜索 (#2810) 2024-02-15 18:56:46 +08:00
Glavo 95ecd29018 Bump JUnit to 5.10.2 (#2803) 2024-02-14 13:42:23 +08:00
Glavo 5d26a106e9 使用 Instant 替代 Date (#2713)
* update

* update

* update

* update

* update

* Update

* update

* update

* Add more tests

* fix checkstyle
2024-02-13 15:29:54 +08:00
zkitefly 95707560ef fix icon (#2791) 2024-02-13 14:14:27 +08:00
zkitefly c54cb88db8 add Crash report analysis (#2790)
* fix i18n

* add

https://github.com/HMCL-dev/HMCL/discussions/1904#discussioncomment-4339947
2024-02-13 14:11:21 +08:00
Glavo 95afaa5112 更新游戏版本比较规则 (#2700)
* Create GameVersionNumber

* Update GameVersionNumber

* Rename DefaultVersionNumber

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update versions.txt

* update

* update

* update
2024-02-13 13:46:01 +08:00
Glavo fe608e32f1 Fix #2783: Alex 皮肤应为 Slim 模型 (#2789) 2024-02-13 10:26:41 +08:00
Glavo 82fc1c067d Fix #2153: 正确显示离线账户头像 (#2788)
* update

* update

* update

* update

* fix checkstyle

* update
2024-02-13 10:08:26 +08:00
Glavo 94890e2924 Update javafx.gradle.kts (#2787) 2024-02-13 00:39:39 +08:00
Glavo 9a76b1ff27 Bump Gradle to 8.6 (#2786) 2024-02-13 00:35:45 +08:00
zkiteflyandGlavo 8db8693132 updata crash report analysis (#2785)
* updata OUT_OF_MEMORY

* Update CrashReportAnalyzer.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-12 23:31:29 +08:00
Glavo 59cc5b9114 Close #2766: 不应提示 Quilt 与 Fabric API 不兼容 (#2768)
* Close #2766

* update
2024-02-08 19:27:28 +08:00
Glavo c1acd0b0b5 删除 RemoteResourceManager (#2761)
* 删除 RemoteResourceManager

* update
2024-02-08 19:19:01 +08:00
Glavo fb6538f042 更新 Linux RISC-V 64 支持 (#2760) 2024-02-08 09:36:37 +08:00
Glavo 20fe5430dd Close #2757: 更新 MultiFileItem (#2758) 2024-02-07 22:37:56 +08:00
zkiteflyandGlavo 7de07319d9 Update CacheRepository.java (#2756)
* Update CacheRepository.java

https://github.com/FCL-Team/FoldCraftLauncher/commit/e96dd55395ed8107e70d8446b9a3ac14f0a17416

* Update CacheRepository.java

* Update CacheRepository.java

* Update CacheRepository.java

* Update CacheRepository.java

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-07 22:21:48 +08:00
zkitefly 8e3c19651e Fix check-update.yml (#2750)
https://github.com/softprops/action-gh-release/issues/236#issuecomment-1147866048
2024-02-06 21:07:43 +08:00
Gamom 2740bdeb77 Fix check-update.yml (#2747)
尝试修复workflow
2024-02-06 18:33:27 +08:00
Glavo 59ab49e0c5 Fix #2740 (#2746) 2024-02-06 17:16:23 +08:00
Glavo bc4aa43834 Fix #2744: 尝试从所有下载源下载 authlib-injector (#2745) 2024-02-06 17:05:14 +08:00
zkitefly c7f3f2a229 Update Fabric warnings pattern in CrashReportAnalyzer (#2742) 2024-02-06 14:47:38 +08:00
ffeabbf1f1 修改日志分析器 (#2495)
* 小小的修改下细节

* 修改细节

* 修改链接

* add failed_to_load_a_library

* 修改一些细节

* 发现重复项(

* add NEED_JDK11 rule

minecraft-exported-crash-info-2023-08-30T18-53-01.zip!/minecraft.log: https://mclo.gs/PGKK8xD

* 修改 Java 下载地址

* add INSTALL_MIXINBOOTSTRAP

minecraft-exported-crash-info-2023-08-31T20-24-28.zip!/minecraft.log: https://mclo.gs/0gmBukK

* Update I18N_zh_CN.properties

* Update I18N.properties

* Update I18N_zh_CN.properties

* d

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* add INCOMPLETE_FORGE_INSTALLATION

* update lang

* Close https://github.com/HMCL-dev/HMCL/issues/2199. add mixin_apply_mod_failed

* fix

* Add crash report analyzer for RTSS and Sodium compatibility

* Fix compatibility issue with RivaTuner Statistics Server (RTSS) and Sodium

* Update CrashReportAnalyzer.java

---------

Co-authored-by: burningtnt <pangyl08@163.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-06 02:33:42 +08:00
Glavo c9ddb7aab4 改进 ModLoader 兼容性检测 (#2730)
* 改进 ModLoader 兼容性检测

* update
2024-02-04 19:40:23 +08:00
zkitefly 997552fb7e fix (#2737) 2024-02-04 19:32:23 +08:00
zkitefly 7d9595945d Add-thanks-to-@zkitefly (#2736)
* add

* add image

* fix image
2024-02-03 22:10:18 +08:00
Pigeon0v0 70157fa8de chores: Update Copyright (#2735) 2024-02-03 21:49:52 +08:00
zkitefly 22e1e6344f fix workflows (#2733) 2024-02-03 20:37:29 +08:00
ShulkerSakuraandGlavo 3952b6e3a8 Fresh icon (#2686)
* Added Optifine Icon

* Updated Optifine Icons

* Updated Optifine Icons

* Added new HMCL logo

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-02-03 13:55:51 +08:00
Glavo dd2a09399e 修复 AdditionalInstallersPage (#2731) 2024-02-03 04:33:17 +08:00
Burning_TNT 7f7068a556 Fix NeoForge modpack support (#2725)
* Fix: Cannot install modpacks with NeoForge for Minecraft 1.20.1.

* Add supports for MultiMC Modpack with NeoForge.
2024-02-02 01:22:56 +08:00
Glavo a79a2014b0 在游戏安装界面隐藏不可用的模组管理器 (#2726)
* 在 Minecraft 1.13+ 的安装界面隐藏 liteloader 选项

* update

* fix checkstyle

* fix checkstyle
2024-02-02 01:11:06 +08:00
Burning_TNT 4c41f25314 Fix: Env arg INST_NEOFORGE is missing in custom commands. (#2723) 2024-02-01 22:59:17 +08:00
zkitefly c26eb2bc69 Updata DownloadListButton I18n (#2715) 2024-01-31 16:25:28 +08:00
Glavo e93e5effa6 优化 BMCLAPIDownloadProvider::injectURL (#2712) 2024-01-31 03:22:56 +08:00
Burning_TNT 7acc39154b Fix: Cannot install modpacks with NeoForge for Minecraft 1.20.1. (#2708)
* Fix: Cannot install modpacks with NeoForge for Minecraft 1.20.1.

* Add supports for MultiMC Modpack with NeoForge.
2024-01-31 01:57:38 +08:00
Glavo 19cae84a01 尝试从候选源获取 AuthlibInjectorVersionInfo (#2711)
* 尝试从候选源获取 AuthlibInjectorVersionInfo

* update

* update

* update
2024-01-31 01:56:06 +08:00
Glavo 0bcda9eb46 删除 DecoratorController 中的背景图缓存 (#2710) 2024-01-31 01:09:11 +08:00
Burning_TNT 7556ef21c4 Fix: I18N for NeoForgeOldInstallTask was missing. (#2693) 2024-01-30 22:28:24 +08:00
Burning_TNT 9290c26c0b Close #2685: Support downloading NeoForge from official maven. (#2694) 2024-01-30 21:53:03 +08:00
Burning_TNT 5388020b35 Render required dependencies first. (#2695) 2024-01-30 21:42:51 +08:00
Burning_TNTandGlavo 017d688e32 Fix: Gradle scripts aren't compatible with Java 8. (#2706)
* Fix: Gradle scripts aren't compatible with Java 8.

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-01-30 21:39:16 +08:00
Glavo 4102029d6d update (#2702) 2024-01-30 12:31:14 +08:00
Glavo d286f93a7e 更新链接 (#2696) 2024-01-27 19:06:28 +08:00
Glavo 207b4d0033 Fix #2687 (#2688)
* Fix #2687

* fix checkstyle
2024-01-26 21:20:29 +08:00
Glavo 351a59149f 弃用 charsetCandidates (#2683)
* update

* create test
2024-01-26 04:37:54 +08:00
Glavo 7880c2f3d9 更新 PLATFORM.md (#2682) 2024-01-26 02:55:32 +08:00
Glavo 644866a24b 支持查看 NBT 文件 (#2402)
* init

* fix checkstyle

* update RootPage

* fix build

* update

* update
2024-01-26 01:24:31 +08:00
ShulkerSakura 43fee4878f Fresh icon (#2677)
* Added Optifine Icon

* Updated Optifine Icons

* Updated Optifine Icons
2024-01-26 00:32:07 +08:00
Glavo 1d8ead2cbb Close #2460: 为 Linux/FreeBSD 提供 moveToTrash 支持 (#2678)
* Close #2460: 为 Linux/FreeBSD 提供 moveToTrash 支持

* fix checkstyle

* Ignore milliseconds
2024-01-26 00:22:46 +08:00
Glavo 4cbf5ce979 Fix #2680: Revert TwoLineListItem (#2681) 2024-01-25 21:11:21 +08:00
Glavo 1eb53b247c 清理 org.jackhuang.hmcl.util (#2673)
* 合并 Holder 与 ReferenceHolder

* 清理代码

* 清理代码

* update

* update

* update
2024-01-24 03:07:56 +08:00
Burning_TNT f53aed9a08 Fix: NeoForge Support (#2632)
* Fix

* Fix: NeoForge may successfully made HMCL think it's Forge.

* Fix #2649

* Fix #2654 partly because MultiMC hasn't support NeoForge yet.

* Get ready for HMCL to support export and read MultiMC Modpacks with NeoForge.
2024-01-24 00:19:49 +08:00
5db3c123a0 I18n (#2468)
* 更新 I18N_zh_CN.properties

* 因为 MMC 不再支持 Curse 整合包,所以 MCBBS 整合包格式将无法被识别

* https://github.com/huanghongxun/HMCL/pull/1410

* 更改一些提示

* 修改语言文件

* 修改提示和一些细节

* (

抱歉(

* Update I18N_zh_CN.properties

* 将大部分 zh_CN 中的 Mod 字样换成 模组

* Revert "Update I18N_zh_CN.properties"

This reverts commit 79d99247b6.

* Update I18N_zh_CN.properties

* 改进I18N
~~眼睛都要瞎掉~~

* 把 Optifine 改成 OptiFine ,然后还加了点空格

* 一定又是IDEA的智慧的编码问题

* 增加锚点
~~一定是IDEA偷偷加这几行字的~~

* 加了亿点点锚点

* `Optifine` -> `OptiFine`

* #2524 不小心带来的翻译错误

* `Tencent` -> `Tencent Cloud`

* Revert

This reverts commit 387935d40c.

Revert "`Tencent` -> `Tencent Cloud`"

This reverts commit a7e11b4e8d.

* 删掉了一些东西

* Update .gitignore

---------

Co-authored-by: hejiehao <hejiehao12@126.com>
Co-authored-by: 何杰豪 <98140773+hejiehao@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2024-01-24 00:11:45 +08:00
Glavo e1e9215f2c 清理 CacheRepository (#2672) 2024-01-23 21:46:01 +08:00
Glavo 23f58e63aa Delete VersionNumber.VERSION_COMPARATOR (#2671) 2024-01-23 14:34:29 +08:00
Glavo 2cad6f33f5 Fix #2552: 对于 Minecraft 1.19+ 不应特殊处理中文路径 (#2670) 2024-01-23 11:22:54 +08:00
Glavo 308086b876 简化 VersionIconType 相关代码 (#2669) 2024-01-23 11:03:25 +08:00
Glavo 9bdf08af1e Fix #2665: newBuiltinImage 不应缓存经缩放的图像 (#2668) 2024-01-22 21:09:26 +08:00
ddcead7c85 Fix: version::compareTo compares the name, instead of the game version. (#2664)
* Fix: version.compareTo compares the name, but not the game version.

* Fix more.

* update

* update

Co-authored-by: zkitefly <z18344203426@qq.com>

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: zkitefly <z18344203426@qq.com>
2024-01-21 00:29:06 +08:00
Glavo 89b5b64943 放弃 HMCLauncher 与 Windows XP 的兼容性 (#2644)
* 放弃 HMCLauncher 与 Windows XP 的兼容性

* update

* Delete unused code

* Disable automatic updates on Windows XP/Vista
2024-01-21 00:06:25 +08:00
Burning_TNTandGlavo 931808d808 Allow updates when self verification is disabled (#2639)
* Allow updates when self verification is disabled

* clean up IntegrityChecker

* Fix: UpdateHandler would transfer the current command properties to the new HMCL.

* Fix.

* Fix.

* update

* update

* update

* update

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-01-20 16:15:28 +08:00
Glavo b9a6401ef3 Move DATE_TIME_FORMATTER to SupportedLocale (#2662)
* Move dateTimeFormatter to SupportedLocale

* update
2024-01-20 14:55:28 +08:00
Glavo 59a9b3a926 清理 ManagedProcess::getPid() (#2660)
* Delete getPID0

* Delete blank line
2024-01-20 10:57:46 +08:00
Glavo 96270b1706 Bump dependencies (#2659)
* Bump dependencies

* Fix build
2024-01-19 23:23:44 +08:00
Glavo af784dd0d0 Fix HMCLauncher.sh (#2658) 2024-01-19 12:51:59 +08:00
Glavo 1a1f2133f7 支持 FreeBSD x86-64 (#2656)
close #2652
2024-01-18 20:13:47 +08:00
Burning_TNT 3f1be961a6 Set the network timeout to 8000ms. (#2650) 2024-01-17 16:25:28 +08:00
Burning_TNT 8f2afdfcff Support #2643 (#2647) 2024-01-15 15:52:09 +08:00
Burning_TNT 532ba6ec2f Fix #1549 (#2648)
* Fix #1549

* Revert changes on ModTranslations.

* Revert changes on StringUtils.
2024-01-15 15:51:22 +08:00
Kevin Z 8678c0dcf2 auth: Always check token.exp when launch game (#2642)
* auth: Always check token.exp when launch game

fix #2048

* Remove wrong notAfter check
2024-01-13 11:53:47 +08:00
Burning_TNT 2116cfd789 Render title in white at anytime (#2389) 2024-01-13 11:27:10 +08:00
zkitefly 45d9033252 Update link (#2633)
* updata link

* Update dynamic-remote-resources json

* Revert "Update dynamic-remote-resources json"

This reverts commit 3d92ff2f56.
2024-01-13 10:36:44 +08:00
Glavo 96df35f728 将 HMCLauncher 源码转换为 UTF-8 编码 (#2641) 2024-01-13 03:26:58 +08:00
zkitefly 16b0335fcf Fixed "isOfficial" judgment (#2636) 2024-01-10 17:50:59 +08:00
Loading InformationandYuhui Huang b69dadc80c Update I18N_ru.properties (#1950)
Co-authored-by: Yuhui Huang <jackhuang1998@gmail.com>
2024-01-09 15:54:43 +08:00
Burning_TNT 7619e48145 Support #2588 记忆 HMCL 窗口位置 (#2593)
* Build configuration DFU system.

* Remove redundant object allocation.

* Fix

* Code cleanup

* Support #2588

* Remove unnecessary GSON usage.

* Fix compiletime unchecked warning.
2024-01-09 15:51:26 +08:00
Burning_TNT fbc667798d Fix #2470 Enhance the behavior of authlib-injector.json (#2471)
* Fix #2470

* Revert "Fix #2470"

This reverts commit b0f391021c.

* Refactor Behavior

* Simpify AuthlibInjectorServers.

* Fix merge

* Fix checkstyle.

* Recover the validation of AuthlibInjectorServers.
2024-01-08 20:41:48 +08:00
Burning_TNT 4e22585faf Build configuration DFU system. (#2589)
* Build configuration DFU system.

* Remove redundant object allocation.

* Fix

* Code cleanup

* Remove unnecessary GSON usage.

* Fix compiletime unchecked warning.
2024-01-08 20:39:55 +08:00
Burning_TNTandGlavo 5d3660ffb8 Enable HMCL to export jstack dump file 让 HMCL 能够导出游戏运行栈文件 (#2582)
* Enable HMCL to create game thread dump while game is running

* Fix checkstyle

* Hide accessToken

* Code cleanup

* Code cleanup

* Enhance I18N and declare the charset (UTF-8) of output file

* Inline variables

* Update the modifier of org.jackhuang.hmcl.game.GameDumpCreator#writeDumpHeadTo from public to private

* Refactor

* Add license for GameDumpCreator, remove support for Java 8

* Remove unnecessary Arrays.copyOf

* Fix checkstyle

* Use system charset to read the inputstream from JVM

* opt GameDumpCreator

* retry on failed attach to vm

* update GameDumpCreator

* Opt GameDumpCreator

* Fix

* Include BCIG

* Use BCIG to get PID.

* Fix.

* Fix again.

* Code cleanup. Fix bugs.

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2024-01-08 20:35:46 +08:00
Burning_TNTandBurning_TNT 4149876e04 Support Neoforge (#2548)
* Support #2376

* Add necessary @Nullable annotations

* Display different types of dependencies in different sections.

* Fix checkstyle

* Add I18N for different types of dependencies.

* Enhance UI

* Code cleanup

* Enhance UI

* Manually sort the result from curseforge when searching mods by name.

* Render the search results from remote mod repositories in several pages.

* Fix merge

* Fix

* Add a button which navigates to the modpack download page in the modpack installl page

* Fix I18N

* Render the mod loaders supported by the version in mod info page.

* Fix #2104

* Enhance TwoLineListItem

* Render the mod loader supported by this mod file on the ModListPage

* Fix chinese searching and curseforge searching

* Update I18N

* Fix

* Fix

* Select the specific game version when clicking the 'download' button on ModListPage

* Support HMCL to update mod_data and mod_pack data from https://github.com/huanghongxun/HMCL/raw/javafx/data-json/dynamic-remote-resources.json

* Enhance :HMCL:build.gradle.kts

* Revert parse_mcmod_data.py

* Abstract 'new Image' to FXUtils.newBuiltinImage and FXUtils.newRemoteImage

FXUtils.newBuiltinImage is used to load image which is supposed to be correct definitely and is a file within the jar. Or, it will throw ResourceNotFoundError.

FXUtils.newRemoteImage is used to load image from the internet. It will cache the data of images for the further usage. The cached data will be deleted when HMCL is closed or hidden.

* Add javadoc for FXUtils.newBuiltinImage and FXUtils.newRemoteImage.

* Fix checkstyle

* Fix

* Fix

* Fix

* Add license for RemoteResourceManager

* Remove TODO

* Enhance Chinese searching

* Support to decode metadata for local quilt mod.

* Enhance ModManager

* Fix checkstyle

* Refactor

* Fix

* Fix

* Refactor DownloadPage

* Fix

* Revert "Refactor DownloadPage"

This reverts commit 953558da77.

* Refactor DownloadPage

* Refactor

* Fix

* Fix checkstyle

* Set org.jackhuang.hmcl.ui.construct.TwoLineListItem.TagChangeListener as a private static inner class.

* Fix

* Fix

* Fix

* Enhance SimpleMultimap

* Revert TwoLineListItem

* Fix

* Code cleanup

* Code cleanup

* Fix

* Code cleanup

* Add license for IModMetadataReader

* Add prefix 'Minecraft' at the supported minecrft version list in DownloadPage

* Fix #2498

* Update README_cn.md

* Opti ModMananger

* Log a warning message when 'hmcl.update_source.override' is used.

* Fix chinese searching

* Enhance chinese searching.

* Enhance memory usage

* Add basic support for NeoForge.

* Enable HMCl to show the version list of NeoForged.

* Fix checkstyle

* Enable HMCL to download and install NeoForged from BMCL. Fix some bugs.

* Close the mod version dialog window after clicking the downloading / save as button if the dependency list is empty.

* Fix

* Enable HMCL to install NeoForge from modpacks.

* Fix the dirty implememtation

* Fix UI

* Cache builtin images.

* Enhance FXUtils (Make tooltip installer faster).

* Fix

* Fix

* Fix #2560

* Fix typo

* Fix remote image cache.

* Fix javadoc

* Fix checkstyle

* Optimize FXUtils::shutdown

* Support NeoForged 1.20.2

* Fix merge

* I have no idea on why the sha1 was matched.

* Revert "Enhance FXUtils (Make tooltip installer faster)."

This reverts commit 0a49eb2c12.

* Support multi download source in order balance the traffic of hmcl.huangyuhui.net and the download speed in China Mainland.

* Modify dynamic remote resource urls.

* Optimize codes with StringUtils.DynamicCommonSubsequence.

* Prevent unofficial HMCL to access HMCL Resource Update URL.

* Zip the dynamic-remote-resources json by Gradle automatically.

* Remove unnecessary getters.

* Fix

* Fix LibraryAnalyzer: Cannot analyze the version number of NeoForge correctly.

* Support to choose the NeoForge icon if a game is installed with NeoForge.

* Fix checkstyle

* Fix: The version number on DownloadPage of NeoForge is incorrect, which has leading string 'neoforge-'.

* Fix.

* Rename references. Fix: Invalid spaces in VersionIconType.

---------

Co-authored-by: Burning_TNT <pangyl08@163.com“>
2024-01-08 20:35:03 +08:00
zkitefly e6e4e36537 在每个检测句后加上引导句 (#2324)
* 在每个检测句后加上引导句

* 更新 GameCrashWindow.java

* 小小调整

* 更新 I18N_zh_CN.properties

* 按要求修改

* Update GameCrashWindow.java
2024-01-07 22:44:28 +08:00
Burning_TNTandburningtnt 5b7149b967 Suppport feature #2554. Fix the width of task progess bar. (#2573)
* Suppport feature #2554. Fix the width of task progess bar.

* Simply codes.

* Fix checkstyle.

---------

Co-authored-by: burningtnt <pangyl08@163.com“>
2024-01-07 22:41:08 +08:00
Burning_TNT f558be7193 Fix (#2622) 2024-01-07 22:40:21 +08:00
Burning_TNT a9159e1a72 Fix: The return value of the dependent mods may be duplicated from CurseForge API. (#2630) 2024-01-07 22:40:05 +08:00
Burning_TNT b0e3ae7684 Remove useless class ClassicLibrary (#2621) 2024-01-02 17:03:55 +08:00
Burning_TNTandBurning_TNT 242df8a81a Enhance mod download (#2411)
* Support #2376

* Add necessary @Nullable annotations

* Display different types of dependencies in different sections.

* Fix checkstyle

* Add I18N for different types of dependencies.

* Enhance UI

* Code cleanup

* Enhance UI

* Manually sort the result from curseforge when searching mods by name.

* Render the search results from remote mod repositories in several pages.

* Fix merge

* Fix

* Add a button which navigates to the modpack download page in the modpack installl page

* Fix I18N

* Render the mod loaders supported by the version in mod info page.

* Fix #2104

* Enhance TwoLineListItem

* Render the mod loader supported by this mod file on the ModListPage

* Fix chinese searching and curseforge searching

* Update I18N

* Fix

* Fix

* Select the specific game version when clicking the 'download' button on ModListPage

* Support HMCL to update mod_data and mod_pack data from https://github.com/huanghongxun/HMCL/raw/javafx/data-json/dynamic-remote-resources.json

* Enhance :HMCL:build.gradle.kts

* Revert parse_mcmod_data.py

* Abstract 'new Image' to FXUtils.newBuiltinImage and FXUtils.newRemoteImage

FXUtils.newBuiltinImage is used to load image which is supposed to be correct definitely and is a file within the jar. Or, it will throw ResourceNotFoundError.

FXUtils.newRemoteImage is used to load image from the internet. It will cache the data of images for the further usage. The cached data will be deleted when HMCL is closed or hidden.

* Add javadoc for FXUtils.newBuiltinImage and FXUtils.newRemoteImage.

* Fix checkstyle

* Fix

* Fix

* Fix

* Add license for RemoteResourceManager

* Remove TODO

* Enhance Chinese searching

* Support to decode metadata for local quilt mod.

* Enhance ModManager

* Fix checkstyle

* Refactor

* Fix

* Fix

* Refactor DownloadPage

* Fix

* Revert "Refactor DownloadPage"

This reverts commit 953558da77.

* Refactor DownloadPage

* Refactor

* Fix

* Fix checkstyle

* Set org.jackhuang.hmcl.ui.construct.TwoLineListItem.TagChangeListener as a private static inner class.

* Fix

* Fix

* Fix

* Enhance SimpleMultimap

* Revert TwoLineListItem

* Fix

* Code cleanup

* Code cleanup

* Fix

* Code cleanup

* Add license for IModMetadataReader

* Add prefix 'Minecraft' at the supported minecrft version list in DownloadPage

* Fix #2498

* Update README_cn.md

* Opti ModMananger

* Log a warning message when 'hmcl.update_source.override' is used.

* Fix chinese searching

* Enhance chinese searching.

* Enhance memory usage

* Close the mod version dialog window after clicking the downloading / save as button if the dependency list is empty.

* Cache builtin images.

* Enhance FXUtils (Make tooltip installer faster).

* Fix

* Fix

* Fix #2560

* Fix typo

* Fix remote image cache.

* Fix javadoc

* Fix checkstyle

* Optimize FXUtils::shutdown

* Fix merge

* I have no idea on why the sha1 was matched.

* Revert "Enhance FXUtils (Make tooltip installer faster)."

This reverts commit 0a49eb2c12.

* Support multi download source in order balance the traffic of hmcl.huangyuhui.net and the download speed in China Mainland.

* Modify dynamic remote resource urls.

* Optimize codes with StringUtils.DynamicCommonSubsequence.

* Prevent unofficial HMCL to access HMCL Resource Update URL.

* Zip the dynamic-remote-resources json by Gradle automatically.

* Remove unnecessary getters.

---------

Co-authored-by: Burning_TNT <pangyl08@163.com“>
2023-12-31 23:15:54 +08:00
Burning_TNT 333fd0ef80 Fix #2375 Fix network NPE (#2579)
* Fix #2375

* Fix

* Fix

* Simply codes.
2023-12-31 22:51:06 +08:00
5defff2bb0 Fix string tokenizer (#2538)
* Fix

* Parse quote and double quote at the same time. Add TokenizerTest.

* Simplify TokenizerTest

* Fix handling multiple space

* Fix handling empty part

* Supports escape sequences

* Remove an unnecessary lambda.

---------

Co-authored-by: Burning_TNT <pangyl08@163.com“>
Co-authored-by: Glavo <zjx001202@gmail.com>
2023-12-31 22:45:26 +08:00
zkitefly bdcbe6c948 修复 #2524 带来的一些小问题 (#2535)
* #2524 不小心带来的翻译错误

* `Tencent` -> `Tencent Cloud`
2023-12-31 22:37:28 +08:00
Burning_TNT cc192c840c [Enhance] Enhance issue templates 改进 Issue 模板 (#2381)
* Enhance issue templates

* Update feature.yml

* Update

* Update issue templates

* Add the missing chinese translation

* Fix typo
2023-12-31 21:57:53 +08:00
Burning_TNT e5ece58d17 Minimize OpenNBT. (#2601) 2023-12-31 21:55:24 +08:00
Burning_TNT 2391b26bd6 Fix #1750 (#2580) 2023-12-31 21:54:19 +08:00
zkitefly c7fad05bae Update exitCode (#2500) 2023-12-31 21:49:56 +08:00
Burning_TNT 1d9d31f906 Exclude META-INF/maven/** (#2613) 2023-12-27 00:03:03 +08:00
Glavo d584c101d4 Filter package-info.class (#2606) 2023-12-20 20:10:06 +08:00
Burning_TNT ab41feff42 Fix DefaultLauncher (#2584) 2023-12-15 20:38:23 +08:00
Burning_TNT cb7975515a Update Minecraft version filter to 1.20.4 when downloding mods. (#2595) 2023-12-14 18:58:55 +08:00
zkitefly 91f684ae2e 不默认设置 forceUnicodeFont (#2501) 2023-12-09 01:02:08 +08:00
Glavo 55c55e9bda Bump Gradle to 8.5 (#2586) 2023-12-01 04:04:41 +08:00
Burning_TNTandBurning_TNT 453ba6eb75 Fix build.gradle and README (#2565)
* Update build.gradle.kts

* Fix table format and typo in README(_cn).md

* Replace Github to GitHub

---------

Co-authored-by: Burning_TNT <pangyl08@163.com“>
2023-11-12 20:30:36 +08:00
zkitefly 5aeaba4636 add PLATFORM_cn (#2536)
* add PLATFORM_cn

* Update PLATFORM_cn.md

* 润色

* Update PLATFORM_cn.md

* Update PLATFORM.md

* 格式化
2023-11-09 21:24:02 +08:00
Burning_TNT df9499b723 Catch Internal Error (#2520)
* Catch Internal Error

* Fix

* Fix I18N
2023-11-09 21:23:10 +08:00
Burning_TNT f84be1e109 Fix #2539 (#2541) 2023-11-09 21:20:39 +08:00
Burning_TNT 31872f4f77 Fix #2547 (#2549) 2023-11-09 21:19:26 +08:00
Burning_TNTandGlavo 54188bf8f2 Enhance booting (#2531)
* Enhance booting

* Delete @Booting

* Rollback OperatingSystem

* Delete Booting

* cleanup

* Update FractureiserDetector

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2023-09-30 14:27:56 +08:00
zkitefly 9fb2bc4c7e 将所有的阿里镜像源地址换成腾讯云镜像源 (#2524) 2023-09-29 19:45:45 +08:00
Glavo 5d8a56cf66 更新对 ARM 平台的支持 (#2522)
* 修复对 Windows Arm64 的支持

* 修复对 Linux Arm64 的支持
2023-09-22 16:48:24 +08:00
ZekerZhayard 833ac2e32e Fix missing null checking. (#2512) 2023-09-12 23:15:44 +08:00
Glavo 50173a3987 添加龙芯新世界 LWJGL2 支持 (#2506)
* 添加龙芯新世界 LWJGL2 支持

* Update Documents
2023-09-06 15:28:55 +08:00
Glavo 9d5202c19f 优化龙芯支持 (#2504)
* 添加龙芯平台提示

* 添加旧世界 JNA

* Update Launcher

* Update HMCLauncher.sh

* Update I18N
2023-09-05 14:47:46 +08:00
何杰豪 e1bbd8b326 System GLFW & System OpenAL are available on Windows too (#2496) 2023-08-29 12:36:55 +08:00
Glavo 06f81b9b09 启动时验证 JavaFX 完整性 (#2489)
* 启动时验证 JavaFX 完整性

* fix

* fix
2023-08-26 12:59:04 +08:00
Glavo 150baa0e30 重构 SVG (#2487)
* Refactoring SVG

* enum

* update IconedMenuItem

* fix checkstyle

* update Theme

* fix checkstyle

* update
2023-08-26 11:25:05 +08:00
Glavo 413d962db0 优化日志性能 (#2488)
* 优化日志性能

* update

* update

* update
2023-08-26 11:13:15 +08:00
Glavo 74118049ce 添加龙芯新世界支持 (#2480)
* 添加龙芯新世界支持

* Update message
2023-08-23 21:29:14 +08:00
zkitefly 05086fa76d 修复日志导出 (#2400)
* 优化日志导出

以防万一的操作,防止一些必要的 log 没被打包

* Update LogExporter.java

* 更新 LogExporter.java

* add

* Revert "add"

This reverts commit 287f9300d7.

* add

* 奇怪我什么时候改过这里了?
2023-08-19 08:00:42 +08:00
Glavo c1ffd26c80 Optimization for Hex (#2476)
* Optimization for Hex

* fix checkstyle
2023-08-19 07:36:52 +08:00
yangyangdaji a8b0f81b68 Update Hub Mirror Action to 1.3 (#2458)
Update `Hub Mirror Action` to 1.3

[Release note](https://github.com/Yikun/hub-mirror-action/releases/tag/v1.3)

> 升级PyYaml以修复PyYaml安装问题 https://github.com/Yikun/hub-mirror-action/pull/182
> 开启pull_request_target帮助开发者触发CI验证 https://github.com/Yikun/hub-mirror-action/pull/139
> 新增clone_style的说明文档 https://github.com/Yikun/hub-mirror-action/pull/146 感谢@AdoShan
> 新增与repo-list-generator共同使用的文档 https://github.com/Yikun/hub-mirror-action/pull/150 感谢@yi-Xu-0100

Fix sync to gitee action

`ModuleNotFoundError: No module named 'yaml'`

[show detail](https://github.com/huanghongxun/HMCL/actions/runs/5783970016/job/15673761600)

Signed-off-by: yangyangdaji <1504305527@qq.com>
2023-08-19 06:16:19 +08:00
Glavo d83b508ce4 Bump Gradle to 8.3 (#2475) 2023-08-19 06:13:39 +08:00
Burning_TNT fe622579cf Support #2416 Enable HMCL to analyze crash reasons from latest.log (#2433)
* Support analyze crash reasons from latest.log

* Fix
2023-08-11 15:14:20 +08:00
zkitefly edcdeaf018 修改语言文件 (#2412)
* 更新 I18N_zh_CN.properties

* 因为 MMC 不再支持 Curse 整合包,所以 MCBBS 整合包格式将无法被识别

* https://github.com/huanghongxun/HMCL/pull/1410

* 更改一些提示
2023-08-07 18:26:19 +08:00
zkitefly 9d4eb9662e Update CrashReportAnalyzer.java (#2439)
* Update CrashReportAnalyzer.java

Fix

* add

* add

https://mclo.gs/Fp8GLWo
2023-08-07 18:21:28 +08:00
Burning_TNT 42e88fe6f5 Fix #2444 (#2445) 2023-08-02 23:17:08 +08:00
Burning_TNT 61165dd71d [Fix 2378] Invalid Modrinth Mod ID 错误的 Modrinth Mod ID (#2385)
* Upgrade timeout to 30s

* Fix #2378

* Revert "Upgrade timeout to 30s"

This reverts commit 366a9dd34b.
2023-07-20 14:30:22 +08:00
zkitefly 83120afb63 add (#2401) 2023-07-18 20:58:38 +08:00
Burning_TNTandGlavo 4fca632efb Fix #2397 Fractureiser 检测没有判断是否为文件 (#2398)
* Fix #2397

* Update FractureiserDetector.java

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
2023-07-16 03:07:33 +08:00
zkitefly 03b20a70bb Update Metadata.java (#2390) 2023-07-14 12:50:55 +08:00
Burning_TNT 20ee9c8ce6 [Fix ----] Game option UseNativeGLFW could work on Windows. Now, it is ignored on Windows. (#2386)
* Fix: Game option UseNativeGLFW could work on Windows. Now, it is ignored on Windows.

* Update translation value
2023-07-12 15:02:52 +08:00
zkitefly 9c0e1b1dc8 check-update.yml 支持 stable 发版 (#2332)
* Update check-update.yml

* test

* test

* test

* Update check-update.yml

* test

* Update check-update.yml

* test

* test

* test

* test

* test

* test

无语了(

* test

* test

* test

* fix

* OK!

* Update check-update.yml

* 按照要求修改

* 按照要求修改

* test

* finish
2023-07-10 20:39:17 +08:00
GlavoandYuhui Huang f29e350b1a #2353: 优化游戏进程被 SIGKILL 信号终止时的提示 (#2358)
Co-authored-by: Yuhui Huang <jackhuang1998@gmail.com>
2023-07-10 15:06:28 +08:00
Burning_TNT bbe0f5d750 Support selecting the best icon when a new version is installed. (#2370) 2023-07-10 15:03:04 +08:00
Burning_TNT 371295d9fe Fix #2372 (#2373) 2023-07-10 15:01:09 +08:00
Burning_TNT cc8a4ddf19 Fix #2371 (#2374) 2023-07-10 15:00:24 +08:00
zkitefly f3ff037f1c 添加日志分析规则 (#2361)
* add

* fix
2023-07-08 23:01:55 +08:00
Burning_TNT 3b36bbc88e Support viewing mods in ModListPage (#2366) 2023-07-08 23:01:27 +08:00
Burning_TNT 80b53119ed Support copying the UUID of the account (#2364) 2023-07-08 23:00:19 +08:00
zkitefly 4d88c2b1bb 在一些地方加些提示 (#2329)
* tips

* add

* 更新 DownloadListPage.java

* fix

* fix
2023-07-07 15:16:50 +08:00
Burning_TNT 29582f6c50 Warn if username is invalid when creating a offline account. 在离线账户创建时对不合法的账户名做出警告 (#2309)
* Fix #2258

* Fix checkstyle

* Merge official branch into current branch and fix checkstyle
2023-07-07 15:15:50 +08:00
zkitefly 623c54cf58 调整日志分析规则 (#2217)
* Update bug-report.yml

* 更新 CrashReportAnalyzer.java

* 添加 帮助 按钮

* 调整日志分析规则

* add

https://mclo.gs/EheKneq#L324

* add

https://mclo.gs/VyH2mV1#L135

* add

https://mclo.gs/rVgURRg

* add

https://mclo.gs/qCDf08i
https://mclo.gs/pyYhqhx

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModCheckUpdatesTask.java

* Update ModListPage.java

* add

* add and fix

* Edit MCEdit Link

* add

https://mclo.gs/Iq1Y5J0#L30
https://mclo.gs/D5MVHDa#L30
https://mclo.gs/nWMnElV#L34
https://mclo.gs/eoO8xjH#L14
https://mclo.gs/Jw1dRMZ#L27
https://mclo.gs/BVagUsw#L14
https://mclo.gs/NCfhHYC#L14
https://mclo.gs/PYRFCj3#L21
https://mclo.gs/4dCbKcJ#L19

* add

https://mclo.gs/h63mMy7#L47

* add

https://mclo.gs/iTTW7Ph#L16

* fix?

* fix?

* fix

* fix

无语,眼瞎(

* add

https://github.com/Hex-Dragon/PCL2/issues/1277

* fix

* fix

* 更新 I18N_zh_CN.properties

* add JADE_FOREST_OPTIFINE

* fix
2023-07-07 15:14:37 +08:00
zkitefly 6eee8b89e1 导出更多日志 (#2262)
* 更新 LogExporter.java

* add

* 更新 LogExporter.java

* 按照要求修改

~嘻嘻,这个是GPT生成的(~
2023-07-07 15:14:02 +08:00
Burning_TNT c74ef3eca8 Fix 2338 自定义命令问题 (#2339)
* Fix 2338

* Fix: Now HMCL could read Post exit command from config file.

* Invoke processListener.onExit before invoking exit command. Remove 'throw new RuntimeException'.
2023-07-07 15:13:12 +08:00
zkitefly 372b6376fc 在一些地方插入些log (#2325) 2023-07-07 15:10:29 +08:00
Burning_TNT 0c3bfc933c Support #1775 Enable HMCL to download resource packs from Modrinth. (#2355)
* Support downloading resource packs from Modrinth. Fix #1775

* Fix: Forget to register ResourcepackDownloadListPage to a button.
2023-07-07 15:08:59 +08:00
Burning_TNT 2414db1c61 Support #2010 一键导出待更新模组列表 (#2354)
* Fix #2010

* Improve behaviour

* Add License
2023-07-07 14:58:14 +08:00
Burning_TNT 008fe17035 Support2128 log window copy able (#2359)
* Support copy action in LogWindow

* Use a pseudo class state to manage render whether the line is selected
2023-07-07 14:57:16 +08:00
何杰豪andYuhui Huang 2be95fdb51 add logs folder open and change the shaderpacks icon (#2347)
* shaderpacks folder and resourcepacks icon

* i18n

---------

Co-authored-by: Yuhui Huang <jackhuang1998@gmail.com>
2023-07-06 23:46:35 +08:00
Yuhui Huang e5842d5a49 Revert "Support copy action in LogWindow (#2351)" (#2357)
This reverts commit 9a59ca95a1.
2023-07-06 23:34:10 +08:00
Burning_TNT 9a59ca95a1 Support copy action in LogWindow (#2351) 2023-07-06 23:31:55 +08:00
Burning_TNT 421f6f8b6d Fix #2303 (#2346) 2023-07-06 23:29:45 +08:00
Burning_TNT a5ee6b05e8 Fix #2305 修复游戏语言自动切换功能与不同版本的兼容性问题 (#2345)
* Fix #2305

* fix
2023-07-06 23:28:41 +08:00
zkitefly c9465d89f5 Fix #2335 (#2336)
* Fix #2335

https://github.com/zkitefly/PCL2/blob/log/Plain%20Craft%20Launcher%202/Modules/Minecraft/ModMinecraft.vb#L2043-L2059

* Update Constants.java
2023-07-06 23:23:10 +08:00
zkitefly 097a8eaf26 Fix #1939 (#2352) 2023-07-06 23:19:54 +08:00
何杰豪 648b671210 i18n (#2344)
* 改错

* 改错

* 改错
2023-07-06 23:12:39 +08:00
zkitefly 75aeed9747 add shaderpacks open (#2331)
* add shaderpacks

* fix

* fix
2023-07-03 00:02:25 +08:00
zkitefly c87f7f39c7 修复搜索栏处的版本顺序错误 (#2318)
我当时写反了(
2023-06-29 01:35:50 +08:00
Burning_TNT 32b10038d9 Fix checkstyle (#2315) 2023-06-26 21:58:27 +08:00
TomatoPuddin 204cbcbed0 添加 Curse RemoteMod.Version 对多个 ModLoaderType 的支持 (#2311) 2023-06-26 17:43:11 +08:00
TomatoPuddin da5a04b3ba 改进 ModItem 中渲染 ModLoaderType 的代码 (#2310)
* 移除 ModItem 中多余的设置 VersionType 图标的代码

* 添加 ModItem 中缺失的 Quilt 标签
2023-06-26 17:43:02 +08:00
zkitefly 126a6dbbed Update HMCLauncher (#2264)
* 更新 lang.h

* 更新 main.cpp

* 更新 HMCLauncher.sh
2023-06-24 09:40:13 +08:00
zkitefly ab43d4a6cf 将配置文件的 logLines 键默认值改为 1000 (#2261) 2023-06-23 19:49:36 +08:00
zkiteflyandYuhui Huang 5f6e11f709 添加 帮助 按钮 (#2214)
* 添加 帮助 按钮

* 调整

* Revert "调整"

This reverts commit 69c7552933.

* 更新 DecoratorSkin.java

* Update DecoratorSkin.java

* Update DecoratorSkin.java

* Revert "Update DecoratorSkin.java"

This reverts commit cdb6267e23.

* Revert "Merge branch 'helpButton' of https://github.com/zkitefly/HMCL into helpButton"

This reverts commit 03673540ee, reversing
changes made to 5342625678.

* add

* Revert "add"

This reverts commit 8381f5f57f.

* add

* Revert "add"

This reverts commit ee14d9ac82.

---------

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2023-06-23 19:45:12 +08:00
Burning_TNT cf61abcf03 Render supported mod loader in org.jackhuang.hmcl.ui.versions.DownloadPage.ModItem. Support #2284 (#2296) 2023-06-23 19:43:10 +08:00
zkitefly 9549819571 检测到多个原因时提示 (#2293) 2023-06-23 18:21:28 +08:00
Nathan Du 95b85c13eb Reorder priority of mod info elements in list entry (#2159)
Gives emphasis to the name and version as reported by the mod, instead
of emphasizing the filename.
2023-06-23 18:19:44 +08:00
Burning_TNT d46efe8410 Enable HMCL to update mod from Modrinth (#2236)
* Enable HMCL to update mod from Modrinth

* Fix UI

* Fix: HMCL won't select the latest version when there is newer version in several remote mod repository.
2023-06-23 18:15:16 +08:00
zkitefly 501ecac4d5 添加一些 tooltip (#2215)
* 添加几个 FXUtils.installFastTooltip

* add

* add

* add

* 更新 VersionPage.java

* add

* add
2023-06-23 18:12:59 +08:00
TomatoPuddin 0dce5bc9a6 添加分类modrinth.category.folia (#2300) 2023-06-23 18:11:17 +08:00
Burning_TNT 9092c8b7a6 Fix #2268 (#2295) 2023-06-23 18:10:48 +08:00
Burning_TNT a5858495ef Fix #2281 (#2294)
* Fix #2281

* Use switch instead of Enum.name().toLowerCase(Locale.ENGLISH)
2023-06-23 18:09:20 +08:00
Burning_TNT 88984378e8 Add support to ModLoaderType.QUILT (#2302) 2023-06-23 18:07:31 +08:00
huanghongxun 470c3d6c24 fix(HMCLCore): do not init JavaFX in JTest. 2023-06-16 08:48:59 +08:00
zkitefly 6133e7e730 Update PURCHASE_URL and MIGRATION_FAQ_URL (#2229) 2023-06-16 08:39:16 +08:00
zkitefly c5cbafea60 添加 remove_assets 按钮 (#2257) 2023-06-16 08:38:27 +08:00
zkitefly 167e25e859 添加更多日志分析规则 (#2192)
* 添加“Forge 安装不完整”的检查

* fix

* fix

* add modName

* 更新 CrashReportAnalyzer.java

* 更新 CrashReportAnalyzerTest.java
2023-06-16 08:36:03 +08:00
zkitefly f47bd8a407 更新 CrashReportAnalyzer.java (#2175) 2023-06-16 08:31:44 +08:00
zkitefly 464be411d1 Updata account.failed.server_disconnected (#2247) 2023-06-16 08:23:11 +08:00
zkitefly d3b6eb36d9 Update DefaultGameRepository.java (#2273) 2023-06-16 08:21:27 +08:00
Glavo 71458f5167 启动时检测 Fractureiser 病毒 (#2270) 2023-06-16 08:21:06 +08:00
zkitefly 597730389f Close #1367 (#2232) 2023-06-16 08:18:41 +08:00
zkitefly 56b227f467 添加新显示项目:物理内存大小 (#2279) 2023-06-16 08:17:30 +08:00
Glavo 213b03ea56 Print warning message when signature key is missing (#2283) 2023-06-16 08:13:05 +08:00
Glavo 959c4d0698 检测 macOS 上的 App Translocation (#2252)
* check app translocation on macOS

* update

* update
2023-06-16 08:12:41 +08:00
zkitefly 57b3bd9ff0 修复分类翻译键问题 (#2277)
* 修复分类翻译键问题

* 更新搜索版本号
2023-06-16 08:11:09 +08:00
zkitefly 68f4038879 更新 Issue 模板 (#2278)
* Create feature.yml

* Update config.yml

* Update config.yml

* Update feature.yml
2023-06-16 08:10:44 +08:00
Glavo 6010008409 使用 ObservableMap 记录已显示的提示 (#2271) 2023-06-13 10:21:03 +08:00
Haowei Wen 4b2e857f08 Fix chat is broken on Minecraft 1.20 2023-06-12 00:42:41 +08:00
Glavo 0280a34d00 fix #2267: filter duplicate jars in the class path (#2280) 2023-06-11 15:47:43 +08:00
Glavo 811707b611 代码清理以及微优化 (#2244) 2023-05-26 00:52:46 +08:00
Glavo 9152550bda Suppress compilation warnings (#2243)
* Suppress compilation warnings

* update
2023-05-24 22:05:30 +08:00
Glavo 6cb4d24600 Log XDG_SESSION_TYPE (#2242) 2023-05-24 21:51:22 +08:00
Glavo 49287453eb 优化 VersionNumber (#2240)
* 优化 VersionNumber

* Add more tests

* update

* update tests
2023-05-24 21:28:22 +08:00
Glavo e7c108d843 修复使用系统 GLFW 选项对 1.19+ 不生效的问题 (#2241)
* 修复使用系统 GLFW 选项对 1.19+ 不生效的问题

* Use UTF-8 in NativePatcher

* update
2023-05-24 21:15:09 +08:00
Glavo 6c9bf6cb09 修复翻译 (#2239) 2023-05-23 15:07:53 +08:00
Glavo efc27c8e21 添加 Linux RISC-V 64 平台支持 (#2238) 2023-05-23 05:17:26 +08:00
Glavo 7c5ccc906e 优化 SSL 异常报错信息 (#2234)
* 改进 SSL 异常报错

* update
2023-05-22 16:40:34 +08:00
zkitefly 69a6ff6053 Fix 2226 (#2227) 2023-05-20 02:18:25 +08:00
zkitefly 605d0c6a92 Fix #2204 (#2228)
https://github.com/huanghongxun/HMCL/issues/2204#issuecomment-1519823562
2023-05-20 02:17:18 +08:00
Glavo 9d54ddf456 修复无法使用 macOS aarch64 JRE 启动部分版本的问题 (#2222) 2023-05-10 20:08:53 +08:00
Glavo c6afd53e73 Bump dependencies (#2191) 2023-04-06 02:41:57 +08:00
Glavo fd9bb88033 添加隐藏提示按钮 (#2018)
* 添加隐藏提示按钮

* update

* update

* update

* update
2023-04-06 02:33:22 +08:00
Glavo 316607b333 添加对 Linux RISC-V 64 平台的基本支持 (#2187) 2023-04-01 06:29:51 +08:00
Glavo af36a0ac2a Bump Gradle to 8.0.1 (#2158) 2023-03-01 23:32:24 +08:00
Glavo 7356335106 添加环境变量设置 (#2157)
* Remove unused methods

* 添加环境变量设置

* update

* update
2023-02-26 23:46:00 +08:00
Glavo d8c96769ea Bump dependencies (#2149) 2023-02-24 04:31:45 +08:00
Glavo d71815aa3d 清理编译警告 (#2148) 2023-02-24 04:14:23 +08:00
Glavo 9a4286620f 简化 JFXHyperlink 用法 (#2147)
* 简化 JFXHyperlink 用法

* fix checkstyle
2023-02-24 03:49:13 +08:00
Glavo 0eac7b5d12 Use MESA_LOADER_DRIVER_OVERRIDE on Linux (#2146) 2023-02-24 01:56:01 +08:00
Glavo af63c3aa3a 添加渲染器设置 (#2145)
close #2143
2023-02-24 01:32:15 +08:00
Glavo 72a8199453 将版本高级设置拆分至单独页面中 (#2142)
* update

* update

* update

* update
2023-02-22 20:37:27 +08:00
Glavo 090b83bf9c fix #2129: Fix computePerfHeight of ComponentList (#2139)
* update

* update style

* update

* update

* update

* update
2023-02-19 19:22:01 +08:00
Glavo 6f70da51f9 Use computeIfAbsent in EventBus (#2138) 2023-02-19 13:25:20 +08:00
Glavo ed0a4c8cf4 fix #2131: Hide feedbackTextFlow from gameDirPane if possible (#2136)
* Add scroll bar to GameCrashWindow

* Hide feedbackTextFlow when it is not visible

* fix checkstyle

* update I18N

* update I18N
2023-02-19 12:36:35 +08:00
Glavo 5fa066de6b fix crash (#2124) 2023-02-15 23:27:59 +08:00
zkitefly 1454ea8358 添加提示 (#2108)
* 添加提示

* Update I18N_zh_CN.properties
2023-02-15 17:45:25 +08:00
Glavo ab264955f5 添加动画开关 (#2111) 2023-02-14 19:12:30 +08:00
Glavo 389d430dbd fix #2094 (#2096) 2023-02-14 19:08:26 +08:00
Glavo 8b98932ae8 fix crash (#2115) 2023-02-13 19:59:24 +08:00
Glavo 62f39a3280 更新 I18N (#2083)
* Remove multiplayer messages

* update

* update

* update

* update

* update

* update

* update

* update
2023-02-12 09:33:52 +08:00
Glavo b116186f42 fix export batch script (#2107) 2023-02-11 23:43:13 +08:00
Glavo 40eeee7137 fix #2105: catch NPE in Accounts (#2106) 2023-02-11 22:53:58 +08:00
zkitefly 54eab87356 初始化内容修改为“热度” (#2099)
* 将初始化内容设定为“名称”排序

* 更新 RemoteModRepository.java

* 初始化内容修改为“下载量”

搜索时排序依然是“名称”

* 还是默认“热度”吧

因为一般搜模组都是搜热度比较高的,搜出的结果就较为准确

* Update DownloadListPage.java
2023-02-11 13:51:32 +08:00
zkitefly d828f7bc7b 初始化排序修改为“下载量” (#2097)
* 将初始化内容设定为“名称”排序

* 更新 RemoteModRepository.java

* 初始化内容修改为“下载量”

搜索时排序依然是“名称”
2023-02-11 11:47:26 +08:00
Glavo ae3e135e06 FileSelector needs to update the value after selecting a file (#2095)
* FileSelector needs to update the value after selecting a file

* update
2023-02-11 00:14:51 +08:00
Glavo 5ba8bdad10 Catch i18n exception (#2093)
* Catch i18n exception

* update

* update
2023-02-10 11:01:34 +08:00
Glavo bffbfd7af9 Fix the problem of adding redundant G1 parameters (#2092) 2023-02-09 22:18:01 +08:00
Glavo a8c1e8d2cb Do not save the default number of log lines to the configuration (#2086) 2023-02-09 00:34:17 +08:00
Glavo 230d6b20ab 修复下载源选择 ComboBox 过长的问题 (#2085) 2023-02-09 00:33:47 +08:00
Glavo 109a14dd74 Remove unused files 2023-02-08 23:17:21 +08:00
Glavo 1ac1ff3c95 fix crash 2023-02-08 21:16:24 +08:00
Glavo 7e522ff136 弃用 commons-lang (#2061)
* Remove ArrayUtils

* Remove MutableObject

* VersionRange

* Replace Range with VersionRange

* fix checkstyle

* Update

* update

* update

* fix checkstyle

* fix checkstyle

* update
2023-02-08 19:57:52 +08:00
zkiteflyandYuhui Huang 90c261ddd1 Quilt 相关功能完善 (#2035)
* Quilt 相关功能完善

- Mcbbs 整合包适配 Quilt
- 自定义命令支持 Quilt 环境变量
- 顺便把语言文件内的机翻错误“\:”修复

* Fix

---------

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2023-02-08 19:54:54 +08:00
Sharll b9bdc29dc2 更改版权声明 (#2078)
* Update AboutPage.java

* Update build.gradle.kts
2023-02-08 19:51:47 +08:00
Glavo 13968fe3aa close #1917 2023-02-08 13:09:42 +08:00
Glavo 825df1981d Use FXUtils.bindString in FileSelector 2023-02-08 13:09:42 +08:00
Glavo dd56f4f8a6 fix #2002: Fix smooth scrolling for GTK3 2023-02-08 13:07:44 +08:00
Glavo b9ef10a73d update 2023-02-07 08:07:20 +08:00
Glavo 0f68a7c703 fix checkstyle 2023-02-07 08:07:20 +08:00
Glavo f5b24b62f1 update 2023-02-07 08:07:20 +08:00
Glavo f9eca35422 update 2023-02-07 08:07:20 +08:00
Glavo 394a74e566 search bar 2023-02-07 08:07:20 +08:00
Glavo 870b73c101 Revert "close #2043: Make ModListPage searchable (#2044)"
This reverts commit 9a3b8545ca.
2023-02-07 08:07:20 +08:00
zkitefly d89a32f620 https://hmcl.huangyuhui.net/eula to https://docs.hmcl.net/eula/hmcl.html (#1967) 2023-02-06 22:58:44 +08:00
zkitefly 29410b951d 优化日志分析器 (#2038)
* 更新 CrashReportAnalyzer.java

* Edit

* Fix

* add

* 删除 CrashReportAnalyzerTest.java

试试删掉的话能不能构建成功

* Fix

* Fix

* Revert "删除 CrashReportAnalyzerTest.java"

This reverts commit 4fe873cfcb.

* 换回 LibericaJDK

* Fix

尴尬(

* 更新 GameCrashWindow.java

修改大小以显示更多内容(防止左下角按钮被吞掉)

* 更新 I18N_zh_CN.properties

- 修改链接,可以跳转至特定地方
- 统一叫法 "全局(特定)游戏设置"
- 将升级显卡的相关教程文章更换

* Fix

* Update CrashReportAnalyzer.java

* Fix

- 删除重复检查项

* 添加日志检查

https://paste.ee/p/vv7DD

* fix

* add

怎么tm还来一个

* 更新 CrashReportAnalyzerTest.java

* 调整

* fix

* 更新 I18N_zh_CN.properties

* 更新 I18N_zh_CN.properties

* 添加重复安装 Optifine检查

https://github.com/huanghongxun/HMCL/discussions/1904#discussioncomment-4340734

* 添加重复安装 Forge 的检查

test

更新 CrashReportAnalyzer.java

fix

* 完善 OPTIFINE_IS_NOT_COMPATIBLE_WITH_FORGE

完善 OPTIFINE_IS_NOT_COMPATIBLE_WITH_FORGE

https://github.com/huanghongxun/HMCL/discussions/1904#discussioncomment-4332485
https://github.com/huanghongxun/HMCL/discussions/1904#discussioncomment-4332522

* fix

* 调整 FABRIC_WARNINGS

https://paste.ee/p/KOTyX
2023-02-06 22:56:58 +08:00
Glavo 461cde3282 改进 LogWindow (#1971)
* Add CircularArrayList

* fix test

* update

* update
2023-02-06 22:55:16 +08:00
Glavo 94b54f3dce Exclude JavaFX (#2073) 2023-02-06 22:54:41 +08:00
zkitefly 6e20ce4db3 插入更多”请求帮助”链接 (#2051)
* 更新 I18N_zh_CN.properties

* 更新

* 更新 I18N_zh_CN.properties
2023-02-06 22:50:09 +08:00
Glavo e76d520cac x86-64 平台上不自动选择 32 位 Java (#2042)
* close #2041

* update

* update
2023-02-06 22:49:37 +08:00
CyenochandCyenoch 9a3b8545ca close #2043: Make ModListPage searchable (#2044)
* close #2043: Make ModListPage searchable

* Optimize layout

---------

Co-authored-by: Cyenoch <cyenoch@qq.com>
2023-02-06 22:49:22 +08:00
Glavo bb66be7c3a 向非官方构建添加提示 (#2069)
* update

* Add GitHub SHA to manifest

* Add unofficial to build channel

* Optimize IntegrityChecker

* update hint

* Add hint
2023-02-06 22:47:23 +08:00
zkitefly 8514eb904d 将日志窗口的最大输出值调整至 10000 (#2065) 2023-02-06 22:45:30 +08:00
Glavo a6c8867ec4 Use Locale.forLanguageTag (#2064) 2023-02-06 22:45:03 +08:00
Glavo 5753c2993c Allow users to override system properties (#2062)
* Allow users to override system properties

* update
2023-02-06 22:44:48 +08:00
Glavo ec6637728a 代码清理 (#2072)
* Remove unused methods

* newRaisedButton

* update

* update

* update

* update

* update

* toLowerCase and toUpperCase

* update

* update
2023-02-06 22:44:19 +08:00
Glavo 3198daa60a fix #2070 2023-02-06 11:01:33 +08:00
Glavo 1a6dffab74 fix #2008: Replace JFXTreeTableView with TableView
close #2068
2023-02-05 17:12:23 +08:00
zkitefly d54d9ae5cb 修改 登录失败 弹窗 (#2057) 2023-02-02 21:36:08 +08:00
Glavo e543ee3f55 建议 Forge 用户使用官方推荐版本的 Java 启动游戏 (#2033)
* Share LibraryAnalyzer

* Add a new JavaVersionConstraint rule

* Update message
2023-02-02 21:35:22 +08:00
zkitefly f7526d4cdc 将初始化内容设定为“名称”排序 (#2054)
* 将初始化内容设定为“名称”排序

* 更新 RemoteModRepository.java
2023-02-02 21:33:43 +08:00
Glavo cf136fe7df 导出更多日志 (#1970)
* close #1883: export vm crash log

* fix #2047: export fml-client-latest.log
2023-02-02 21:30:15 +08:00
Glavo 5d9d9b9cc4 fix #2020: write stylesheet with default charset (#2024)
* fix #2020: write stylesheet with default charset

* Check JavaFX Version

* javadoc

* close #1051
close #1957
2023-02-02 21:29:24 +08:00
Glavo 6a05df2c35 Prompt when there is no mods to update (#2026) 2023-02-02 21:28:32 +08:00
Gamom 34cfdb8073 添加翻译键:modrinth.category.datapack (#2053)
* 添加翻译键:modrinth.category.datapack

* More translation
2023-02-02 21:28:15 +08:00
Glavo 259ed570ab Remove drawAvatarJ2D (#2027) 2023-02-02 21:26:58 +08:00
Glavo c86302df58 Update NormalizedSkin (#2036)
* Update NormalizedSkin

* Add tests

* update

* update
2023-02-02 21:26:27 +08:00
Glavo 3d9c9689e3 fix #2023: Set Tooltip text fill (#2028)
* fix #2023: Set tooltip text fill

* close #1077
2023-02-02 21:25:27 +08:00
Glavo d4398764a1 Update Texture (#2037)
* Update Texture

* fix checkstyle
2023-02-02 21:25:05 +08:00
zkitefly fb573d8205 Update bug-report.yml (#2050) 2023-02-02 21:19:30 +08:00
CyenochandCyenoch da5097ab78 Related to #1961: Add a Dialog to warn. (#2055)
Co-authored-by: Cyenoch <cyenoch@qq.com>
2023-02-02 21:19:07 +08:00
Glavo 59b5aabfe5 Bump OpenJFX from 17.0.2 to 19.0.2.1 (#2029)
fix #1896
fix #1923
2023-02-02 21:15:53 +08:00
Glavo 8355f02de0 fix memory leak (#2034) 2023-02-02 21:15:34 +08:00
zkitefly bd4b334185 清除所有 QQ频道 相关内容 (#2016)
* 删除日志分析器的QQ频道链接

* 清除所有QQ频道相关内容

应该都清理完了吧
2023-01-16 22:48:39 +08:00
Glavo bbe0a3f567 #1998: Log prism pipeline name (#2007)
* #1998: Log prism pipeline name

* update

* update
2023-01-14 22:36:27 +08:00
Glavo 41620dcd70 fix #2006 (#2009) 2023-01-14 22:35:24 +08:00
Glavo e865b31b62 fix I18N_es (#2011) 2023-01-14 22:35:04 +08:00
Glavo 92ce7c83ca 清理代码 (#1990)
* cleanup

* Cleanup StringUtils

* Cleanup CommandBuilder

* Cleanup CommandBuilder

* Use 'String::replace(char, char)'
2023-01-14 02:37:55 +08:00
Glavo 753ba956fe fix #1705: Do not re-enable mod after update (#2001)
* fix #1705: Do not re-enable mod after update

* update

* Do not select disabled mods by default
2023-01-14 02:37:11 +08:00
Glavo 0130676b42 fix #1999: Add overrideJavaArguments to LaunchOptions (#2000)
* fix #1999: Add overrideJavaArguments to LaunchOptions

* fix checkstyle
2023-01-14 02:16:20 +08:00
Glavo 0ec00ede70 close #1980: Make LittleSkin optional (#1995)
* close #1980: Make LittleSkin optional

* update

* update

* update
2023-01-14 02:15:07 +08:00
Glavo 42dd877b97 Bump junit from 4 to 5 (#1997) 2023-01-14 01:48:38 +08:00
Glavo 00e8ae691c fix CI 2023-01-12 15:17:37 +08:00
Glavo 1cc429c7ca 优化 mod 列表加载速度 (#1985)
* Share ZipFileSystem

* fix memory leak
2023-01-08 18:15:55 +08:00
Glavo bf822d7cf7 Update all icons (#1988) 2023-01-08 18:15:01 +08:00
Glavo 5dc1d14f73 fix #1562: Do not save stage size when window is minimized (#1979) 2023-01-07 21:38:36 +08:00
Glavo 15a93a9dc0 Deprecated Pack200 (#1977) 2023-01-07 21:38:06 +08:00
Glavo 0c0cd97a6e Check whether HMCL is in temporary path when starting (#1960)
* Check whether HMCL is in temporary path when starting

* Update the path of the trash folder on Linux
2023-01-07 21:36:10 +08:00
Glavo b132aba1e3 fix #1927: limit title label width (#1982) 2023-01-07 21:34:01 +08:00
zkitefly 7d97e5d104 改进 Github Issue 模板 (#1976)
* Update bug-report.yml

* Update bug-report.yml

* Update config.yml

* Update bug-report.yml

* Update bug-report.yml

* Update bug-report.yml

* Update bug-report.yml
2023-01-07 21:33:35 +08:00
Glavo 7c8d75f5a4 fix memory leak (#1986) 2023-01-07 21:33:16 +08:00
Glavo 1b5522958a 更新 FeedbackPage (#1913)
* Update FeedbackPage

* Update logo

* close #1259
2023-01-07 21:32:52 +08:00
Glavo 593e70fa43 优化对 Java 兼容性问题的检测 (#1973)
* Optimization Tips

* cleanup code
2023-01-07 21:29:18 +08:00
Glavo 6475e03c01 Improve quilt support (#1974) 2023-01-07 21:27:25 +08:00
Glavo 2d4ca3100e fix NPE (#1983) 2023-01-07 21:26:01 +08:00
Glavo 3f0e8138b1 fix Modpack::acceptFile (#1972)
* fix Modpack::acceptFile

* fix separator
2023-01-07 21:25:36 +08:00
Glavo 9e6d6ff95a Delete javaw.exe extension filter (#1969) 2023-01-07 21:24:22 +08:00
Chen Youyou 6ae6020b73 语言文件中错别字改正 (#1981)
* 把“及表示登录成功”的“及”改为“即”

又写错别字了。。

* 把“及表示登入成功”的“及”改为“即”

又写错别字。。
2023-01-07 21:24:00 +08:00
Glavo 4b14673aaf Update JavaFXPatcher (#1955) 2023-01-01 14:35:55 +08:00
Glavo bd7365e944 Add icon for qq channel (#1956) 2023-01-01 14:34:58 +08:00
Glavo 8c38e4c628 fix tags color (#1959) 2023-01-01 14:34:36 +08:00
Glavo 2b4e476153 fix crash on Java 8 (#1954) 2022-12-30 17:21:57 +08:00
Glavo 0f0f5cbc28 fix #1905: fix path splicing (#1951) 2022-12-30 17:01:49 +08:00
Glavo bed74dc66c fix find HMCL directory (#1953) 2022-12-30 17:00:10 +08:00
Glavo ba196dcdf4 Do not put LittleSkin in authlibInjectorServers (#1952) 2022-12-30 16:59:29 +08:00
zkiteflyandYuhui Huang 9f8ca0255b 修改提示 (#1919)
* 修改提示

* fix

* Update I18N_zh_CN.properties

Co-authored-by: Yuhui Huang <jackhuang1998@gmail.com>
2022-12-30 02:07:09 +08:00
zkitefly b6a156b0d6 修复 (#1921) 2022-12-30 02:04:15 +08:00
Glavo d032b3c677 close #1376: 在用户文件夹中存储账户信息 (#1941)
* Save accounts in hmcl dir

* close #1377: Add conversion button

* fix YggdrasilAccount::getIdentifier()

* Check whether the account exists before moving

* Add server url to selected account

* Add global prefix
2022-12-30 02:03:19 +08:00
Glavo 4c2d1063ec fix #1922: Add Xms only when the min memory is less than the max memory (#1949) 2022-12-30 01:59:02 +08:00
Yingchi Long 2e7e2d261c feat: use XDG_DATA_HOME as for Linux working directories (#1948) 2022-12-30 01:58:15 +08:00
zkitefly 9750c69742 fix #1926 (#1940) 2022-12-28 23:22:46 +08:00
Glavo d67dd700e9 修复主题相关问题 (#1915)
* Update blue.css

* fix

* fix

* Cleanup Code

* Use blue.bss

* fix read theme
2022-12-17 18:34:10 +08:00
zkitefly 7f599638d7 完善 QQ 频道 的入口 (#1910)
* 完善 QQ 频道 的入口

* 日志分析窗口结果中添加相关入口

* 完善

* 取消使用qq图标
2022-12-11 00:43:26 +08:00
Glavo bf04854e68 弃用 FXML (#1911)
* Remove message-dialog.fxml

* Remove authlib-injector-server-add.fxml

* Remove profile.fxml

* Remove task-dialog.fxml

* Flatten AddAuthlibInjectorServerPane

* Flatten MessageDialogPane

* Remove unused fxml files

* Remove modpack.fxml

* Remove modpack-source.fxml

* Remove selection.fxml

* Remove type.fxml

* Remove versions.fxml

* Remove input-dialog.fxml

* Remove fxml

* Cleanup code

* Cleanup code

* Cleanup Code

* Cleanup Code
2022-12-10 22:36:14 +08:00
Glavo 2bcf9ee39e Optimize automatic memory allocation (#1914) 2022-12-10 22:32:09 +08:00
huanghongxun 7a90035278 feat(about): add link to qq channel. 2022-12-07 15:52:55 +08:00
Glavo a7108f318a Adjust JVM arguments priority (#1898)
* Adjust JVM arguments priority

* #1889: Use the default JVM arguments consistent with the official launcher

* Follow -Xmx settings in modpack info

* Do not force generation of -Xmx
2022-12-06 13:34:32 +08:00
zkitefly a3eaccb45b exe and sh download java link update (#1903)
* Update main.cpp

* exe and sh download java link update

* fix
2022-12-06 13:33:00 +08:00
zkitefly e0c97da9fd Update main.cpp (#1900) 2022-12-03 23:22:15 +08:00
zkitefly 3843002113 (c)->© (#1899)
* Update I18N_zh_CN.properties

* Update I18N.properties

* Update I18N_es.properties

* Update I18N_ja.properties

* Update I18N_ru.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N.properties

* `(c)`->`©`

* fix

* Update AboutPage.java

* Update AboutPage.java
2022-12-03 15:05:42 +08:00
Glavo 1ec9e41459 fix #1885: filter letter color code (#1890) 2022-12-02 17:28:25 +08:00
Glavo a6231ac593 修复内存泄漏 (#1888)
* Fix memory leaks

* Cache icon in cell

* Lazy acquisition of availableCharsets
2022-12-02 17:27:52 +08:00
Glavo ea91ee91ec close #1199: Reimplement Zipper (#1891) 2022-12-02 17:26:00 +08:00
Glavo d4c0c30283 修复在精简 JRE 上崩溃的问题 (#1893)
* Do not load javafx.web on unsupported platforms

* No longer requires jdk.zipfs to exist
2022-12-02 17:23:08 +08:00
Glavo 9e2c46a6f8 Gradle 7.6 (#1895) 2022-12-02 17:22:09 +08:00
Glavo ce31bcc6e0 Catch the exception that the OperatingSystemMXBean does not exist 2022-12-01 03:51:21 +08:00
Glavo d11e22026c 修复遗留问题 (#1875)
* Check the sha1 file of java in the minecraft runtime dir

* Fix download Java on macOS

* misc

* Verify Java at startup

* Find the Java installed by official launcher on osx-arm64

* Extract the downloaded file to a temporary file

* Rollback JavaRepository.getSystemJavaPlatform
2022-11-26 21:43:35 +08:00
Glavo a96fe298b8 fix #1108 (#1882)
close #1791
close #1868
2022-11-26 21:43:03 +08:00
Glavo 8e28c2487f Load gif background (#1877) 2022-11-26 20:33:46 +08:00
Glavo fda6f75333 fix #1553: Hide popup before rolling back mod (#1881) 2022-11-26 20:33:02 +08:00
zkitefly e414450f3c 优化日志分析器 (#1813)
* 优化日志分析器

* 修复翻译错误

* 修复翻译错误

* 修复翻译错误

* Update CrashReportAnalyzer.java

* Update I18N_zh_CN.properties

* 逐步修复问题

* 逐步修复问题

* 尝试修复

* 修复错误

* 修复错误

* 尝试

* 尝试

* 修复

* 按照要求修改

* 依据 Check Style 修复问题

* 按提示修改

* 修复错误

* Update CrashReportAnalyzer.java

* 删除

* Update I18N_zh_CN.properties

* Update CrashReportAnalyzer.java
2022-11-25 22:45:00 +08:00
Glavo 95e15d7df4 Replace ObservableMap with MapProperty (#1870) 2022-11-25 22:40:16 +08:00
Glavo 7e826a5952 close #1803: Add new rule to CrashReportAnalyzer (#1872) 2022-11-25 22:37:41 +08:00
Glavo 43e2644f0e fix NPE (#1871) 2022-11-25 22:37:05 +08:00
Glavo a0ef16bef4 close #1781: Add suggestions for naming (#1873) 2022-11-25 22:35:01 +08:00
Glavo f5d83095cc 修复 Java 下载相关问题 (#1874)
* fix #1561: Replace existing files when downloading Java
close #1756

* close #1755: Download Java using mirror

* Skip existing files
2022-11-25 22:34:26 +08:00
Glavo a94bb407e5 Submit the modification of JFXTextField after editing (#1869) 2022-11-25 17:20:16 +08:00
Glavo 6611774aa9 Fix the problem of garbled logs on Java 18+ (#1866) 2022-11-25 17:17:31 +08:00
Glavo e1eb40c129 Optimization HMCLProcessListener::onLog (#1867) 2022-11-25 17:16:23 +08:00
Glavo 2e532be34c 将 Quilt 图标添加至 VersionIconDialog (#1864)
* Safely deserialize VersionSetting

* Add quilt to VersionIconDialog
2022-11-24 12:54:56 +08:00
Glavo b9d0cfc12a Update GitHub Action (#1863) 2022-11-24 12:54:48 +08:00
Glavo 3d88bc35bb fix memory leak (#1865) 2022-11-24 12:53:16 +08:00
Glavo 8c9c8fd28f 修复 macOS Dock 中的图标 (#1862)
* High resolution logo

* Load original icon on Java 8
2022-11-24 12:52:54 +08:00
Glavo 5e685b7f26 Modify the version number for unofficial builds (#1861) 2022-11-23 22:54:28 +08:00
Glavo c76b562705 Remove log4j-patch (#1860)
* Remove log4j-patch

* Update I18N
2022-11-23 17:20:15 +08:00
Glavo 4c9f8d3dca Complete the missing translation (#1859) 2022-11-23 17:19:45 +08:00
Glavo 0f26ae5cfc 代码清理与修复 UI 卡顿资源占用过高的问题 (#1849)
* Lazy initialization of Swing

* Load ISRG Root X1 certificate only on Java 8

* Replace JOptionPane with JavaFX Alert

* Avoid using java.awt.Desktop

* Rewrite TexturesLoader

* Optimization SelfDependencyPatcher

* fix typo

* close #968: Use computeIfAbsent to ensure thread safety

* Optimization GameVersion::minecraftVersion

* code cleanup

* Set the initial capacity of readFullyWithoutClosing

* code cleanup

* Mark inner classes as static if possible

* Cache version icon

* Code cleanup

* Fix ListView scrolling performance issues

* DatapackListPage::items

* Replace OutputStream with FileChannel::write
2022-11-23 16:33:14 +08:00
Glavo 035469240d close #1855: Add 1.19 to DEFAULT_GAME_VERSIONS (#1857) 2022-11-23 16:32:20 +08:00
Glavo 7ee81f7425 Deny access to CurseForge without api key (#1858) 2022-11-23 16:32:07 +08:00
zkitefly 024c90d688 在HMCL显示changelog时自动定位该更新内容 (#1850)
* 在HMCL显示changelog时自动定位该更新内容

* 尝试修复错误

* updata

* fix
2022-11-23 14:07:13 +08:00
Glavo 1253272626 fix #1852: Lookup manifest in this jar (#1853) 2022-11-23 14:06:35 +08:00
zkitefly 73b938cfd9 修复微软账户登录弹窗不显示“忘记密码”的问题 (#1829) 2022-11-13 23:44:01 +08:00
Glavo ee843543bf 性能与内存优化 (#1848)
* optimization

* Make ManagedProcess::getLines thread safe
2022-11-13 23:43:45 +08:00
skbeh fa8a479aac Bump Gradle Shadow to 7.1.2 (#1831)
This allows compilation with `sourceCompatibility = 17`.
2022-11-13 23:40:10 +08:00
Glavo bac14db432 修复无法正确处理带有 Quilt 的 MultiMC 整合包的问题 (#1841)
* fix #1760: typo

* export quilt info in mmc modpack
2022-11-13 23:39:56 +08:00
zkitefly f6242ea868 添加提示 (#1844) 2022-11-13 23:39:40 +08:00
zkitefly 16db35655a 修改 2022-11-11 21:50:10 +08:00
Glavo 53336aedc9 Remove HiPer Support 2022-11-11 18:30:27 +08:00
zkitefly eaddb67932 Update HelpPage.java (#1827) 2022-11-05 23:21:47 +08:00
Glavo c7b7dd9500 fix crash (#1826) 2022-11-05 22:51:10 +08:00
Glavo 9901bf738d fix crash (#1825) 2022-11-05 22:12:44 +08:00
huanghongxun a3d8c6b3b3 feat(skin): introduce new official skins. 2022-11-05 20:22:02 +08:00
huanghongxun 948928f740 feat: update HMCLauncher.exe 2022-11-05 20:11:22 +08:00
Rei Hatada 71e98c035e change (#1824) 2022-11-05 19:51:39 +08:00
Glavo af107b15c0 完善 HiPer 支持 (#1821)
* Display hiper token expiration time

* Automatically download missing files when starting HiPer

* Disable start HiPer when token is empty
2022-11-05 19:36:17 +08:00
Glavo 8687bb9487 修复 MessageDialogPane 总是折叠内容的问题 (#1815)
* fix #1751: Use ScrollPane in message dialog only when needed
close #1777

* Parse segment only if it contains xml tag
2022-11-05 19:28:24 +08:00
huanghongxun a70e53364d fix(multiplayer): force using authlib-injector in multiplayer mode. 2022-10-30 13:45:34 +08:00
zkitefly 11a1c37765 将日志更换至docs.hmcl.net (#1812)
* 将日志更换至docs.hmcl.net,以便更新日志

* 修改链接

* 更换目录
2022-10-30 01:29:51 +08:00
Glavo 68a8944810 改进 HiPer 支持 (#1763)
* Disable start hiper button when token is empty

* Use gsudo start hiper

* Automatically request administrator privileges on Windows and Linux

* Automatically start hiper with sudo on mac

* Update I18N

* Prompt user to chown unwritable files

* Update HiPer file verification failed prompt

* Log other hiper exceptions

* temporarily disable gsudo

* Detect admin rights with 'net session'

* Hide the hint pane when running with administrator privileges

* Prompt user when starting HMCL with sudo

* Fix ClassNotFoundException when no JavaFX

* Update the prompt when the permission cannot be obtained

* Add support for saving multiple HiPer configurations

* update link
close Glavo/HMCL#3

* Detect RISC-V 32

* Save original hiper authorization information

* Add support for importing and exporting license files

* Set initial license file name

* Complement the missing message

* Handling gsudo failure to obtain permission
2022-10-25 21:55:12 +08:00
zkitefly b280b238df 尝试完成 #1780 (#1789) 2022-10-25 21:52:23 +08:00
huanghongxun 0f39aad429 fix(auth): legacy user type for OfflineAccount. 2022-10-25 21:48:55 +08:00
Glavo ff78297ee5 fix IntegrityCheck::performCheck (#1792) 2022-10-23 23:28:22 +08:00
Glavo f75bbdcecc Log HMCL jar path (#1794) 2022-10-23 23:27:42 +08:00
Glavo 694e31a7fe Cache manifest in JarUtils (#1797) 2022-10-23 23:27:21 +08:00
Glavo c113670cda Migrate HMCL_DIRECTORY to the user directory (#1785) 2022-10-09 13:21:02 +08:00
zkitefly 1b3c399335 添加“测试能否联机”按钮 (#1757)
* add

* 修改图标(其实我不知道该弄啥图标好……)

* fix
2022-10-09 11:18:00 +08:00
huanghongxun 45cbe20449 fix: crash when there is an incorrect authlib-injector server url. Closes #1774. 2022-10-08 17:39:04 +08:00
zkiteflyandYuhui Huang 4d606510f6 修改 登录失败 弹窗 (#1782)
* 修改 登录失败 弹窗

* fix

* Update I18N.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Edit

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-10-08 17:33:07 +08:00
huanghongxun a6639552cf fix: lblCreationWarning not wrapping text. Closes #1783. 2022-10-08 17:31:28 +08:00
zkitefly 2a088ec555 更新多人联机页面 (#1758)
* 更新 multiplayer.error.file_not_found 翻译

* 修改翻译

* 移除标题

* add

* try fix

* try fix

* try fix

* try fix

* try fix

* try

* try

* try

* fix

* fix

* add

* fix

* fix

* add

* fix

* edit
2022-10-08 17:13:55 +08:00
zkitefly 1c46d63367 try (#1768) 2022-10-08 17:12:13 +08:00
zkitefly f2b7fae694 添加“忘记密码”按钮 (#1765) 2022-10-08 17:11:56 +08:00
Glavo 43986676d8 修复中文路径下启动 <= 1.12.2 的版本没有声音的问题 (#1754)
* Use native encoding by default

* Fix the problem that LWJGL fails to load OpenAL under Java 18
2022-10-08 17:10:27 +08:00
Glavo 2649e79d47 fix build fails with Java 19 (#1784) 2022-10-08 17:09:12 +08:00
zkitefly f83d3cd769 Fix https://github.com/huanghongxun/HMCL/issues/1703 (#1753) 2022-09-28 19:14:15 +08:00
zkitefly f124910f94 修改多人联机帮助与提示 (#1733)
* 修改多人联机提示

* text

* 撤回修改

* 1

* 修改帮助

* fix

* try

* Delete try

* Update I18N.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* 修改提示框样式

* 修改提示框样式

* fix

* 修改样式

* 修改样式

* 修改提示框样式

* 修改翻译

* 修改翻译

* add

* return

* add

* add

* 去掉图标

* 试图让左边图标为空

* Update MultiplayerPageSkin.java

* ruturn

* add

* add

* Update MultiplayerPageSkin.java

* Update I18N_zh.properties

* Update I18N_zh_CN.properties
2022-09-25 14:25:05 +08:00
zkitefly c1712c3e1e 修改翻译 (#1742) 2022-09-25 14:23:39 +08:00
huanghongxun d817c80629 fix: checkstyle. 2022-09-25 00:31:51 +08:00
huanghongxun 8d830dbf96 fix(microsoft): ok cannot be clicked. Closes #1729. 2022-09-24 23:54:36 +08:00
huanghongxun 0396e96334 feat(multiplayer): update agreement. Closes #1724. 2022-09-24 23:50:48 +08:00
huanghongxun 7b23c7bc79 feat(multiplayer): link to video tutorial. 2022-09-24 23:36:48 +08:00
huanghongxun d3ca50259a fix(modrinth): quilt is already supported. Closes #1746. 2022-09-24 23:16:11 +08:00
huanghongxun 499a65d5fb fix(multiplayer): connection. 2022-09-24 23:03:07 +08:00
huanghongxun 0b61f9856c fix(multiplayer): ui incorrect 2022-09-24 21:55:53 +08:00
huanghongxun c8409dd258 build: update gradle. 2022-09-24 18:38:04 +08:00
huanghongxun e5ea4cd79f feat(multiplayer): broadcast address to local network. 2022-09-24 18:31:22 +08:00
huanghongxun 4d54c7f6ba feat(multiplayer): show token expire time. 2022-09-24 18:31:22 +08:00
Cato : Love the world and myselfandzkitefly 0f4a5c26df 远端配置无法读取,使用本地缓存 (#1740)
* 远端配置无法读取,使用本地缓存

* 远程配置&本地配置确认

* fix err

* fix-err

* fix-err

* fix-err

* fix-err

* restore

* code clean

* fix-err

* add

* fix

* 远端读取异常日志

Co-authored-by: zkitefly <64117916+zkitefly@users.noreply.github.com>
2022-09-24 18:17:34 +08:00
Akira Yamazaki a8b09e2195 修复执行 PreLaunchCommand 时会被阻塞的问题 (#1737) 2022-09-24 16:57:19 +08:00
Cato : Love the world and myself 3ed6e38ad3 新增有效期getValidAt #1727 (#1732)
* 界面标题描述纠正

* 新增有效期getValidAt #1727

* fix err

* fix-err-again

* fix-err

* fix

* fix-4

* rename

* fix

* fix

* code clean
2022-09-24 16:56:19 +08:00
huanghongxun 2028b0ef9c fix(multiplayer): restore LocalServerBroadcaster and impl port forwarding. 2022-09-17 18:36:12 +08:00
zkitefly aa7f9b91c7 修改微软账户重新登录页面提示 (#1710)
* 修改微软账户重新登录页面提示

* 添加忘记添加的

* 修改翻译

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties
2022-09-17 12:15:56 +08:00
Cato : Love the world and myself d6598310b2 界面标题描述纠正 (#1728) 2022-09-17 12:11:00 +08:00
zkitefly ce6c06987c 修改多人联机提示 (#1725)
* 修改多人联机提示

* 修改

* fix
2022-09-17 12:10:46 +08:00
huanghongxun 90955d7916 fix(ui): ComponentList prefheight wrong. 2022-09-16 02:34:23 +08:00
huanghongxun b3bbd21f1f chore(multiplayer): remove JSTUN. 2022-09-16 01:41:23 +08:00
Cato : Love the world and myselfandYuhui Huang 735a407f0e 缺少字符矫正 / NAT 检测移除 (#1718)
* 缺少字符矫正 / NAT 检测移除

* 错误修复

* 修正nat移除不彻底导致的编译错误

* 授权协议发布

* code-clean

* fix-err

* fix-err-2

* update-in18

* url-fix

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-09-16 01:18:26 +08:00
Glavo 8461b0418c close #1534: open 'java.com' when need to download Java 8 (#1716) 2022-09-15 21:28:02 +08:00
Glavo e46ab8bf9e fix the build errors and checkstyle (#1717) 2022-09-15 21:27:43 +08:00
zkitefly 330548b24e 添加游戏档案创建链接 (#1696)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* add makegameidsettings

* fix

* fix

* fix

* Update CreateAccountPane.java

* Update CreateAccountPane.java

* 听取意见,修改变量名为createProfileLink
2022-09-14 21:46:38 +08:00
Cato : Love the world and myselfandYuhui Huang cf5ccca42d tap/tun二选一,目前优先tun先看效果。 (#1713)
* tap/tun二选一,目前优先tun先看效果。

* 描述修正

* 修正故障描述

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-09-14 21:45:55 +08:00
Cato : Love the world and myself a18bd0729b 配置文件读取失败及证书失效判断 (#1715)
* 配置文件读取失败及证书失效判断

* 描述修正
2022-09-14 21:43:44 +08:00
huanghongxun 662b42c40f fix(multiplayer): get ip from hiper log. 2022-09-12 23:05:13 +08:00
huanghongxun b69c400a08 fix(multiplayer): get ip from hiper log. 2022-09-12 22:45:38 +08:00
huanghongxun f1eb2da57c feat(multiplayer): hiper. 2022-09-12 22:38:36 +08:00
huanghongxun 99be810b5e feat(multiplayer): wip: hiper 2022-09-12 22:38:36 +08:00
zkitefly 03541a6f85 修改 response.errorDescription.contains() 参数 (#1708)
* 添加两个 response.errorDescription.contains()

* 再添加一个qwq

* 删除重复项

* 恢复删除的项

* 修改

* 修改
2022-09-10 21:50:08 +08:00
Chikage0o0 5fe1ef899d fix modrinth modpack install (#1702)
* fix modrinth modpack install

* fix NullPointerException
2022-09-10 21:49:31 +08:00
CNCloud_Casey 6a81f4d240 更新整合包安装的文本介绍 (#1695)
* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_ru.properties

* Update I18N_ja.properties

* Update I18N_es.properties

* Update I18N.properties
2022-09-05 20:11:10 +08:00
zkitefly 3c6aa489e8 添加sh支持 (#1697) 2022-09-05 20:10:49 +08:00
zkitefly ba7043fa92 修改一些提示 (#1699) 2022-09-05 20:10:37 +08:00
huanghongxun bfb6c99484 fix: checkstyle. 2022-09-04 00:26:14 +08:00
huanghongxun a6b26a0cd9 fix(microsoft): show relogin dialog when grant expired. Closes #1691. 2022-09-03 23:35:09 +08:00
huanghongxun 0c7b0f285e fix(microsoft): retry 5 times when socket timeout. Closes #1691. 2022-09-03 23:33:11 +08:00
huanghongxun 5e1ae56740 fix(ui): error dialog overflow. Closes #1677. 2022-09-03 22:44:38 +08:00
huanghongxun b17bac050b fix(ui): not parsing xml. Closes #1678. 2022-09-03 22:28:58 +08:00
huanghongxun b1952a7b37 fix(ui): not wrapping text. Closes #1673. 2022-09-03 22:26:05 +08:00
huanghongxun cc1fc1e4be fix(ui): some text overflow. Closes #1694. 2022-09-03 22:21:38 +08:00
Glavo 6c07924d9b 完善 osx-arm64 原生支持 (#1689)
* Add 1.6~1.18.2 support for osx-arm64

* fix crash on 1.12

* Update I18N
2022-09-03 16:55:27 +08:00
Glavo 835668d781 Update purchase link (#1692) 2022-09-03 16:53:55 +08:00
Glavo 8f7c09d2cc 添加 Linux MIPS64el 平台支持 (#1686)
* Add support for linux-mips64el

* Update PLATFORM.md
2022-09-02 11:37:09 +08:00
Chikage0o0 837939e27c Fix #1684 (#1685)
* 修复生成option.txt与YOSBR模组的冲突

* only search 1 layer
2022-09-02 11:36:58 +08:00
Glavo 058a8e2335 Update JavaVersionConstraint (#1683) 2022-09-01 22:20:16 +08:00
CNCloud_Casey a5af5265be Update README (#1682) 2022-08-31 15:25:29 +08:00
Gamom 8a724587ca 更新错误的翻译键 world.info.player.game_type.adventure (#1674)
* 更新错误的翻译键 world.info.player.game_type.adventure

我不理解 adventure 冒险 为什么会 翻译成极限 ...

* Update I18N_zh.properties
2022-08-31 14:20:56 +08:00
zkitefly bef9c0ad88 Fix #1675 (#1676) 2022-08-31 14:20:39 +08:00
Glavo 8d29775410 添加 Windows on Arm 平台的支持 (#1679)
* Add Minecraft 1.6~1.18.2 support for windows-arm64

* Prompt at startup on non-x86 platforms

* Automatically download JavaFX on windows-arm64

* Update PLATFORM.md
2022-08-31 14:20:27 +08:00
Glavo 620c0e4c73 Automatically download JavaFX on linux-loongarch64_ow (#1670) 2022-08-29 19:28:20 +08:00
Glavo b5c92c1564 Fix stuck on startup when no suitable Java is found (#1671) 2022-08-29 19:27:51 +08:00
huanghongxun 3c3c28aaa5 fix(launch): not escape spaces in exported shell script. Closes #1541. 2022-08-29 00:19:32 +08:00
huanghongxun a80fc34ef1 fix(launch): should not create file when throwing CommandTooLongException. Closes #1606. 2022-08-29 00:03:35 +08:00
huanghongxun 08d7ff138b fix(login): prompt to retry logging in. Closes #1658 #1591 #1544 1608. 2022-08-28 23:57:47 +08:00
huanghongxun 0c172e5d08 fix(crash): incorrect layout for feedbackLabel. Closes #1667. 2022-08-28 21:45:05 +08:00
Glavo bce2d1ea5c Revert ExitWaiter (#1668) 2022-08-28 21:05:43 +08:00
huanghongxun cbc1e26b6f feat(log): hyperlink for log analyzer feedback. Closes #1652. 2022-08-28 17:12:03 +08:00
Yuhui Huang 3b5ddcafa6 fix(quilt): Quilt Hash not enabled. Closes #1657. 2022-08-28 17:03:54 +08:00
Glavo c6a5624708 引导用户下载 Java (#1659)
* Direct users to download Java

* Read JVM options from environment variables
2022-08-28 17:00:15 +08:00
Glavo 23cc568bac Support Microsoft login in dev version (#1651) 2022-08-28 16:59:03 +08:00
Glavo 7ff679c3c8 Revert AdvancedListBox (#1647)
fix #1258, fix #1397, fix #1559, fix #1589, fix #1646
2022-08-28 16:56:52 +08:00
Glavo c873352ee9 更多平台的原生适配 (#1654)
* Add Minecraft 1.13+ support for linux-loongarch64_ow

* Add Minecraft 1.6~1.12.2 support for linux-arm32

* Update documents
2022-08-28 16:56:33 +08:00
Glavo a4164a86af Support path containing unicode characters on Mac OS (#1661) 2022-08-28 16:54:22 +08:00
Glavo c21671e86b 修复通过自动安装更新游戏时的崩溃 (#1662)
* fix game upgrade

* fix #1459: Invalidate the version number cache before saving the version
close #1566
2022-08-28 16:52:26 +08:00
Glavo d9bdc2b1f7 fix #1579: Hide tokens in the exported Minecraft logs (#1665) 2022-08-28 16:51:49 +08:00
huanghongxun 19b6633f4a fix: checkstyle. 2022-08-21 21:29:09 +08:00
huanghongxun 2ad3e7a291 fix(account): Authlib-Injector server may be unknown when in CreateAcountPane tab mode. 2022-08-21 21:26:20 +08:00
huanghongxun 1fa63a528d fix(download): installer items overflow. Closes #1645. 2022-08-21 21:20:10 +08:00
Glavo b80c787b60 为非 x86 平台提供开箱即用的启动支持 (#1610)
* Replace native libraries on non-x86 platforms

* Add support for Linux ARM

* Relax the constraint 'JavaVersionConstraint.VANILLA_X86'

* Fix several hints

* Add OpenGL software renderer option

* Add support for OpenGL software renderer on windows

* Add __GLX_VENDOR_LIBRARY_NAME on Linux

* Catch GLFW error 65543

* Allow users to disable auto completion of native libraries

* fix NPE when violated mandatory constraint

* Add support for Minecraft 1.13

* Add support for Minecraft 1.6~1.12

* Add base support for linux-loongarch64

* Improve warnings on Loongson platform

* Add linux launcher script

* fix typo

* Add support for loongarch64 (old world)
2022-08-21 19:00:05 +08:00
Lonelyxiya c8652012c5 I18N_zh.properties (#1611)
修訂了一些台灣詞彙。來自https://discord.com/channels/815819580840607807/815860830058840064/1003644502478704671
RPMTW的翻譯討論頻道
2022-08-21 18:27:27 +08:00
Glavo e70b43be26 Temporarily rollback HMCLauncher.exe (#1625)
close #1583, close #1577
2022-08-21 18:26:54 +08:00
K1t3AB 2d182807b2 feat(README_cn.md): Added Chinese translation for README.md (#1637)
* feat(README_cn.md): Added Chinese translation for README.md

* edit(README.md): Improved formats for README.md

* fix(README): Roll back to Commit ID to pin lines

* edit(README): Let languages switchers "more like buttons"

* fix(README): Fix errors caused by misunderstanding

* edit(README): Added 8 spaces
2022-08-21 18:26:42 +08:00
huanghongxun 675af3a5aa feat(download): quilt installation. 2022-08-21 18:20:11 +08:00
huanghongxun 4ba4c3e749 feat(quilt): WIP: partially implements quilt installation. 2022-08-20 15:52:22 +08:00
huanghongxun 11e2277c9e feat(account): show little-skin login method by default. Closes #1622. 2022-08-20 14:58:38 +08:00
Yuhui Huang 66642fe621 fix(i18n): line ending of Spanish lang file. 2022-08-08 17:21:19 +08:00
Nekrodamus 3189a22f26 Update I18N_es.properties (#1614)
* Update I18N_es.properties

Updated the Spanish language file to the latest Beta version, using as template the I18N.properties file.

[Completed]

* Update I18N_es.properties

Little fix

* Update I18N_es.properties

More fixes.

* Update I18N_es.properties

Corrected the label for modpacks "Investigaciones" to "Quests".

* Update I18N_es.properties

More fixes

* Update I18N_es.properties

Final fixes

* Update I18N_es.properties

Fixed bug that caused a string not to be translated.

* Update I18N_es.properties

Little fix

* Update I18N_es.properties

Little fix

* Update I18N_es.properties

Little fixes

* Update I18N_es.properties

Little fix

* Update I18N_es.properties

Improved the context of some sentences

* Update I18N_es.properties

Reverted fix

* Update I18N_es.properties

Changes in capitalization

* Update I18N_es.properties

Some fixes...

* Update I18N_es.properties

More corrections...
2022-08-08 17:19:59 +08:00
Tungstend 366e359d83 修复litemod信息获取失败 (#1545)
* 修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能

* Revert "修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能"

This reverts commit 1d29c69d92.

* 修复CF源获取模组版本列表不全

* Modrinth源获取类别列表,搜索时增加类别选项

* Update ModrinthRemoteModRepository.java

* Update ModrinthRemoteModRepository.java

* 修复cf源获取前置模组失败

* Revert "Update ModrinthRemoteModRepository.java"

This reverts commit 40b7c1d59d.

* Revert "Revert "Update ModrinthRemoteModRepository.java""

This reverts commit 2027c9ef4c.

* Revert "修复cf源获取前置模组失败"

This reverts commit 4404daf0ea.

* fix conflict

* 修复cf源无法获取前置模组

* fix check style

* Update LiteModMetadata.java
2022-08-08 16:58:55 +08:00
ZekerZhayard f7d8a473aa Fix NoSuchMethodError when compiling with Java 9+ (#1585) 2022-08-08 16:58:14 +08:00
zkitefly 8276bf5d8c 在 README 中添加 Fabric (#1587)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* Update README.md
2022-08-08 16:57:47 +08:00
zkitefly 371150dfa8 添加提示 (#1597)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* text

* text

* now

* now

* 优化下简体,繁体,和英文

* 再优化了下

* fix

* fix

* text

* text

* now
2022-08-08 16:57:31 +08:00
Glavo 38d71fd6bb Add world info page (#1620) 2022-08-08 16:56:03 +08:00
king tsaltedfishandYuhui Huang c627ca96c9 Rewrite badges (#1612)
Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-08-08 16:52:58 +08:00
Rovniced 09167bb302 修复curseforge文件名存在中文下载失败 (#1578)
* Update RemoteMod.java

* Update RemoteMod.java

* Update RemoteMod.java
2022-08-08 16:52:02 +08:00
zkitefly a3c24b1b02 因 开黑啦 改名成 KOOK,所以改名 (#1600)
* 修改加入链接的前缀“kaihei.co”->“kook.top”

* 因开黑啦改名成KOOK,所以改名。邀请链接也做相应调整。

* Update bug-report.yml

* Update bug-report.yml

顺便无伤大雅的修改了下这里

* Update config.yml

修改了下这里的英文空格隔离

* Add files via upload

上传新logo

* Delete kaiheila.jpeg

删除旧logo

* Update I18N.properties

修改回原来样式

* Update I18N_zh.properties

修改開黑啦为KOOK
2022-08-08 16:51:38 +08:00
Glavo 9f4a3cf8a8 Disable Microsoft login when auth id is empty 2022-06-19 18:58:38 +08:00
xmdhs 702d27393e replace piston-data.mojang.com 2022-06-17 11:16:33 +08:00
xmdhs 698d3a311d replace piston-meta.mojang.com 2022-06-17 11:16:33 +08:00
Glavo c92f8e3b51 Complete optifine that is not installed automatically 2022-06-16 20:35:14 +08:00
Glavo 7c502ecd01 fix #1311: fix auto complete OptiFine 2022-06-16 20:35:14 +08:00
Glavo a237a31d23 Output the URL in the log when the file is not found 2022-06-16 20:35:14 +08:00
Glavo 5f4fc77258 Use language-independent links 2022-06-15 11:42:16 +08:00
ZekerZhayard 8bd4df68f5 Fix #1543 2022-06-14 12:49:56 +08:00
Glavo 29cff02f3c Update JavaVersionConstraint 2022-06-13 23:30:41 +08:00
Glavo d1bc41e1f1 Detect loongarch 2022-06-13 23:30:41 +08:00
huanghongxun 2fd1bf4c29 fix(i18n): incorrect translation change. 2022-06-05 14:11:04 +08:00
huanghongxun edd89fb040 fix: test. 2022-06-03 23:59:35 +08:00
ByacryaandYuhui Huang 08d6b58c8c feat(i18n): greatly improve en_US translation (#1522)
It seems that the current translation was largely done by Google Translation, so I did some work to improve it.

Some translations can definitely be improved, but I don't have much time at the moment so I will leave it for now.

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-06-03 23:27:27 +08:00
huanghongxun e3fc9f5116 feat: auto find java by default. Closes #1511. 2022-06-03 23:19:47 +08:00
huanghongxun 6dcd808cc8 fix(crash): suggest jdk corruption. Closes #1509. 2022-06-03 23:17:00 +08:00
huanghongxun df6230fc01 fix(install): date deserialization. Closes #1512. 2022-06-03 23:13:40 +08:00
huanghongxun 9f502ae6ae fix(modpack): missing .mrpack extension. Closes #1531. 2022-06-03 22:53:23 +08:00
huanghongxun 2a979d2b15 fix(download): incorrect mcmod url for modpacks. Closes #1525. 2022-06-03 22:47:03 +08:00
huanghongxun ae484e3b67 fix(download): inappropriate translation for "Vanilla+" 2022-06-03 22:47:02 +08:00
Byacrya 7206832edb feat(i18n): correct zh_CN translation
Vanilla+ should be translated to '原版增强' instead of '香草+'.
2022-06-03 21:08:45 +08:00
zkitefly 288b32ed79 I18N.properties (#1526)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_ru.properties

* Update I18N_ja.properties

* Update I18N_es.properties

* Update I18N.properties
2022-06-03 17:01:37 +08:00
zkitefly 53bc3b354f 修改微软账户上传皮肤链接 (#1521)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* Update AccountListItem.java
2022-06-02 20:53:39 +08:00
Tungstend 30565c2b50 修复cf源无法获取前置模组 (#1520)
* 修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能

* Revert "修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能"

This reverts commit 1d29c69d92.

* 修复CF源获取模组版本列表不全

* Modrinth源获取类别列表,搜索时增加类别选项

* Update ModrinthRemoteModRepository.java

* Update ModrinthRemoteModRepository.java

* 修复cf源获取前置模组失败

* Revert "Update ModrinthRemoteModRepository.java"

This reverts commit 40b7c1d59d.

* Revert "Revert "Update ModrinthRemoteModRepository.java""

This reverts commit 2027c9ef4c.

* Revert "修复cf源获取前置模组失败"

This reverts commit 4404daf0ea.

* fix conflict

* 修复cf源无法获取前置模组

* fix check style
2022-06-02 14:14:51 +08:00
zkitefly c400c7be9d Account migration link update (#1513)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* Update YggdrasilService.java
2022-06-02 13:04:12 +08:00
zkitefly 7acd9d2491 Update AboutPage.java (#1516)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java

* Update AboutPage.java
2022-06-02 13:01:30 +08:00
huanghongxun dc26dba1d0 fix(download): category may be null. 2022-05-24 01:17:29 +08:00
huanghongxun 66cbb23a38 fix(java): broken macos java location. 2022-05-24 01:09:10 +08:00
huanghongxun 663fc176d8 feat(java): find java in minecraft runtime directory with any component name. As a new component name "java-runtime-gamma" was added. 2022-05-24 01:02:16 +08:00
Mingye Wang b9ec22677e chore(javaversion): refactor searchPotentialJavaExecutables (#1485)
* chore(javaversion): refactor searchPotentialJavaExecutables

This is a very draft quality PR. I don't even have JDK installed to build it, that's how draft it is.

1. Refactor function to share common steps of the code: system locations, Minecraft launcher runtimes, PATH/HMCL_JRES
2. Modify Minecraft launcher runtimes to include java-runtime-beta

* Update HMCLCore/src/main/java/org/jackhuang/hmcl/util/platform/JavaVersion.java

* Make optional happy

* correct stream-of paren; add macOS bundle exploration

* who needs exploreBundle?

* fix a stupid typo; add debugging tools. It works!
2022-05-23 23:17:32 +08:00
Tungstend 7d55c7f37f Modrinth获取类别列表,搜索时增加类别筛选 (#1501)
* 修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能

* Revert "修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能"

This reverts commit 1d29c69d92.

* 修复CF源获取模组版本列表不全

* Modrinth源获取类别列表,搜索时增加类别选项

* Update ModrinthRemoteModRepository.java

* Update ModrinthRemoteModRepository.java
2022-05-23 23:03:33 +08:00
huanghongxun f12882a389 fix(download): should update category list after download soruce changed. 2022-05-23 22:27:30 +08:00
huanghongxun 8cfde9d5c6 feat(download): modrinth moddpack category translations. 2022-05-23 21:57:08 +08:00
huanghongxun 10180a74b6 fix: style. 2022-05-22 15:43:08 +08:00
huanghongxun abd30c397d fix(ui): support reveal file in finder, or just open parent directory in Linux. Closes #1458. 2022-05-22 15:40:47 +08:00
huanghongxun a230027974 fix(download): not showing download mod progress. Closes #1456. 2022-05-22 15:31:57 +08:00
Tungstend 59f6837914 修复cf源获取模组版本列表不全 (#1499)
* 修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能

* Revert "修复Modrinth源加载列表失败,增加Modrinth源获取前置模组功能"

This reverts commit 1d29c69d92.

* 修复CF源获取模组版本列表不全
2022-05-22 14:34:54 +08:00
huanghongxun 7546af9067 feat(HMCLauncher): update HMCLauncher.exe. Closes #1498. 2022-05-22 14:30:56 +08:00
huanghongxun 1aa3860706 fix(auto-installing): catch logs of forge-installer. Closes #1420. 2022-05-22 14:24:25 +08:00
huanghongxun fd098a65c5 fix(download): not translated some modrinth categories. Closes #1493. 2022-05-22 13:22:40 +08:00
huanghongxun 77f7a56df9 fix(modpack): should accept dragging .mrpack file into rootpage. Closes #1497. 2022-05-22 13:15:20 +08:00
huanghongxun 4fe879085f chore: update gitignore. 2022-05-22 01:45:09 +08:00
huanghongxun 96914ec4f6 feat(launch): open microsoft openjdk download link for Chinese users. 2022-05-22 01:38:30 +08:00
Mingye Wang a1c6b6439c Update HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java 2022-05-22 01:36:20 +08:00
Mingye Wang b91dba3141 fixup! use suggested OS detection 2022-05-22 01:36:20 +08:00
Mingye Wang 718c0a668a fixup! Uh, I think *that's* how you override it. I don't know. Squash this please I don't want to install git qwq 2022-05-22 01:36:20 +08:00
Mingye Wang c62963f455 fix(config): Use Consolas by default on Windows only
Currently non-Windows systems get a proportional font for logging. Bad.

Fix #139.
2022-05-22 01:36:20 +08:00
huanghongxun cc39a37beb feat(download): show modrinth dependencies. 2022-05-22 01:33:27 +08:00
huanghongxun 1bc8450110 fix(skin): littleskin domain changed. Closes #1484. 2022-05-22 01:10:13 +08:00
huanghongxun bec0ca6dea feat: add .editorconfig. Closes #1425. 2022-05-22 00:54:57 +08:00
huanghongxun 7c7296a57d fix(download): show Chinese name of mods in English locale. Closes #1461. 2022-05-22 00:49:26 +08:00
huanghongxun 9684ce4a1b feat(modpack): support installing Modrinth modpack. 2022-05-22 00:39:33 +08:00
huanghongxun c0df55ee62 fix(download): modrinth category forge. 2022-05-21 21:54:41 +08:00
huanghongxun b598e2613e feat(crash): emphasise words that requires user not takking screenshot of game crash window. 2022-05-21 19:02:18 +08:00
huanghongxun 99e578b1bc fix(download): switch to api.curseforge.com. 2022-05-21 19:00:51 +08:00
huanghongxun 3766491c28 feat(download): modpack search chinese words. 2022-05-21 18:58:41 +08:00
huanghongxun 71f13b24cb chore: update mod_data.txt 2022-05-21 18:58:41 +08:00
Mingye Wang 4a3d20576e fix(i18n): mod "nickname" -> filename 2022-05-10 18:04:27 +08:00
zkitefly ecbc243c6b Uptada I18N (#1442)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* updata curse.category.+~

* Update I18N_zh.properties

* updata curse.category.+~

* Update I18N_zh_CN.properties

* 删除。

* 0

* 0

* Update I18N_zh_CN.properties

* 按照 @huanghongxun 的要求“不要去掉句号”

* Update I18N.properties

* Update launch.advice.vanilla_linux_java_8

* 修改错误

* Update I18N_zh_CN.properties
2022-05-08 18:58:46 +08:00
zkitefly c012ca1183 Update MultiplayerPageSkin.java (#1477)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java

* Update MultiplayerPageSkin.java
2022-05-08 00:06:44 +08:00
AustinC2 820acd90e7 Update README (#1466)
* Update README

* Update README

* Update README

* Update README

* Update README

* Delete ⛏_Hello_Minecraft!_Launcher_💎.png
2022-05-08 00:06:21 +08:00
zkitefly e904a5b42d 修改链接 s.mcer.cn 至 mcer.cn (#1452)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Update AboutPage.java

* Update I18N.properties

* Update I18N.properties

* Update I18N_ja.properties

* Update I18N_ja.properties

* Update I18N_ja.properties

* Update I18N_ru.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update CreateMultiplayerRoomDialog.java

* Update CreateMultiplayerRoomDialog.java
2022-05-07 18:52:17 +08:00
Haowei Wen cb051c033f Add openjfx to testImplementation dependencies 2022-05-07 18:51:52 +08:00
zkiteflyandYuhui Huang 565fc1f7b6 添加 Glavo 鸣谢 (#1450)
* Create  获得加入Cato社区网络身份凭证的方法.txt

* Delete 获得加入Cato社区网络身份凭证的方法.txt

* Add files via upload

* Update AboutPage.java

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N.properties

* Update AboutPage.java

* Update I18N_ja.properties

* Update I18N_ru.properties

* Update AboutPage.java

* Update I18N.properties

* Update I18N_ja.properties

* Update I18N_ru.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

Co-authored-by: Yuhui Huang <i@huangyuhui.net>
2022-04-09 16:39:24 +08:00
ShellWen | 颉文 f6b8fae903 Update I18N_zh_CN.properties
修改翻译
2022-04-09 16:36:43 +08:00
ShellWen | 颉文 5819af5e7c Update I18N_zh_CN.properties
修复语法问题
2022-04-09 16:36:43 +08:00
vanja-san f4057fb8f8 Update I18N_ru.properties 2022-04-09 16:36:12 +08:00
vanja-san 783a36be84 Update I18N_ru.properties 2022-04-09 16:36:12 +08:00
vanja-san 091d6e00b1 Update I18N_ru.properties 2022-04-09 16:36:12 +08:00
zkitefly d721d00139 Update Metadata.java 2022-04-09 16:34:29 +08:00
zkitefly f7797d4b6f Delete 获得加入Cato社区网络身份凭证的方法.txt 2022-04-09 16:34:29 +08:00
zkitefly f810373227 Create 获得加入Cato社区网络身份凭证的方法.txt 2022-04-09 16:34:29 +08:00
ZekerZhayard c808225f78 Fix #1405 2022-03-20 21:02:19 +08:00
zkitefly 0b06bf82d8 Update FeedbackPage.java 2022-03-20 16:39:47 +08:00
zkitefly 6969107c68 Update I18N_zh_CN.properties 2022-03-20 16:39:47 +08:00
zkitefly 6347de62f0 Update I18N_zh.properties 2022-03-20 16:39:47 +08:00
zkitefly 1f09c122de Update I18N_ru.properties 2022-03-20 16:39:47 +08:00
zkitefly f3de209f99 Update I18N_ja.properties 2022-03-20 16:39:47 +08:00
zkitefly d29c833866 Update I18N.properties 2022-03-20 16:39:47 +08:00
Glavo edac522d06 fix #1411: Set file executable permission after download Java 2022-03-20 16:36:24 +08:00
Glavo 3b1a890423 Filter exported startup parameters 2022-03-20 16:34:42 +08:00
Glavo 4d4864819a Register forbiddenTokens in MicrosoftSession and YggdrasilSession 2022-03-20 16:34:42 +08:00
Glavo 151e7e04eb Add a filter policy to logs 2022-03-20 16:34:42 +08:00
Glavo 03c634658b fix I18N_zh_CN.properties 2022-03-15 20:49:18 +08:00
Glavo 10c54509aa Prohibit login with Mojang account 2022-03-15 20:49:18 +08:00
zkitefly 8c74a559c3 Update I18N_zh_CN.properties 2022-03-15 20:48:47 +08:00
zkitefly d3ae8045c0 Update I18N_zh.properties 2022-03-15 20:48:47 +08:00
zkitefly b9753beeac Update I18N.properties 2022-03-15 20:48:47 +08:00
zkitefly 6a602adc4d Update CreateAccountPane.java 2022-03-15 20:48:47 +08:00
zkitefly c6835ac4f9 Update I18N_zh_CN.properties 2022-03-15 20:48:47 +08:00
zkitefly 7af4332d04 Update I18N_zh.properties 2022-03-15 20:48:47 +08:00
zkitefly af5b5e0f67 Update I18N_zh_CN.properties 2022-03-15 20:48:47 +08:00
zkitefly e6368f5355 Update I18N_zh.properties 2022-03-15 20:48:47 +08:00
zkitefly 1fe9826871 Update CreateAccountPane.java 2022-03-15 20:48:47 +08:00
zkitefly 01ea047521 Update I18N_zh_CN.properties 2022-03-15 20:48:47 +08:00
zkitefly f24a73412a Update CreateAccountPane.java 2022-03-15 20:48:47 +08:00
zkitefly 4def209c62 Update I18N_zh.properties 2022-03-15 20:48:47 +08:00
zkitefly a4a7a1e507 Add files via upload 2022-03-15 20:48:47 +08:00
zkitefly 560725431d Add files via upload 2022-03-15 20:48:47 +08:00
Glavo ffde4987e9 delete css on exit 2022-03-02 16:14:17 +08:00
Glavo 3334371ae3 Show upload skin button for microsoft account
close #1328
2022-03-02 16:13:57 +08:00
Glavo 8b1808fa2e fix #1385: crash when show DatapackListPage
close #1373
2022-03-02 16:13:28 +08:00
CNCloud_Casey 7d86d31844 多人联机界面Cato版本标识优化 (#1353)
* 对Cato版本号问题做出了修改

* test to come back

* 更改了一个因Cato现在实时更新而不合时间的问题

* 学着编译了一下

在gradlew.bat末尾加了一个pause以检查编译结果

* 把pause改掉了
2022-03-01 23:32:13 +08:00
zkiteflyandYuhui Huang 0cbfa62731 Update language (#1388)
* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

修改微软登录的提示
修改noin网站的前缀为https://s.mcer.cn,内容不变,因为网站地址已变更

* Update I18N_zh_CN.properties

添加微软账户添加提示
修改noin.cn为s.mcer.cn,因为链接已变更,但内容不变

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N_zh.properties

将https://s.mcer.cn/agreement改成https://hmcl.huangyuhui.net/api/redirect/multiplayer-agreement,可以在链接修改时上游同步即可

* Update I18N_zh_CN.properties

* Update I18N_zh_CN.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N_ru.properties

* Update I18N_ja.properties

* Update I18N_zh_CN.properties

* Update AboutPage.java

* Accidentally edit license header.

Co-authored-by: Yuhui Huang <jackhuang1998@gmail.com>
2022-03-01 23:31:38 +08:00
Glavo db735b58ec No longer download from tuna mirror 2022-03-01 23:27:51 +08:00
Glavo 17fe11e8f5 Download Zulu by default 2022-03-01 23:27:51 +08:00
Glavo fc65d8a535 Fix the problem that SSL connection cannot be established on Windows 7 2022-03-01 23:27:51 +08:00
Glavo 6ade056bf8 Enable Visual Styles 2022-03-01 23:27:51 +08:00
Glavo a5ebf08a8c Reduce refresh UI controls 2022-03-01 23:27:51 +08:00
Glavo ca2eccc762 Fix compatibility issues with Windows XP 2022-03-01 23:27:51 +08:00
Glavo efd76a1407 Download JRE instead of JDK; Add support for changing download sources 2022-03-01 23:27:51 +08:00
Glavo 48785e93f0 Import error messages 2022-03-01 23:27:51 +08:00
Glavo 20ddf60c48 Make HMCLLauncher find Java in more places 2022-03-01 23:27:51 +08:00
Glavo 1b9b0226f2 Automatically download Java when Java is not found 2022-03-01 23:27:51 +08:00
Glavo a1f8ff7463 Extract and execute the script 'java-download.ps1' 2022-03-01 23:27:51 +08:00
Glavo aafe7f44e7 Replace 'PathAppend' with 'MyPathAppend' 2022-03-01 23:27:51 +08:00
Glavo 8909224a13 Add 'java-download.ps' as win32 resource 2022-03-01 23:27:51 +08:00
Glavo f5d01bef93 git: No longer track file 'HMCL.APS' 2022-03-01 23:27:51 +08:00
Glavo f7de97c844 Fix download dialog title 2022-03-01 23:27:51 +08:00
Glavo 2136fddec5 Update java download window style 2022-03-01 23:27:51 +08:00
Glavo 312152de78 Download and install Java using scripts 2022-03-01 23:27:51 +08:00
Glavo 4fe7d10872 Find Java in HMCL directory 2022-03-01 23:27:51 +08:00
Glavo 9ec10dc5dc Extract 'FindJavaInDirAndLaunchJVM' 2022-03-01 23:27:51 +08:00
Glavo 2672459287 Check Windows version in HMCLLauncher 2022-03-01 23:27:51 +08:00
Glavo 8a8918c049 Try starting with JRE in 'jre-x86' on x64 2022-03-01 23:27:51 +08:00
Glavo 98e0a92745 Get system arch using 'GetNativeSystemInfo' 2022-03-01 23:27:51 +08:00
Glavo 2b21f5f48f Replace 'java.com' with 'www.microsoft.com/openjdk' 2022-03-01 23:27:51 +08:00
ZekerZhayard 69ddc939b3 Fix #645, #1381 2022-03-01 23:16:34 +08:00
Glavo 17ad6362c3 Use Commons Compress to readModpackManifest 2022-03-01 22:48:11 +08:00
Glavo 0f22c9e31a fix #1368: Guess Zip file encoding using Commons Compress 2022-03-01 22:48:11 +08:00
Lonelyxiya 07f9b30ed5 修订繁体中文里面的用语 整合包=模組包
修订繁体中文用语
2022-03-01 22:37:25 +08:00
Glavo 7756becb35 Add an eye-catching version isolation prompt 2022-02-09 15:08:54 +08:00
Gamom cf2ac010bb Update bug-report.yml
修复About过长(???)
2022-02-03 22:15:18 +08:00
zkitefly 76c8e98c02 Update I18N_zh.properties 2022-02-03 22:14:36 +08:00
zkitefly fe66b44b10 Update I18N_zh.properties 2022-02-03 22:14:36 +08:00
zkitefly 97695e4b5a Update I18N_zh_CN.properties 2022-02-03 22:14:36 +08:00
Gamom 59653f4030 Update bug-report.yml
添加英文
方便部分玩家反馈。
2022-02-03 18:18:32 +08:00
Glavo 530b632c46 Fix the problem that the suggestion does not prevent starting the game 2022-02-03 18:17:27 +08:00
Glavo c8f1a10b8b Analyze the crash caused by incompatibility between modlauncher and Java 2022-02-03 18:17:27 +08:00
Glavo 84570c5024 fix import curse forge modpack without modlist.html 2022-02-03 18:16:48 +08:00
Glavo 624d21c4dc Show launcher version on game crash window 2022-02-03 18:16:08 +08:00
Glavo 8db54d015a When creating symbolic links, delete the existing ones first
fix #1342
2022-02-03 18:15:41 +08:00
kitefly 9204c720a4 Update I18N_zh.properties 2022-02-03 18:15:19 +08:00
kitefly daf81b5d78 Update MultiplayerManager.java
更新一下版本号
2022-01-25 22:01:49 +08:00
Glavo c2c3f55925 Replace 'Launcher.getCurrentJarFiles()' with 'JarUtils.thisJar()' 2022-01-25 22:01:39 +08:00
kitefly ba520f1d90 Update I18N_zh_CN.properties 2022-01-25 22:00:58 +08:00
Glavo 81d1dbdcf0 fix the 'run' task of Gradle 2022-01-25 22:00:05 +08:00
kitefly 23cb2655d1 Update I18N_zh_CN.properties 2022-01-24 16:47:06 +08:00
huanghongxun 9c4f94238d feat: support hyperlinks in message dialog. 2022-01-24 16:29:33 +08:00
huanghongxun d28723916d fix(multiplayer): incorrect cato arguments. 2022-01-24 01:45:23 +08:00
Glavo 65d4c9c8ee Fix build on JDK without JavaFX 2022-01-23 19:47:26 +08:00
Glavo 3a1f7a13a1 Add a new GameJavaVersion 'java-runtime-beta' 2022-01-23 19:47:07 +08:00
Glavo 69d97a508c Add JavaVersionConstraint rules 2022-01-23 19:47:07 +08:00
Glavo e7dc23ae26 fix: Cato exit code is not displayed normally 2022-01-23 19:36:31 +08:00
huanghongxun 4cffe30910 fix: test build 2022-01-23 15:06:22 +08:00
NekoCaffeine 6f0dfba4aa fix updateSkin 2022-01-23 10:01:54 +08:00
NekoCaffeine 6d617216c7 fix #1130 2022-01-23 10:01:54 +08:00
kitefly 13d5e5e6af Update MultiplayerManager.java
更新cato版本;将String[] commands = new String[]{exe.toString(), "-auth.token", StringUtils.isBlank(token) ? "new" : token};改成String[] commands = new String[]{exe.toString(), "-auth.token", token};原因是新版cato已经取消这个now传值了
2022-01-23 09:59:19 +08:00
kitefly c697413554 Update I18N_zh_CN.properties 2022-01-23 09:55:42 +08:00
kitefly 994ae3e2f5 Update I18N_zh_CN.properties 2022-01-23 09:55:42 +08:00
kitefly 69ab292394 Update I18N_zh_CN.properties 2022-01-23 09:55:42 +08:00
kitefly 1985d31a07 Update I18N_zh_CN.properties
根据@huanghongxun 的说法修改
2022-01-23 09:55:42 +08:00
kitefly b813185dc3 Update I18N_zh_CN.properties 2022-01-23 09:55:42 +08:00
kitefly 6daf04c9fc Update I18N_zh_CN.properties
决定删除:<b>登录后请勿将同HMCL目录下的hmcl.json文件开放给他人,否则可能存在文件里的token(不是明文密码)会泄露账号(仅限微软账号)</b>
并添加:若发现账号头像是默认头像、进入游戏看到是默认皮肤且无法进入开启正版认证的服务器,则可能需要重新登录微软账号
2022-01-23 09:55:42 +08:00
kitefly fd49ec4f97 Update I18N_zh_CN.properties
添加
<b>你需要在新打开的网页窗口中按照提示完成登录</b> 设备代码为:<b>%1$s</b>,已拷贝至剪切板中,若无法粘贴,可点击此蓝方框再次拷贝。若网页未能打开,请手动在网页浏览器中打开:%2$s <b>登录后请勿将同HMCL目录下的hmcl.json文件开放给他人,否则可能存在文件里的token(不是明文密码)会泄露账号(仅限微软账号)、

讨论:是否添加?:登录后请勿将同HMCL目录下的hmcl.json文件开放给他人,否则可能存在文件里的token(不是明文密码)会泄露账号(仅限微软账号)
2022-01-23 09:55:42 +08:00
kitefly 84a4921629 Update I18N_zh_CN.properties 2022-01-23 09:55:42 +08:00
Glavo b4aeaff783 fix preTouchOpenJFXDependencies 2022-01-23 09:55:03 +08:00
Glavo caf73b89ea Fix #1196: Use OpenJFX 17.0.2 2022-01-23 09:55:03 +08:00
Glavo 1c406557a4 Allow different platforms to use different versions of OpenJFX 2022-01-23 09:55:03 +08:00
Glavo d39eac9512 Replace static fields with instance fields 2022-01-23 09:55:03 +08:00
Glavo 2530d31d58 Clean up build scripts 2022-01-23 09:55:03 +08:00
Glavo c6894a1e95 Modify the path where openjfx is placed 2022-01-23 09:55:03 +08:00
Glavo 05fdea3267 Improve the format of openjfx-dependencies.json 2022-01-23 09:55:03 +08:00
Glavo e0f7d9cd32 Generate 'openjfx-dependencies.json' at build time 2022-01-23 09:55:03 +08:00
asdqp233 91b9fe23cc Update Japanese translation
Update and fix Japanese translation
2022-01-23 09:47:35 +08:00
kitefly 4e4e2907d9 Update I18N_zh_CN.properties
添加一些小提示
multiplayer.exit.timeout.static_token=无法连接多人联机服务,你的凭证可能无法正常工作,你可以使用空凭证再试。\n你可以联系在线管理员询问鉴权服务器是否正常工作,或者耐心等待十分钟。
multiplayer.exit.timeout.dynamic_token=无法连接多人联机服务,你可以在多人联机页面的反馈中反馈问题。\n可能原因:鉴权服务器被人占满,请等待每个半点,鉴权服务器会自动刷新一次。\n没有耐心且有闲钱的可以考虑购买联机凭证,不受鉴权服务器影响。
2022-01-23 09:44:44 +08:00
Glavo b94f54f56d Delete unused submodule 'JSTUN' 2022-01-15 01:48:30 +08:00
Glavo 12932a1d33 Adjust the policy of automatic memory allocation 2022-01-15 01:44:41 +08:00
Glavo 7beef30d20 Set an upper limit of 8G for the suggested memory 2022-01-15 01:44:41 +08:00
Glavo f1e4e3c753 Prevent HMCL from crashing when exiting 2022-01-15 01:43:28 +08:00
Glavo d1ba758c6f fix #1297: export logs without 'Schedulers.io()' 2022-01-15 01:43:11 +08:00
Glavo c0472056ac fix #1287, fix #1284: sort type is null 2022-01-10 19:08:55 +08:00
kitefly 1780c479d5 Update I18N_zh_CN.properties 2022-01-07 21:38:24 +08:00
kitefly 59f8465ece 修复多人联机界面过大的问题
通过修改I18N_zh_CN.properties中的
2022-01-03 14:24:53 +08:00
Glavo 256f72dd08 Fix 'DefaultGameRepository::getVersionJar' 2022-01-02 14:28:32 +08:00
huanghongxun 836c7769f6 fix(skin): when texture file is lost, continue launching game instead of failing. 2022-01-02 01:40:37 +08:00
huanghongxun ebe880bd39 fix(multiplayer): ConcurrentModificationException. Closes #1278. 2022-01-02 01:19:14 +08:00
huanghongxun 10fea79a35 fix: cannot update Forge for Minecraft 1.18 when "patches" lost in game version json. Closes #1262. 2022-01-02 01:08:22 +08:00
huanghongxun 451600448e fix: do not show install failure if it is cancelled by user. 2022-01-02 01:08:22 +08:00
huanghongxun 7fccd6261e fix: not on FX thread. 2022-01-02 01:08:21 +08:00
huanghongxun 2436dab797 fix: load fabric api version list by incorrect game version. Closes #1273. 2022-01-02 01:08:21 +08:00
huanghongxun 56538623a5 fix: sort not work in modrinth. Closes #1274. 2022-01-02 01:08:20 +08:00
YuanYuanOwO 42a667c0ef 修正句子 2022-01-02 00:10:58 +08:00
Glavo 5df95ad0fa Use Gradle Kotlin DSL 2022-01-01 20:59:36 +08:00
Glavo 345598016a Use more hash algorithms to generate checksum files 2021-12-30 23:58:57 +08:00
kitefly 6da38e537b 修改错字
将"室"改成"是"
2021-12-25 14:16:21 +08:00
Glavo e65f664821 Check for illegal characters of the HMCL directory path 2021-12-24 23:02:53 +08:00
Glavo cfa1eedae7 Extract the log4j patch to the HMCL directory 2021-12-24 23:02:53 +08:00
Glavo 130c8d0e89 Add 'javaAgents' to LaunchOptions 2021-12-24 23:02:53 +08:00
Hcat c4a2695b69 Update MultiplayerPage.java
Replace the stun server to solve the problem of slow and inaccurate detection
2021-12-24 17:34:31 +08:00
Glavo 246069cc48 Use ArrayList instead of LinkedList 2021-12-21 22:36:42 +08:00
Glavo 36a7d4a510 Stop syncing to gitee on fork 2021-12-21 22:31:21 +08:00
Glavo 1b4a53f572 Update version string to 3.5 2021-12-21 22:26:02 +08:00
huanghongxun 61a9d80e2c fix: checkstyle. 2021-12-20 01:46:25 +08:00
huanghongxun 31568a5806 fix(download): we can check hash of fabric-api jar. 2021-12-20 01:43:41 +08:00
huanghongxun 9aa73f5a5c fix(download): download fabric-api from curseforge. 2021-12-20 01:22:54 +08:00
Glavo 35943feb08 Use GitHub action sync to gitee 2021-12-20 01:21:00 +08:00
huanghongxun 73de21f936 feat(game): select last renamed game version. Closes #1185. 2021-12-19 20:35:15 +08:00
huanghongxun 4f1af87ba0 fix(download): 1.18 released. 2021-12-19 20:31:05 +08:00
huanghongxun d129bd3e6b fix(multiplayer): allow updating cato at server. 2021-12-19 20:30:03 +08:00
Glavo c2c52e8747 fix: Bootstrap launcher version does not need to be displayed on GameCrashWindow 2021-12-19 19:55:00 +08:00
huanghongxun 7e2ab49f89 fix(about): missing JSTUN. Mentioned in #1203. 2021-12-19 19:50:02 +08:00
huanghongxun 3192d41fe3 fix(download): cannot refresh mod download list. Mentioned in #1203. 2021-12-19 19:47:55 +08:00
huanghongxun ad5b260460 fix(download): cannot refresh game list. Mentioned in #1203. 2021-12-19 19:45:01 +08:00
huanghongxun e57cb0ce51 fix(launch): should not patch offline account skin when generating launch script or stopping the launcher immediately after game launched. 2021-12-19 18:45:26 +08:00
Glavo 9514357391 Temporarily disabled adjust process priority on Windows 2021-12-19 17:59:13 +08:00
Glavo 6fd0a40cc6 Delete redundant exception catches 2021-12-19 17:17:40 +08:00
Glavo 350d63231f Improved the logging message of log4j-patch 2021-12-19 17:17:40 +08:00
Glavo cc28f1d5b5 Fix #1241: Use javaagent patch log4j 2021-12-19 17:17:40 +08:00
Glavo 4b73af9250 update check-update.yml 2021-12-19 01:52:48 +08:00
zhixuan 6e0efe521d Change the wrong translation for ja 2021-12-19 01:18:24 +08:00
huanghongxun 041ab30778 fix(multiplayer): incorrect cato option name. 2021-12-18 19:54:51 +08:00
huanghongxun 678be4b92f feat(multiplayer): upgrade cato to 1.2.0 2021-12-18 17:15:18 +08:00
Glavo 74d759a45b Update I18N text 2021-12-18 17:08:16 +08:00
huanghongxun 295499c17b fix: missing translations. 2021-12-18 16:55:33 +08:00
Glavo 043d1c88c8 Fix #1096: Feedback item cannot be clicked 2021-12-18 16:49:42 +08:00
bling-yshs 55e9b93b1c fix curse category 2021-12-18 16:49:23 +08:00
Glavo aa5e3e9697 Fix #1148: Specify console encoding explicitly 2021-12-18 15:59:24 +08:00
Glavo fbf7ff6ebd Lifting GB2312 to GB18030 2021-12-18 15:59:24 +08:00
Glavo 3a5e8a827c update README 2021-12-18 15:59:24 +08:00
Glavo 6176dfaaf7 Lifting encoding to be compatible with more Unicode characters 2021-12-18 15:59:24 +08:00
Glavo 76c361e9bc Fix #1201: Download version jar if the jar is empty 2021-12-18 15:58:33 +08:00
Glavo ccb8981634 Remove invalid Appender 2021-12-18 15:47:41 +08:00
Glavo 99b55d03c1 Allows user to enable message lookup 2021-12-18 15:47:41 +08:00
Glavo 651fa6e2d1 fix #663: Incorrect judgment of WebView loading status 2021-12-18 15:46:24 +08:00
Glavo 4b258da0a0 Fix code format 2021-12-18 15:45:35 +08:00
Glavo 9a66605616 Disable JndiLookup using log4j-patch 2021-12-18 15:45:35 +08:00
Glavo da13895b72 fix: InputStream not closed 2021-12-18 15:45:35 +08:00
Glavo c657a1d2cc Introducing log4j-patch 2021-12-18 15:45:35 +08:00
Glavo eac8896f3e fix NPE in MultiplayerManager 2021-12-18 15:39:57 +08:00
Gamom 113f4f948b Update I18N_zh_CN.properties
翻译问题
解决 https://github.com/huanghongxun/HMCL/issues/1203#issuecomment-997155654
2021-12-18 15:39:26 +08:00
huanghongxun 1b8dc1f75e fix(mcbbs): NPE when fileApi is not empty. Closes #1197. 2021-12-18 01:40:25 +08:00
huanghongxun 001236e8e6 feat(download): refresh button 2021-12-18 01:40:25 +08:00
Glavo 8025b8e24b Support offline installation of OptiFine 1.17.1 H1 and later 2021-12-18 01:26:03 +08:00
Glavo e48bf012a6 Safely save config 2021-12-18 01:21:45 +08:00
Glavo 94e32694b2 fix: Game should use global settings by default 2021-12-12 16:51:01 +08:00
Glavo cbcf033b15 Delete unnecessary properties 2021-12-11 23:52:03 +08:00
Glavo 7da2671ec5 Use custom log4j2.xml 2021-12-11 23:52:03 +08:00
Glavo b6ea4c7003 Fix the problem that you cannot continue after canceling the prompt 2021-12-11 23:52:03 +08:00
Glavo 49378061bf Always issue security warning 2021-12-11 23:52:03 +08:00
Glavo 38013a9298 Issue security warning for CVE-2021-44228 2021-12-11 23:52:03 +08:00
Glavo 5ffbd16800 Prohibit JNDI remote invoke 2021-12-11 23:52:03 +08:00
Glavo bcb29ef792 Update GitHub workflow configuration 2021-12-11 23:50:37 +08:00
Glavo d20adb20d3 fix: Logs should be encoded in UTF-8 2021-12-11 23:49:29 +08:00
Kylepoops 4f28cdd58f Fix CVE-2021-44228 again
https://github.com/apache/logging-log4j2/pull/614
2021-12-11 23:49:09 +08:00
huanghongxun 4b6b5c2f0e fix(game): fix remote code execution from log4j. 2021-12-10 14:22:16 +08:00
Glavo c31269a358 Disable message pattern lookup by default 2021-12-10 14:19:32 +08:00
Glavo 55092326a4 Use StringWriter instead of ByteArrayOutputStream 2021-12-10 14:18:08 +08:00
Glavo 10dbcf63b7 Adjust the default capability to reduce memory allocation 2021-12-10 14:18:08 +08:00
Glavo 32fad77104 Optimize logging performance 2021-12-10 14:18:08 +08:00
SettingDust 05d2fbbbb9 fix(core): zipper lose the file attributes 2021-12-10 14:17:03 +08:00
ttsdzb 916733a1e8 根据最近的变化对README下载部分稍作修改 2021-12-10 14:16:50 +08:00
huanghongxun 83b4b58cb2 feat(crash): 1.18 requires Java 17. Closes #1186. 2021-11-22 01:54:05 +08:00
huanghongxun 11f53c0e03 feat(crash): show link for jdk downloading. Closes #1186. 2021-11-22 01:15:15 +08:00
huanghongxun 2fa8aee055 fix(multiplayer): incorrect cato checksum. 2021-11-20 18:42:25 +08:00
huanghongxun d2848e12f3 fix: not reloading VersionPage when renaming the game version. Closes #1176. 2021-11-20 18:24:00 +08:00
huanghongxun 36ba840fe8 fix: should close version settings page when deleting related version. Closes #1175. 2021-11-20 18:19:39 +08:00
asdqp233 819d41a75c Update I18N.properties 2021-11-20 01:57:39 +08:00
huanghongxun 3a6c3dc51d fix(multiplayer): update cato. 2021-11-11 22:00:46 +08:00
huanghongxun db721107ae fix(microsoft): remove redundant login steps. 2021-11-09 00:35:52 +08:00
huanghongxun 5b9be471e3 fix(microsoft): should not pass client secret when refreshing token. Closes #1164. 2021-11-06 22:32:08 +08:00
huanghongxun e8f8617412 fix: crash when all profiles removed. Close #1165. 2021-11-06 22:32:07 +08:00
Glavo eb472037db fix: checkstyle 2021-11-03 17:30:27 +08:00
Glavo 81b7afd08d Update issue template 2021-11-02 22:46:41 +08:00
Glavo 7a71ecf177 Fix #1153: On Windows, the 'classpath' environment variable in the context overrides the 'CLASSPATH' 2021-11-02 22:46:13 +08:00
huanghongxun a12e485a27 chore(multiplayer): adjust some translations. 2021-10-31 18:42:27 +08:00
huanghongxun af521b2893 chore: localize SSLHandshakeException. 2021-10-31 18:42:27 +08:00
Glavo aab6f68492 Fix #1147: shadowJar minimize exclude JFoenix 2021-10-31 17:04:11 +08:00
huanghongxun f67c3d5284 fix(launch): cannot launch game in path that contains CJK characters. Closes #1141. 2021-10-30 23:03:49 +08:00
Glavo 46385a8445 Do not delete soft links when exiting HMCL 2021-10-30 23:01:27 +08:00
Glavo 9f617c33f5 Support path containing unicode characters on Linux 2021-10-30 23:01:27 +08:00
Glavo 6aef034880 Avoid generating command lines repeatedly 2021-10-30 23:01:27 +08:00
huanghongxun c4941a01d6 fix(multiplayer): show "failed to test" instead of "testing". 2021-10-30 22:40:44 +08:00
huanghongxun 7380423eee fix(multiplayer): does not show correct kick message. 2021-10-30 22:13:32 +08:00
huanghongxun f35073ffb5 fix(ui): alignment. 2021-10-30 21:05:40 +08:00
huanghongxun f89ea56d86 fix(multiplayer): session name null. 2021-10-30 21:04:53 +08:00
huanghongxun a240b8ca9f fix(skin): Do not load authlib-injector when skin manually set to steve/alex. 2021-10-29 22:52:16 +08:00
huanghongxun 230ec2bc8e feat(update): show update dialog when found update. 2021-10-29 02:51:08 +08:00
huanghongxun a3c2642b93 feat(download): customize name of the mod to be downloaded. 2021-10-29 01:21:48 +08:00
huanghongxun de755d5495 feat(game): more version icon choices. 2021-10-29 01:21:48 +08:00
huanghongxun 08054f3026 feat(countly): initial 2021-10-27 00:21:42 +08:00
Glavo 026239903d Fix IllegalAccessError: tried to access protected method 2021-10-25 17:23:07 +08:00
Glavo a320fe4cc4 Use shadowJar minimize 2021-10-24 17:26:26 +08:00
vanja-san b122d4c5e4 Update I18N_ru.properties
Full text translation
2021-10-24 17:25:40 +08:00
huanghongxun ec37b32eaa feat(microsoft): copy device code instead of verification uri. 2021-10-23 23:28:56 +08:00
huanghongxun f893eeea1f fix: not closing launching progress pane when java downloading was cancelled. Closes #1134. 2021-10-23 23:28:55 +08:00
huanghongxun 8d559dba02 feat(multiplayer): verify checksum. Closes #1122. 2021-10-23 23:28:55 +08:00
Glavo d167c712a6 Improved Java path display when no suitable Java is found 2021-10-23 22:06:07 +08:00
Glavo 8b224cb240 Avoid throwing NPE when the Java version cannot be obtained 2021-10-23 22:06:07 +08:00
huanghongxun 651aedaa50 feat(microsoft): use device code to login. 2021-10-23 02:22:04 +08:00
huanghongxun a36b0ebed2 feat: WIP: use commons-compress 2021-10-23 01:47:24 +08:00
huanghongxun 5a9e8683e4 feat(microsoft): WIP: use device code grant flow instead. 2021-10-23 00:01:55 +08:00
huanghongxun 843b29eff0 feat(multiplayer): update cato to 1.1.1 2021-10-22 22:03:08 +08:00
huanghongxun 7bad0be2c4 fix: incorrect system architecture detection in macOS. 2021-10-22 22:02:28 +08:00
huanghongxun 5561011122 fix: cannot choose custom java path. 2021-10-22 22:02:27 +08:00
huanghongxun 192a0c987e fix: unable to rename game versions that do not have jar file in directory. 2021-10-22 22:02:27 +08:00
huanghongxun 85a9a21e57 fix: unable to duplicate game version. 2021-10-22 22:02:27 +08:00
huanghongxun 610864d674 feat(launch): compatible with TLauncher. 2021-10-22 22:02:27 +08:00
Glavo 06d0fa6e22 Pass Launcher.class to Application.launch 2021-10-22 14:16:25 +08:00
Glavo fb9a6fd314 Lazy initialization CrashReporter 2021-10-22 14:16:25 +08:00
Glavo 5cab8ad41e Lazy initialization JFX_DEPENDENCIES 2021-10-22 14:16:25 +08:00
Glavo d9f2b964c9 Close letsEncryptFile (#1127) 2021-10-21 23:59:01 +08:00
huanghongxun ac78b69ec2 fix: Update Lets Encrypt root certificate. 2021-10-21 22:43:03 +08:00
Glavo 516975688f 在发现文件系统实现出错时提示用户 (#1040)
* fix: The implementation of 'Files.isWritable' is incorrect

* Report Samba problems to users

* Rollback FileUtils
2021-10-21 22:05:46 +08:00
Glavo 7f07439388 Limit check updates 2021-10-21 21:37:33 +08:00
Glavo bf87c4ac52 Skip generate release note when not necessary 2021-10-21 21:37:33 +08:00
Glavo 272132af51 GitHub Action: Check Update 2021-10-21 21:37:33 +08:00
zhixuan 04dd179631 Update I18N_ja.properties 2021-10-21 21:37:16 +08:00
huanghongxun 57554c8d4b fix(ui): validate background image url. Mentioned in #1116. 2021-10-20 15:49:16 +08:00
huanghongxun 74a20bf11d feat(crash): find module name in stacktrace if available. 2021-10-20 02:17:58 +08:00
huanghongxun 16c96419d3 feat(crash): TwilightForest may be incompatible with OptiFine on Minecraft 1.16.5. 2021-10-20 02:09:50 +08:00
huanghongxun 0b444bc5a2 feat(ui): support translucent background.
Fix no background when network image url is wrong. Closes #1116.
2021-10-20 01:24:12 +08:00
huanghongxun 7846e9e196 fix: checkstyle. 2021-10-20 01:15:22 +08:00
huanghongxun a74b971b98 fix(auto-installing): verify fabric api file. 2021-10-20 01:15:21 +08:00
huanghongxun 6707ce1ed9 feat(crash): read crash report from raw log as fallback. 2021-10-20 01:15:21 +08:00
huanghongxun de421f93b5 feat(multiplayer): much simpler invitation code format. 2021-10-20 01:15:20 +08:00
huanghongxun 1762e78cb3 fix(java): java7 suggestion warning. 2021-10-19 23:17:27 +08:00
huanghongxun 2631f3a007 fix(java): test forge installation instead of main class. 2021-10-19 23:11:13 +08:00
huanghongxun f70ec8a090 fix(launch): cannot launch far old versions on macOS. 2021-10-19 23:07:00 +08:00
huanghongxun a4f22671c6 feat(java): download java 8/16 when auto selected java not found. 2021-10-18 23:38:57 +08:00
huanghongxun 71c23df971 feat: use ArrayUtils from commons-lang3 instead. 2021-10-18 15:33:44 +08:00
huanghongxun 1bb2593c5b feat(crash): more detailed message when cause of game crash cannot be found out. 2021-10-18 15:33:19 +08:00
huanghongxun 45d5ef4602 build: commons-lang3 & commons-compress. 2021-10-18 03:28:09 +08:00
huanghongxun 7b5b2596b9 feat(multiplayer): WIP: update cato 1.1.0 2021-10-18 03:03:21 +08:00
huanghongxun 23a805d576 build: proguard works without multi-release. 2021-10-18 03:02:57 +08:00
huanghongxun f8cf3c22a1 fix(lang): incorrect hyperlinks marks. 2021-10-18 03:02:21 +08:00
Glavo 8e5725946c Copy manifest into final jar 2021-10-18 01:41:00 +08:00
Glavo 99bb0d7a93 final jar 2021-10-18 01:41:00 +08:00
huanghongxun 9695ddf8de build: WIP: make proguard work. 2021-10-17 16:29:24 +08:00
huanghongxun 50cff0bcba feat(lang): ja_JP. 2021-10-17 16:29:24 +08:00
zhixuan 1832cf5924 . 2021-10-17 16:26:20 +08:00
zhixuan 10fba0a2b2 Add Google translate 2021-10-17 16:26:20 +08:00
zhixuan e2050fd47a . 2021-10-17 16:26:20 +08:00
zhixuan f72da5973f first commit 2021-10-17 16:26:20 +08:00
huanghongxun 0ebeffac32 feat: improve error message for AccessDeniedException. Mentioned in #1114. 2021-10-17 15:13:42 +08:00
huanghongxun dcfe724803 fix: incorrect position of title when drag in a modpack file. 2021-10-17 03:24:05 +08:00
huanghongxun 821020e7b6 fix: typo. 2021-10-17 00:54:27 +08:00
huanghongxun 33efc38f4c feat(crash): more detailed prompt for graphics card issue. Closes #1102. 2021-10-17 00:49:52 +08:00
huanghongxun f01c0408e1 fix: misused skin texture as cape texture. Closes #1106. 2021-10-17 00:32:48 +08:00
huanghongxun 365ae0ef8c fix: FlowPane does not accept max-width setting. Closes #1109. 2021-10-17 00:32:48 +08:00
huanghongxun a6e593e3a1 feat(multiplayer): timeout for cato main net connection. 2021-10-17 00:32:47 +08:00
huanghongxun 67d38d6333 fix: DST ROOT CA X3 has already expired. Closes #1100. 2021-10-17 00:32:46 +08:00
Glavo 513d101fc4 fix github action warnings 2021-10-17 00:22:14 +08:00
Glavo 5e1304f699 Add short SHA to the name of github action artifact 2021-10-16 17:48:18 +08:00
Glavo 955a06d689 Optimize invitation code generation 2021-10-16 17:47:43 +08:00
Glavo 59e1a470e3 Log classpath in log window 2021-10-16 00:01:17 +08:00
Glavo 16952f5695 Update script format 2021-10-16 00:01:17 +08:00
Glavo 67feca075b Allow users to export PowerShell scripts 2021-10-16 00:01:17 +08:00
Glavo 12368375cf Pass classpath by environment variable 2021-10-16 00:01:17 +08:00
Glavo 284ffb7aa0 Clearly specify the in all places 2021-10-14 23:09:06 +08:00
Glavo 1aefb26575 Extract duplicate code 2021-10-14 22:27:34 +08:00
Glavo 670d0f8462 Remove unnecessary code 2021-10-14 22:27:34 +08:00
Glavo 3bbe422421 When using Java with different architectures, display the Java architecture in the crash window 2021-10-14 22:27:34 +08:00
Glavo d68e988c15 Relax timeout restrictions 2021-10-14 22:27:34 +08:00
Glavo 74fd35047c Use 'PROCESSOR_IDENTIFIER' to determine the architecture on Windows 2021-10-14 22:27:34 +08:00
Glavo 21a9d19105 Allow use cato on Windows on ARM 2021-10-14 22:27:34 +08:00
Glavo f662e983a4 Improve 'Architecture' 2021-10-14 22:27:34 +08:00
Glavo 7db379492a Log architecture information 2021-10-14 22:27:34 +08:00
Glavo 4a82daccec fix: checkstyle 2021-10-14 22:27:34 +08:00
Glavo 21e44bbc90 Do not automatically select Java non for x86 and x86-64 2021-10-14 22:27:34 +08:00
Glavo 42a40a844a Minor improvements 2021-10-14 22:27:34 +08:00
Glavo cd7c515097 Accurately obtain the Windows version number; Report error when users try to run minecraft using java for other platforms 2021-10-14 22:27:34 +08:00
Glavo 9595626a27 Enhance JavaVersionConstraint 2021-10-14 22:27:34 +08:00
Glavo db105c90b5 Update the link of AdoptOpenJDK 2021-10-14 22:27:34 +08:00
Glavo 74a305c32a Download Java on MacOS aarch64 2021-10-14 22:27:34 +08:00
Glavo 3af115ce37 Use separate native folder for each platform 2021-10-14 22:27:34 +08:00
Glavo b4e7934396 Improve display Java version 2021-10-14 22:27:34 +08:00
Glavo 0a54037460 Improve 'Architecture' and 'JavaVersion' 2021-10-14 22:27:34 +08:00
Glavo dd8da57cfb New 'Platform' class 2021-10-14 22:27:34 +08:00
Glavo 9b6f2d00ca Rename 'Platform' to 'Bits' 2021-10-14 22:27:34 +08:00
huanghongxun 2852821d2c fix(mod): does not translate mod with id "examplemod" 2021-10-13 21:20:08 +08:00
huanghongxun 673bc75cd5 fix(javafx): should download javafx at beginning, not let user choose download source first. 2021-10-13 13:58:33 +08:00
Glavo 3fad76aae1 自动开始下载 JavaFX (#1063)
* Automatically start downloading JavaFX

* Reimplement ProgressFrame with JDialog
2021-10-13 13:54:24 +08:00
huanghongxun 2cb70a41fb fix(multiplayer): forget to respond handshake. 2021-10-13 13:48:47 +08:00
huanghongxun ef4f2ed791 feat(ui): translations for AccessDeniedException. 2021-10-13 13:47:59 +08:00
huanghongxun c9a73a366d fix: test. 2021-10-13 02:39:25 +08:00
huanghongxun 7973179ddf feat(crash): ModResolutionException (Could not resolve valid mod collection) 2021-10-13 02:32:56 +08:00
huanghongxun 6b656bdc48 chore(crash): some comment. 2021-10-12 23:56:34 +08:00
huanghongxun 428c1a1799 feat(java): WIP: show expected java version range when no valid java. 2021-10-12 23:49:06 +08:00
huanghongxun 8892e2b273 feat(crash): fabric 0.12 may be incompatible with mods. 2021-10-12 23:39:12 +08:00
huanghongxun 2e8e6dd39e fix: mess tag. Closes #1090. 2021-10-12 23:16:01 +08:00
huanghongxun 3ac16b15c6 fix(multiplayer): should not show unsupported platform when running on 32-bit Java, but 64-bit OS. 2021-10-12 23:06:13 +08:00
huanghongxun ec3dfb908c fix(auth): OAuth login dialog logging state. 2021-10-12 22:53:35 +08:00
huanghongxun b5484bb13b fix(system): we may get x86_64 when running 32-bit JVM in 64-bit Windows. 2021-10-12 22:53:09 +08:00
huanghongxun c5a43efafb fix: checkstyle. 2021-10-12 22:06:25 +08:00
huanghongxun 23c2c0689c feat(account): OAuth account relogin. 2021-10-12 22:00:41 +08:00
huanghongxun 948d64237e fix(multiplayer): show friendly message when cato file not found. 2021-10-12 21:14:14 +08:00
huanghongxun 3433e88bb8 feat(microsoft): WIP: handle credentials expiration. 2021-10-12 14:04:18 +08:00
huanghongxun 28278c091d feat(multiplayer): more logs. 2021-10-12 14:03:46 +08:00
huanghongxun 04a68d0b95 feat(ui): show world download button in worldlistpage. 2021-10-12 14:03:27 +08:00
huanghongxun 9b0c0d1870 feat(download): resourcepack game version selection. 2021-10-10 22:32:59 +08:00
huanghongxun 86734d2f5f fix(ui): incorrect padding. 2021-10-10 22:24:09 +08:00
huanghongxun 7d5a73828d feat(download): category 2021-10-10 22:20:32 +08:00
huanghongxun 0fd8a78972 fix(multiplayer): show connection timeout instead of kicked. 2021-10-10 21:51:36 +08:00
huanghongxun fca78cf419 fix(crash): JVM_32BIT pattern. 2021-10-10 21:18:19 +08:00
huanghongxun 5f98c8c8bb feat(crash): detect memory exceeding. 2021-10-10 20:13:21 +08:00
huanghongxun dc4a791a9e fix(multiplayer): show anti-virus software warning if cato file not found. 2021-10-10 19:02:37 +08:00
huanghongxun d89c8cc2d2 fix(crash): not displaying memory and java version. 2021-10-10 17:12:49 +08:00
huanghongxun 9b24c683c3 feat: show bilibili url in dev/nightly version. 2021-10-10 16:45:31 +08:00
huanghongxun 2acea40204 fix: compile. 2021-10-10 02:48:12 +08:00
huanghongxun f014f59cf2 feat(update): update channel is bound to build type. 2021-10-10 02:27:20 +08:00
huanghongxun 6221f4b616 feat(multiplayer): auto rejection. 2021-10-10 02:02:44 +08:00
huanghongxun 32d557ee07 fix(skin): CustomSkin API should have suffix slash. 2021-10-10 01:09:41 +08:00
huanghongxun ba6eb17a38 fix: checkstyle. 2021-10-10 00:48:06 +08:00
huanghongxun 935436ce49 fix(ui): crash when duplicating game version. 2021-10-10 00:41:39 +08:00
huanghongxun 877c5a827c feat(multiplayer): join request should be cancelled when user operation time out. 2021-10-10 00:37:12 +08:00
huanghongxun eedcff785e feat(multiplayer): an option for auto allowing all join requests. Closes #1093. 2021-10-10 00:37:11 +08:00
huanghongxun 60865163db chore(ui): title gray background. 2021-10-09 20:44:19 +08:00
huanghongxun c32a14a959 chore(multiplayer): test NAT by fast method. 2021-10-09 20:39:08 +08:00
huanghongxun 3ddab57ab5 chore(multiplayer): NAT full cone should be evaluated as good. 2021-10-09 20:34:54 +08:00
huanghongxun 20957106df feat(skin): custom model. 2021-10-09 17:49:17 +08:00
huanghongxun f528f3bf40 feat(crash): UnsatisfiedLinkError. 2021-10-09 17:24:07 +08:00
huanghongxun 14fd0d484e feat(ui): show warnings for workaround options. 2021-10-09 17:17:28 +08:00
huanghongxun 6fc60f1387 fix(feedback): incorrect action when clicking blank space. Closes #1087. 2021-10-09 15:59:22 +08:00
huanghongxun 5ad46279c2 feat(multiplayer): cato 1.0.c supports darwin-aarch64. 2021-10-09 15:45:09 +08:00
huanghongxun f4894ccb9d feat(about): minecraft-jfx-skin. 2021-10-09 15:42:48 +08:00
huanghongxun 2b04292424 fix(multiplayer): does not download cato if not found. 2021-10-07 19:00:27 +08:00
huanghongxun 775b58d0fd fix(crash): crash when game crashed. 2021-10-07 18:50:11 +08:00
huanghongxun 6d09d0984e fix(ui): refine local modpack install page ui. Closes #1089. 2021-10-07 15:25:25 +08:00
huanghongxun 6ce5106271 fix: update curse fabric mod. Closes #1091. 2021-10-07 15:22:03 +08:00
huanghongxun fdbe38496a fix: checkstyle. 2021-10-06 17:44:51 +08:00
huanghongxun abae32fd70 feat(mod): only show buttons for selection when some mods selected. 2021-10-06 17:28:21 +08:00
huanghongxun e4a1fa5bb4 fix(ui): crash. 2021-10-06 17:18:21 +08:00
huanghongxun 1b8cf6b1e5 feat(ui): decorator page animation.. 2021-10-06 17:05:51 +08:00
huanghongxun 135f15dd9f feat(ui): DecoratorAnimatedPage for future animation implementation. 2021-10-06 16:08:08 +08:00
huanghongxun 1cd4470187 feat(ui): title opacity setting. 2021-10-06 15:52:31 +08:00
huanghongxun 3a2f5a77ef fix(multiplayer): broadcast port wrong. 2021-10-06 03:22:14 +08:00
huanghongxun 4201ac9203 fix(ui): add download new game in gamelistpage again for tradition. 2021-10-06 03:22:14 +08:00
huanghongxun 6c3178a831 feat(mod): mod update. 2021-10-06 03:22:13 +08:00
huanghongxun d48591a4cd feat(mod): WIP: mod update. 2021-10-06 03:22:13 +08:00
huanghongxun 271885a85e fix(launch): On Linux x86_64, Minecraft 1.12.2- are not compatible with JDK 9+. 2021-10-06 03:22:12 +08:00
huanghongxun 57e6765bfd feat(crash): show forge/fabric version in game crash window. 2021-10-06 03:22:12 +08:00
huanghongxun 9a46596b6c fix(skin): NPE. 2021-10-06 03:22:11 +08:00
huanghongxun 27f6ff74fc fix(feedback): localized login error message. 2021-10-06 03:22:11 +08:00
huanghongxun 03df019638 fix: onWeakChange should return ChangeListener instead of WeakChangeListener. 2021-10-06 03:22:10 +08:00
huanghongxun 23750e1222 feat(mod): mod upgrade detection. 2021-10-06 03:22:09 +08:00
huanghongxun ba0a7ddfa8 refactor(task): fix stage progress. 2021-10-06 03:22:08 +08:00
huanghongxun 31327d685b refactor(mod): ModInfo -> LocalMod 2021-10-06 03:22:07 +08:00
huanghongxun bb6bd86e71 refactor(download): RemoteModRepository 2021-10-06 03:22:07 +08:00
catsout 4ba897660b Support XDG_Base_Directory environment variable (#1085) 2021-10-05 19:57:21 +08:00
catsout c3fc22c6a9 Check writable on jarfile for conifg (#1084) 2021-10-05 18:12:08 +08:00
huanghongxun 8748b8c277 feat(multiplayer): relay mode. 2021-10-04 00:38:05 +08:00
huanghongxun e934d6c983 feat(install): Optifine 1.17 compatible with Forge now. 2021-10-03 22:04:55 +08:00
huanghongxun dfb2d3f2bf feat(feedback): implement feedbacks. 2021-10-03 20:34:42 +08:00
huanghongxun c2a87e2474 feat(multiplayer): token should be hidden. 2021-10-03 20:34:41 +08:00
huanghongxun 9229727544 feat(multiplayer): move token storage to glboal to prevent from token leak. 2021-10-03 20:34:40 +08:00
huanghongxun cc8dd77d90 fix: cancellation exception. 2021-10-03 20:34:40 +08:00
Jason_Khew de9233fba5 fix(ui): AccountLoginPane not showing. (#1080) 2021-10-03 14:15:53 +08:00
huanghongxun 05cda699a5 fix(ui): AboutPage JSTUN. 2021-09-30 20:29:12 +08:00
huanghongxun 358a520305 feat(ui): adjust root page icons. 2021-09-30 20:22:46 +08:00
huanghongxun 57cb98f13a feat(build): try proguard again. 2021-09-30 19:38:51 +08:00
huanghongxun 33aeb24dbe fix(download): no longer download fabric api list from bmclapi. 2021-09-30 19:35:47 +08:00
huanghongxun 973510a3df fix(skin): cannot find image. 2021-09-30 19:31:39 +08:00
huanghongxun ae8f67d507 feat(multiplayer): join application. 2021-09-30 17:38:18 +08:00
huanghongxun a48336db4f feat(multiplayer): kick player. 2021-09-30 17:27:43 +08:00
huanghongxun e69ef0ce25 feat(multiplayer): cato EULA. 2021-09-30 15:01:56 +08:00
huanghongxun e01c31fb1d feat: show java version detection progress when launching game. 2021-09-29 18:52:56 +08:00
huanghongxun 36d1096976 fix: java version detection timeout. 2021-09-29 17:24:32 +08:00
huanghongxun 6901228a15 fix: ui crash. 2021-09-29 14:58:56 +08:00
huanghongxun 8100662eb5 feat(about): JSTUN 2021-09-28 21:42:40 +08:00
huanghongxun ee4c03a427 chore(about): show bilibili ID instead of personal website. 2021-09-28 20:42:56 +08:00
huanghongxun adb2ed0614 fix(download): only list required mod dependency 2021-09-28 20:30:36 +08:00
huanghongxun e78fd8ba94 feat: adjust announcement pane look. 2021-09-28 17:32:29 +08:00
huanghongxun 754ecadbb1 feat(crash): notify feedback when game crashed. 2021-09-28 17:23:07 +08:00
huanghongxun ee89815628 feat(build): build with version type. 2021-09-28 17:09:19 +08:00
huanghongxun 8a9527bf38 feat(crash): show fabric mod confliction and missing. 2021-09-28 17:08:43 +08:00
huanghongxun e476460ab4 feat: notification of dev/nightly version. 2021-09-28 12:54:54 +08:00
huanghongxun b556c51307 feat(multiplayer): see lobby members. 2021-09-28 02:13:33 +08:00
huanghongxun fac812f1dd fix(multiplayer): MultiplayerChannel works. 2021-09-28 02:05:22 +08:00
huanghongxun c66cea41bd fix: offline account crash. 2021-09-28 01:44:57 +08:00
huanghongxun 8a3cdfb14c fix(multiplayer): wrong port. 2021-09-27 22:18:48 +08:00
huanghongxun a58de31d4b feat: auto select java. Closes #1068. 2021-09-27 20:54:23 +08:00
huanghongxun 04e6897a0d feat: WIP: auto java. 2021-09-27 20:54:22 +08:00
huanghongxun 59df08cd93 fix(multiplayer): accidentally enabled relay. 2021-09-27 20:54:04 +08:00
huanghongxun c70898ced7 fix(skin): remove javafx.swing dep. 2021-09-27 20:53:33 +08:00
huanghongxun ecc38621df feat: print found java versions in log. 2021-09-27 13:05:46 +08:00
huanghongxun 56327eba54 feat(multiplayer): show cato version. 2021-09-27 13:05:28 +08:00
huanghongxun e776532947 feat(multiplayer): save token. 2021-09-27 12:48:42 +08:00
huanghongxun 35f2d89a23 fix(multiplayer): bind to 0.0.0.0 instead of 127.0.0.1. 2021-09-27 12:38:43 +08:00
huanghongxun 3515ce1034 fix: bidirectional binding cannot be chained. Closes #1067. 2021-09-26 03:24:32 +08:00
huanghongxun 6c1046c060 feat(multiplayer): notify user if connection lost. 2021-09-26 03:08:06 +08:00
huanghongxun 7c76c919bc feat(multiplayer): custom token. 2021-09-26 00:34:18 +08:00
huanghongxun 54954f276b feat(multiplayer): detect game port when creating multiplayer room. Update cato to 1.0.8. 2021-09-26 00:02:48 +08:00
huanghongxun 7fd8e0721f feat: import .minecraft modpack. Closes #973. 2021-09-25 20:24:50 +08:00
huanghongxun d075dd27bc fix: style. 2021-09-25 02:17:11 +08:00
huanghongxun ace8be3df4 fix(multiplayer): update cato. 2021-09-25 02:16:44 +08:00
huanghongxun c60df2c26b chore: cleanup. Closes #989. 2021-09-25 02:11:31 +08:00
huanghongxun cd030c1de0 feat: support change skin of offline accounts. 2021-09-25 02:04:25 +08:00
huanghongxun 01893b053d feat(skin): WIP: download skin from CSL server. 2021-09-23 23:41:45 +08:00
huanghongxun 79e4aa5aaf feat: show nav to special settings page in global settings page if needed. 2021-09-23 21:20:16 +08:00
huanghongxun 3f4dbb6978 feat: discord & kaiheila in AboutPage. 2021-09-23 16:17:26 +08:00
huanghongxun 1df5b0c629 fix: compilation. 2021-09-23 15:04:15 +08:00
Glavo 83dbeddb12 More OpenJDK Providers 2021-09-22 13:43:01 +08:00
Glavo a8522320ed Search for Java installed by the official launcher 2021-09-22 13:43:01 +08:00
Glavo bcaf3de164 fix #1054: Avoid incorrect UI layout caused by long strings 2021-09-22 13:39:59 +08:00
huanghongxun cbe9f7474e feat: skin viewer 3d 2021-09-22 13:38:35 +08:00
huanghongxun 3a4b4e129f fix: offline account skin 2021-09-22 13:38:26 +08:00
huanghongxun 4535b546f4 feat: EULA. 2021-09-21 21:55:58 +08:00
huanghongxun 7d0bbc6952 fix: feedback page navigation. 2021-09-21 19:57:21 +08:00
huanghongxun ea844e03f9 feat: clicking blanks area also toggles component list cell expansion. 2021-09-21 19:57:07 +08:00
huanghongxun c7a893b02d feat: clicking blanks area also controls the toggle button. 2021-09-21 19:38:34 +08:00
huanghongxun ca75ecbc93 feat(multiplayer): report. 2021-09-21 18:47:34 +08:00
huanghongxun 3f9d16ab8c feat: eula. 2021-09-21 18:41:50 +08:00
huanghongxun fb7ff7b891 feat(multiplayer): check cato version before establishing connection. 2021-09-21 16:39:15 +08:00
huanghongxun 3cf5b97201 feat(multiplayer): show state of cato. 2021-09-21 11:12:04 +08:00
huanghongxun 2f5a9a50af fix: checkstyle. 2021-09-21 10:19:07 +08:00
huanghongxun 8153d3bda9 feat: customize font. Closes #967. 2021-09-21 10:11:00 +08:00
huanghongxun e796e0f35e feat(offline-skin): WIP: implement yggdrasil server to serve skin/cape textures. 2021-09-21 09:35:14 +08:00
huanghongxun 4537fe0390 fix: refine UI. Closes #1052. 2021-09-21 06:59:10 +08:00
huanghongxun 87944a4934 feat: AggregatedObservableListTest 2021-09-21 06:50:31 +08:00
huanghongxun 33b9d37250 fix: cannot launch game at first click with 1.17.1+fabric 0.2 2021-09-21 06:44:43 +08:00
huanghongxun ebeec04f83 feat: crash report analyzer more rules. 2021-09-21 06:43:52 +08:00
huanghongxun 798327eabb feat: crash report analyzer test. 2021-09-21 01:19:22 +08:00
asdqp233 702dd21eec 更新英文翻译 (#1050)
* updata lang file

* Update I18N_zh_CN.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N.properties

* Update I18N_zh_CN.properties

* Update I18N_zh.properties

* Update I18N.properties

* Update I18N_zh.properties

* Update I18N_zh_CN.properties

* Update I18N.properties
2021-09-20 23:47:01 +08:00
huanghongxun e5754c70da feat: allow not checking modpack completion when option "Do not check game completion" is checked. Closes #1038. 2021-09-20 21:58:47 +08:00
huanghongxun b95119bb0e feat: show more friendly message if installing mod in InstallerListPage. Closes #996. 2021-09-20 21:55:07 +08:00
huanghongxun ae8600c882 feat: update HMCLauncher. Closes #1014. 2021-09-20 21:25:28 +08:00
huanghongxun 271fb4ce87 feat: show warning when logging in Microsoft account if app is released as snapshot version. 2021-09-20 19:36:11 +08:00
huanghongxun 37cee9cebd feat: show migration hint when 410. Closes #1045. 2021-09-20 18:07:51 +08:00
huanghongxun 0b2e9c3f62 feat: Mod dependencies & Mod downloads should be classified. 2021-09-20 17:36:02 +08:00
huanghongxun f2741f9725 fix: crash when PATH is malformed. Closes #1048. 2021-09-20 17:36:02 +08:00
Glavo e4a7ba0bb5 fix: build 2021-09-20 14:29:17 +08:00
Glavo 6ac8519a9f Automatically download OpenJFX when JavaFX is missing 2021-09-20 14:29:17 +08:00
huanghongxun 01158c2320 feat: thanks to noin.cn 2021-09-20 13:52:16 +08:00
huanghongxun bbb78ec749 fix: clicking gamelistitem should navigate to versionpage. 2021-09-20 13:35:48 +08:00
huanghongxun 90976cc7e0 fix: should not navigate to DownloadPage when clicking GameListItem 2021-09-20 13:31:35 +08:00
huanghongxun cfe801f280 feat: classic background. 2021-09-20 03:09:55 +08:00
huanghongxun ca3bc69a61 feat: multifileitem supports multiple custom type. 2021-09-20 01:54:08 +08:00
huanghongxun 43236049ac feat: game crash window. 2021-09-19 16:15:36 +08:00
huanghongxun 3c797252d8 feat(multiplayer): support cato --allow. 2021-09-19 12:15:45 +08:00
huanghongxun 543238be4b fix: crash when concurrency is negative. 2021-09-19 10:42:55 +08:00
huanghongxun 1b539ade9d feat: hint for custom commands. 2021-09-19 03:00:18 +08:00
huanghongxun c0f82b1ae7 feat: env vars for pre-launch command and post-exit command. 2021-09-19 02:44:51 +08:00
huanghongxun 5b02393c79 feat: smooth scrolling. 2021-09-19 01:35:27 +08:00
huanghongxun c54a3f2053 feat(multiplayer): broadcast lan server. 2021-09-19 01:31:00 +08:00
huanghongxun e90ec531e5 fix(task): test. 2021-09-19 00:43:22 +08:00
huanghongxun 6ac2274c29 fix(multiplayer): exchange "from" and "to". 2021-09-19 00:42:51 +08:00
huanghongxun ea9f547ada fix: FileDownloadTask does not fail if 404. 2021-09-18 23:26:37 +08:00
huanghongxun 28cc7c0700 fix: checkstyle. 2021-09-18 22:01:15 +08:00
huanghongxun 6761028afd feat(multiplayer): cato temp token 2021-09-18 21:57:12 +08:00
huanghongxun 27f87c3a25 feat: show no version hint in GameListPage 2021-09-18 20:10:28 +08:00
huanghongxun 223c45130f feat: add purchase link 2021-09-18 20:00:06 +08:00
huanghongxun 3513c1a431 chore: cleanup. 2021-09-18 15:49:07 +08:00
huanghongxun 5b4185840f fix: Cannot recognize MCBBS v2 modpack. Closes #1041. 2021-09-18 15:49:07 +08:00
huanghongxun 390f6e016d fix(task): wrong cancellation implementation. Closes #1035. 2021-09-18 15:49:07 +08:00
huanghongxun e10e1e0613 fix: Browse Mod Directory Button in ModListPage does not work. Closes #1039. 2021-09-18 15:49:07 +08:00
huanghongxun ba41e6bc5e feat(multiplayer): invitation code generation. 2021-09-18 14:23:00 +08:00
huanghongxun 05562b5c2a feat: unify look. 2021-09-18 02:57:03 +08:00
huanghongxun 7d9cce131b fix: icon color should be changed when active. 2021-09-18 01:51:38 +08:00
huanghongxun 112b096c56 feat: rippler container cursor hover animation 2021-09-18 01:41:44 +08:00
huanghongxun aecb100498 feat: refine modpack export page look. 2021-09-18 00:05:33 +08:00
huanghongxun d5b4aa5d44 chore: exchange title and tag of modlistpage item. 2021-09-17 23:32:03 +08:00
huanghongxun cbe5e3a9ff fix: horizontal scroll bar should not appear. 2021-09-17 23:29:09 +08:00
huanghongxun ff3fa31c25 chore: larger contrast color palette in game memory setting. 2021-09-17 23:29:08 +08:00
Glavo 7baf8eba6e Upgrade gradle wrapper 2021-09-17 23:26:54 +08:00
Glavo 53b0223257 Use Gradle 7.2 2021-09-17 23:26:54 +08:00
Glavo 91cc83c054 fix: action name 2021-09-16 14:29:35 +08:00
Haowei Wen b1031be748 fix: nanohttpd isn't started as daemon thread 2021-09-13 16:59:22 +08:00
Haowei Wen c6b76f5200 remove 'Download' category in DownloadPage 2021-09-13 16:42:45 +08:00
huanghongxun 08269b7ce9 build: checkstyle in another workflow. 2021-09-13 15:45:41 +08:00
Redlnn c65ef8107d fix: fixed Chinese grammatical errors & clean up 2021-09-13 14:25:54 +08:00
huanghongxun 9fdf4f16c6 chore: remove Mod download page link in version page. 2021-09-13 00:16:55 +08:00
huanghongxun e821f7cb09 feat(auto-installing): download Fabric-API to mod folder. 2021-09-13 00:09:19 +08:00
huanghongxun 5c9deb129d feat(download): add import local modpack file button in modpack download page. 2021-09-12 23:38:47 +08:00
huanghongxun fa1c11db5c feat(feedback): remove feedback page. 2021-09-12 22:11:21 +08:00
huanghongxun 69b118eaec fix: incorrect max memory size. 2021-09-12 20:22:15 +08:00
huanghongxun f088bfa114 feat: search modrinth 2021-09-12 20:14:39 +08:00
huanghongxun f4c25003a0 feat: show error message when failed to refresh account. 2021-09-12 18:20:26 +08:00
huanghongxun 43ae5a24e5 fix: missing line separator before "cd". 2021-09-12 15:27:37 +08:00
Glavo fd27f16714 fix: ReflectionHelper missing method 'getHandle' (#1026) 2021-09-12 15:26:17 +08:00
Glavo beea7d0ae2 Export logs using UTF-8 (#1028) 2021-09-12 15:25:54 +08:00
huanghongxun eebd373aef fix: mcmod.png 2021-09-12 15:23:55 +08:00
huanghongxun b10330548e fix: doGet utf-8. #992. 2021-09-12 02:32:21 +08:00
huanghongxun 475d7f50f1 feat: directly show game download page in download page. 2021-09-12 02:23:56 +08:00
huanghongxun b689e68d46 fix: checkstyle and translations. 2021-09-12 01:46:09 +08:00
huanghongxun 87adbde965 feat(crash): some translations. 2021-09-12 01:41:33 +08:00
huanghongxun 5057691fb3 feat: open mcbbs/mcmod page in mod info/mod donwload page. 2021-09-12 01:41:32 +08:00
huanghongxun d7f5109a65 fix: cannot display all game versions in some cases of DownloadPage. Closes #1007. 2021-09-12 01:41:32 +08:00
huanghongxun 97f08bf74e feat: microsoft account migration link. 2021-09-12 01:41:31 +08:00
huanghongxun 1b3e4e5bd6 fix: Microsoft Account add family hint. 2021-09-12 01:41:30 +08:00
huanghongxun 0032242da6 feat: Mod chinese search 2021-09-12 01:41:30 +08:00
huanghongxun 0df1b62c3e chore: rename ModDownloadPage -> DownloadPage. 2021-09-12 01:41:29 +08:00
Glavo 8927260b75 适配 JEP 411:移除安全管理器操作 (#1000)
* Avoid calling AccessController.doPrivileged

* Keep the behavior unchanged before Java 17
2021-09-12 01:13:51 +08:00
Glavo edd4add76e 在缺失 javafx.web 的环境使用替代方案 (#1022)
* When WebView is missing, use JEditorPane instead

* Open update log with browser when WebView is missing

* Remove unnecessary prompt messages

* Update error message

* fix: checkstyle

* Add HyperlinkListener to JEditorPane
2021-09-12 01:12:50 +08:00
Glavo 2d543e69ff fix: Read instance.cfg with wrong encoding (#1023) 2021-09-12 01:11:49 +08:00
Glavo 275c14f556 在更多位置搜索 JRE (#1024)
* Search JDK in 'Program Files (ARM)'

* fix: HMCL_JRES should use the root path of JRE
2021-09-12 01:11:31 +08:00
huanghongxun f45e0abbf4 feat(mod): Chinese translations for mod names. 2021-09-11 20:08:38 +08:00
huanghongxun 2d8248461a feat(mod): Chinese translations for mod names. 2021-09-11 18:18:51 +08:00
huanghongxun e4847bbfcf Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx 2021-09-11 18:17:49 +08:00
Kaedehara Kazuha 127e069041 fix: fix grammatical errors & clean up (#1021) 2021-09-11 17:08:13 +08:00
ExDragine 87bf7c8d8d Fixed Chinese grammatical errors (#1020) 2021-09-11 15:43:37 +08:00
Glavo 513897406d Use GitHub Action to check for missing translations (#1019)
* Use GitHub Action to check for missing translations

* fix: missing translations

* fix: Traditional Chinese in I18N_zh_CN

* fix: I18N_zh
2021-09-11 14:39:55 +08:00
Glavo 4ace33bd80 fix: missing translations for mods.* 2021-09-11 13:12:08 +08:00
Jason_Khew c8ba03ae2b Fix MEMINFO_PATTERN is null 2021-09-11 13:11:46 +08:00
huanghongxun 784e22afa7 feat(crash-report): WIP: crash-report parser rules. 2021-09-10 23:22:27 +08:00
huanghongxun eedb9f148c feat: change directory to game dir in generated shell scripts. 2021-09-10 23:19:12 +08:00
huanghongxun 0256b8a971 feat: force application running on dedicated graphics card. So Minecraft can also run on dedicated graphics card too. 2021-09-10 21:21:51 +08:00
huanghongxun 95ea6610b1 feat: use available memory in Linux instead of free memory. 2021-09-10 20:58:40 +08:00
huanghongxun fd246b7bc6 feat(download): WIP: select game version in download page. 2021-09-09 23:51:24 +08:00
huanghongxun 98c6651793 feat: copy link when web browser cannot be opened. 2021-09-09 23:16:44 +08:00
huanghongxun 3327899cf6 feat(multiplayer): WIP: download cato. 2021-09-09 22:09:12 +08:00
huanghongxun 8c3904cdcc feat: modify UUID of offline accounts. Closes #1003. 2021-09-09 22:09:12 +08:00
huanghongxun 108df2293b fix: 411 Length Required when POST live.com. 2021-09-09 22:09:12 +08:00
huanghongxun 336d7ba221 fix: #1004. 2021-09-09 22:09:12 +08:00
Glavo 0fdac8a308 Use JDK 11 in gradle.yml 2021-09-09 14:12:08 +08:00
huanghongxun effc6d725a fix: missing translations for account.create.*. Closes #1010. 2021-09-09 14:08:46 +08:00
huanghongxun 7adc517872 fix: "does not support loading the entire remote list" 2021-09-09 14:08:46 +08:00
huanghongxun e385a85755 feat: WIP: FeedbackPage 2021-09-09 14:08:46 +08:00
huanghongxun 279b2d6f80 fix: #1002. 2021-09-09 01:19:12 +08:00
Glavo 71a845c1ed create release using github action 2021-09-08 23:15:42 +08:00
Glavo 8a908b06b6 fix typo 2021-09-08 15:06:12 +08:00
Glavo d94b3385c7 fix: DirectoryStream not closed 2021-09-08 15:06:12 +08:00
Glavo 1b72e43cce follow environment variables when finding JREs 2021-09-08 15:06:12 +08:00
Yuhui Huang 2e8d1a7702 Merge pull request #998 from Glavo/aarch64
支持在更多平台上自动补全 JavaFX
2021-09-08 15:05:45 +08:00
Glavo 6eeb2e922d Merge branch 'javafx' into aarch64 2021-09-08 14:56:25 +08:00
Glavo 36878c81aa update I18N 2021-09-08 10:17:20 +08:00
Glavo e4ac791569 exclude javafx.media on linux arm32 2021-09-08 09:53:26 +08:00
Glavo 8f591f99bd reorder dependencies 2021-09-08 09:38:45 +08:00
Glavo fc21e76507 use OpenJFX 17 2021-09-08 09:13:33 +08:00
Glavo ef118d3736 fix: Progress bar reaches 100% in advance 2021-09-07 22:23:30 +08:00
Glavo 47523db331 Pre-Touch OpenJFX dependencies 2021-09-07 21:35:35 +08:00
Glavo 7a2c52b34c Download OpenJFX on Linux ARM32 2021-09-07 20:07:11 +08:00
Glavo 3ff2284ccc Generating openjfx-dependencies.json using gradle task 2021-09-07 19:41:57 +08:00
Haowei Wen e7ce68d3cb remove useless files 2021-09-07 11:52:47 +08:00
Haowei Wen b78f5407cc redesign account list page sidebar 2021-09-07 00:46:46 +08:00
Haowei Wen b7997dfbce fix: error message overlapping in add account dialog 2021-09-06 22:45:37 +08:00
Haowei Wen d8943c266e fix: polish accounts list page 2021-09-06 21:56:17 +08:00
Haowei Wen 3051332165 feat: continue launching after adding account 2021-09-06 21:55:16 +08:00
Haowei Wen dc2911f7d8 fix: build 2021-09-06 20:17:03 +08:00
Haowei Wen 6a2ffa4f30 fix: inconsistent sidebar icon size & color 2021-09-06 19:27:10 +08:00
Haowei Wen 56bd2e0d95 feat: move auth servers to sidebar 2021-09-06 10:53:57 +08:00
huanghongxun d521d10a85 feat: WIP: multiplayer 2021-09-06 00:45:25 +08:00
huanghongxun c844a77f02 fix: sort mod download list page items. Closes #1001. 2021-09-05 21:48:29 +08:00
huanghongxun b3e3192876 feat: refine mod list ui. 2021-09-05 21:47:26 +08:00
huanghongxun 21e28ab038 feat: show more human-readable error message when no Java Edition profile existing in MicrosoftAccount. 2021-09-05 19:50:48 +08:00
huanghongxun 47540e7f91 fix: wrong i18n id 2021-09-05 19:50:05 +08:00
huanghongxun 3a406744c2 fix: padding around sponsor pane 2021-09-05 19:49:39 +08:00
huanghongxun 8a0faa72c1 fix: listview not filled the blank 2021-09-05 19:48:45 +08:00
Glavo 5d768fe64e update I18N text 2021-09-04 16:19:54 +08:00
huanghongxun 7166a809c3 feat: jstun? 2021-09-04 15:54:23 +08:00
huanghongxun c11d947099 FIx: too large spinner in accountlistitem. Closes #999. 2021-09-04 15:52:40 +08:00
huanghongxun 723f216e6b feat: JSTUN 2021-09-04 15:49:55 +08:00
huanghongxun 1000cc1ab0 feat: WIP: account login progress 2021-09-04 15:49:34 +08:00
Glavo d77a09042d fix ExceptionInInitializerError 2021-09-04 13:21:55 +08:00
Glavo 5310de88d5 select classifier according to the platform 2021-09-04 13:07:13 +08:00
Glavo ba33b927a7 update openjfx-dependencirs.json 2021-09-04 12:49:24 +08:00
huanghongxun 72ca0f20a1 feat: MicrosoftAccount: find skin by uuid instead of fetching profile. 2021-09-04 02:07:58 +08:00
huanghongxun bb15d72032 fix: add some missing tranditional Chinese translations. 2021-09-04 02:06:56 +08:00
huanghongxun cabba82118 feat: move custom commands settings to individual section. 2021-09-04 01:26:32 +08:00
Glavo 353d2794c9 improve FXUtils#openLink on Linux 2021-09-03 14:42:21 +08:00
Glavo faede16662 fix: LZMAInputStream not closed 2021-09-03 14:41:24 +08:00
Haowei Wen 665b0c4390 wip: account refactor 2021-09-03 13:02:29 +08:00
huanghongxun feb15a3c64 feat: change style of text field and combo box to match Chrome. 2021-09-03 03:56:10 +08:00
Glavo 050fa30a35 update README 2021-09-03 03:55:56 +08:00
Glavo 3314bf93b0 fix style 2021-09-03 03:55:56 +08:00
Glavo 7eed142a6f fix style 2021-09-03 03:55:56 +08:00
Glavo ac4de8b9fd fix style 2021-09-03 03:55:56 +08:00
Glavo 64a92b7768 ask the user whether to download javafx 2021-09-03 03:55:56 +08:00
Glavo cb49700ded use system look and feel 2021-09-03 03:55:56 +08:00
huanghongxun 243c9b83b9 feat: download concurrency settings. Closes #990. 2021-09-03 03:18:36 +08:00
huanghongxun b3d083877f fix: javafx bound property 2021-09-03 03:18:35 +08:00
huanghongxun 5d7c9cd15d feat: make scrollbar opaque. 2021-09-03 03:18:35 +08:00
huanghongxun 96987763a5 feat: WIP: Fabric API install 2021-09-03 03:18:34 +08:00
huanghongxun 57bb719cb8 feat: show account creation dialog when no account exists at launching game. 2021-09-03 03:18:33 +08:00
huanghongxun 981127c355 feat: set background to translucent white to make texts easier to read. 2021-09-03 03:18:32 +08:00
Haowei Wen 770a7411aa fix: scrollbar not properly displayed on jfx 16 2021-09-02 13:09:15 +08:00
Haowei Wen e428cc7117 fix: check if property is bound before setting value of ReadWriteComposedProperty 2021-09-01 16:43:07 +08:00
Haowei Wen 832c029db6 fix: can't run as named module 2021-09-01 16:43:06 +08:00
huanghongxun b700ad6b0c feat: refine download page. 2021-08-29 21:15:03 +08:00
huanghongxun 575f27cde1 feat: some sort of Minecraft CurseForge sections. 2021-08-29 17:19:35 +08:00
huanghongxun 48aab94fdf feat: balanced download provider. Closes #959. 2021-08-29 17:19:09 +08:00
huanghongxun 19bcf453a5 fix: cannot launch fabric 1.18 experimental version. Closes #950. 2021-08-29 17:18:07 +08:00
huanghongxun 0210b9fd3d feat: balanced download provider & show release date in download versions page & download page ui refactor. 2021-08-29 17:16:58 +08:00
huanghongxun a20331dfa8 fix: cannot build by latest IDEA. 2021-08-29 17:00:36 +08:00
huanghongxun 088b2f146f fix: AutoDownloadProvider not fully adapted file requests. 2021-08-29 17:00:15 +08:00
huanghongxun e9847918a9 fix: potential infinite loop when exporting game crash info.. 2021-08-29 08:35:22 +08:00
huanghongxun fcbfbd060e fix: Cannot launch modded 1.16 game from PCL. Closes #977. 2021-08-29 08:34:58 +08:00
huanghongxun f2ff523109 feat: [STDERR/] as error message. 2021-08-29 08:03:31 +08:00
huanghongxun 9cee8fa49f fix: compilation 2021-08-29 07:57:37 +08:00
huanghongxun 13038437fd fix: some stderr not recognized. 2021-08-29 07:57:15 +08:00
huanghongxun 8c641555cf fix: no longer replace player uuid in logs. 2021-08-29 07:54:53 +08:00
huanghongxun 4df2af82d1 fix: list-cell padding 2021-08-29 07:52:57 +08:00
huanghongxun 50ce3617be feat: download game in download page 2021-08-29 07:51:09 +08:00
huanghongxun fb78b8f573 feat: scroll thumb color 2021-08-29 07:50:17 +08:00
huanghongxun f8eae1f534 feat: WIP auto choose download source. 2021-08-29 06:57:24 +08:00
huanghongxun 1a0fd11e83 fix: incorrect isInitialized implementation. 2021-08-29 06:28:13 +08:00
huanghongxun d5e2bc455f fix: replace ${file.jarVersion} in mods.toml. 2021-08-29 06:26:37 +08:00
huanghongxun 23bb926acf feat: WIP: Download Page 2021-08-29 05:53:19 +08:00
huanghongxun 038cc97ecf fix: clicking global game settings should be navigated to launcher settings page. 2021-08-29 05:27:45 +08:00
huanghongxun b778d0a014 fix: memory related English translations missing. 2021-08-29 05:19:23 +08:00
huanghongxun de99213d8a feat: export necessary (debug.log, latest.log, launch script, hmcl.log and json) crash information when game got crashed. 2021-08-29 05:07:41 +08:00
huanghongxun 5a1ebe906d fix: no longer check linux for free memory size. 2021-08-29 04:27:31 +08:00
huanghongxun c23b9511b8 feat: option to use system glfw/OpenAL. Closes #958. 2021-08-29 04:26:12 +08:00
huanghongxun 6ee39aa302 fix: checkstyle 2021-08-29 02:01:40 +08:00
huanghongxun e3fb36c63f fix: discord channel is still necessary. 2021-08-29 01:51:22 +08:00
Haowei Wen dd6c192a93 do not alter version_name 2021-08-29 00:41:39 +08:00
Haowei Wen 798c67eaef display HMCL full name only in titlebar 2021-08-29 00:41:38 +08:00
huanghongxun 33d27f74fe feat: switch to KaiHeiLa instead of Discord 2021-08-29 00:37:31 +08:00
huanghongxun 680bcb8129 feat: show global game settings in launcher settings page. 2021-08-29 00:02:33 +08:00
huanghongxun 90ee87062f feat: new memory settings 2021-08-28 23:34:03 +08:00
huanghongxun 924743c622 feat: 3.4 2021-08-28 19:48:57 +08:00
huanghongxun 56865d602f fix: minimize. 2021-08-28 19:46:45 +08:00
huanghongxun dc32151501 fix: process microsoft account login error code. 2021-08-28 19:37:51 +08:00
huanghongxun b9382ce966 fix: macos also support 'nice' command. 2021-08-28 18:54:09 +08:00
huanghongxun 2de5c36df5 feat: process priority settings. 2021-08-28 18:53:24 +08:00
huanghongxun 3972683583 feat: allow cancel logging task. 2021-08-28 18:22:21 +08:00
huanghongxun fcafca2fdb feat: download settings page & appearance settings page. 2021-08-28 18:12:12 +08:00
huanghongxun 635351d58d feat: move doc link to help page. 2021-08-28 16:57:33 +08:00
huanghongxun dc3caea2f4 Merge commit '9bef8b432b9095bdb470f7a1e7c1ce8fcab37c01' into javafx 2021-08-28 16:41:29 +08:00
huanghongxun 9bef8b432b feat: account list page left pane 2021-08-28 16:35:28 +08:00
huanghongxun 6e45ea835f bulid: minimize jar 2021-08-28 16:29:36 +08:00
huanghongxun f1e4364ffa fix: background size 2021-08-28 16:29:35 +08:00
huanghongxun a560eff153 feat: title bar with color 2021-08-28 16:29:33 +08:00
huanghongxun e9bc1524f1 fix: no longer hide player id in game logs. 2021-08-28 16:29:31 +08:00
huanghongxun cc868656a8 fix: mess code in Microsoft login page. Closes #981. 2021-08-28 16:27:27 +08:00
huanghongxun 96e6554b37 fix: remove useless Content-Type header in GET request. Closes #984. 2021-08-28 16:26:19 +08:00
huanghongxun f869ddbbd2 feat: about page javafx 2021-08-28 15:22:38 +08:00
huanghongxun 6ebdfb0a26 feat: launch icon -> rocket-launch-outline 2021-08-28 15:22:18 +08:00
huanghongxun e2ac171a92 feat: account list page left pane 2021-08-28 15:13:23 +08:00
huanghongxun 9abca03e50 bulid: minimize jar 2021-08-28 01:29:26 +08:00
huanghongxun 63f4a3dc5c fix: background size 2021-08-28 01:29:13 +08:00
huanghongxun 34ceb1a29d feat: title bar with color 2021-08-28 01:24:36 +08:00
Haowei Wen 8230c28084 feat: press esc to go back 2021-08-27 23:24:57 +08:00
Haowei Wen a85d3b504f fix: main page account item not working properly 2021-08-27 22:57:49 +08:00
Haowei Wen 6ac3f68dfc feat: add param hmcl.openjfx.repo 2021-08-27 20:38:12 +08:00
Haowei Wen e37322d147 fix: NPE caused by uninitialized processPriority 2021-08-27 20:05:18 +08:00
huanghongxun f2dd7e4295 fix: no longer hide player id in game logs. 2021-08-27 00:19:37 +08:00
huanghongxun 647bbb38e8 feat: new background image 2021-08-26 03:53:17 +08:00
huanghongxun ff899db6b0 feat: rounded window corner 2021-08-26 03:53:09 +08:00
huanghongxun 8bf2a2fad3 fix: about 2021-08-26 03:52:53 +08:00
huanghongxun e9061ebc8b feat: process priority 2021-08-26 03:52:32 +08:00
huanghongxun 5edd7a7b72 feat: help page 2021-08-26 03:11:23 +08:00
huanghongxun bd343f49f6 feat: discord chat 2021-08-24 21:14:39 +08:00
huanghongxun f5a16ae005 fix: sponsor mess code 2021-08-24 03:44:12 +08:00
huanghongxun 837c7adaa7 fix: crash when profile outdated 2021-08-24 01:35:25 +08:00
huanghongxun a27df90fc5 fix: missing microsoft auth id 2021-08-23 11:44:10 +08:00
huanghongxun c18b0510bc feat: sponsor list 2021-08-23 00:30:07 +08:00
huanghongxun 7bf06684c4 feat: WIP: Sponsor page 2021-08-22 22:26:10 +08:00
huanghongxun f79465038b fix: remove debug statements 2021-08-22 22:18:02 +08:00
huanghongxun 54fe311a3d fix: checkstyle 2021-08-22 21:32:54 +08:00
huanghongxun 3b8a0989de fix: Microsoft Account refresh token 2021-08-22 21:29:08 +08:00
huanghongxun 5890f0c782 feat: authenticate Microsoft accounts via external browser 2021-08-22 20:35:30 +08:00
huanghongxun b39508922f fix: memory leak 2021-08-22 17:14:06 +08:00
huanghongxun 13034db3f9 fix: checkstyle 2021-08-22 17:10:46 +08:00
huanghongxun a21e303a8a chore: adjust order of account factory 2021-08-22 17:06:21 +08:00
huanghongxun 4bdacb66a6 feat: AddAccountPane tabularization 2021-08-22 17:05:10 +08:00
huanghongxun 2fea07fc6c revert: RootPage UI 2021-08-21 22:10:34 +08:00
huanghongxun 9741ba4b07 feat: show avatars of contributors in about page. 2021-08-21 19:57:58 +08:00
huanghongxun d2d0875c36 feat: WIP: new account look 2021-08-21 18:47:37 +08:00
huanghongxun 494d40c8b5 feat: WIP: change launcher ui font 2021-08-21 18:47:17 +08:00
huanghongxun 0b78033a87 feat: new about page 2021-08-21 18:46:28 +08:00
huanghongxun 66bb27c4dd fix: incorrect detected forge version. Closes #961. 2021-08-21 15:50:22 +08:00
huanghongxun f00afbe1b5 feat: support mcbbs modpack second edition. Closes #964. 2021-08-21 15:01:30 +08:00
huanghongxun 7da9d0f283 feat: cache detected game version 2021-08-21 14:41:56 +08:00
huanghongxun ddd0f83936 perf: no longer using JFXColorPicker to speed up page loading. 2021-08-21 00:59:53 +08:00
Haowei Wen c660b1d3f8 fix: version id checking not working properly 2021-08-20 23:13:11 +08:00
Yuhui Huang 2022af8a53 build: fix gradle script deprecations. 2021-08-20 15:58:44 +08:00
Haowei Wen b3564d9d27 fix #916 2021-08-20 07:58:06 +08:00
Haowei Wen 28d6f1f19a feat: press esc key to close dialog 2021-08-20 07:31:53 +08:00
Haowei Wen 588fe5cb5f fix: hide GameVersionList$1 task 2021-08-20 07:30:19 +08:00
Haowei Wen e138815c7b patch forge installer DOWNLOAD_MOJMAPS task to use mirrors 2021-08-20 07:30:19 +08:00
Haowei Wen 9bc10fb27d use separate tasks for forge installer processors 2021-08-20 07:30:19 +08:00
Haowei Wen 7d39d00a6a align add acount pane labels to left 2021-08-20 06:14:16 +08:00
Haowei Wen f448229e22 feat: search on enter in modpack search page 2021-08-20 04:48:22 +08:00
Haowei Wen d74d800d48 fix: trim modpack name when installing new modpack, fix #962 2021-08-20 03:12:58 +08:00
Haowei Wen 69ded66c92 fix: callExternalProcess() not displaying stderr 2021-08-19 02:11:14 +08:00
Haowei Wen 3c8d17b564 fix: files from maven.minecraftforge.net not downloaded from mirror 2021-08-19 00:47:37 +08:00
huanghongxun e61af71415 fix: cannot launch game with bootstraplauncher > 0.1.17. 2021-08-14 18:29:25 +08:00
huanghongxun 7d1f50519c fix: version.manage.manage.title not translated. Closes #956. 2021-08-14 17:03:32 +08:00
huanghongxun 6f5cc4d59d fix: crash in ModDownloadListPage. Closes #951. 2021-08-10 01:08:05 +08:00
huanghongxun 220cd144f5 fix: GameItem always show empty tag 2021-08-07 18:25:40 +08:00
huanghongxun 820c5a563a perf: slightly speed up VersionSettingsPage loading. 2021-08-07 18:14:51 +08:00
huanghongxun b2594122a8 feat: display mod description and website url. Closes #945. 2021-08-07 16:18:23 +08:00
huanghongxun 1ddcc23b89 feat: lazy initialize game mamangement popup menu. 2021-08-07 16:17:33 +08:00
huanghongxun d57a4655a6 perf: try to speed up css style lookup. 2021-08-07 14:30:55 +08:00
huanghongxun df4620518c fix: wrong thread. Closes #944. 2021-08-05 01:52:31 +08:00
huanghongxun b8ca9a3b09 fix: sort mod downloads. Closes #942. 2021-08-05 01:51:16 +08:00
Glavo 1ad8beb49e insert explicit 'System.gc()' call 2021-08-04 23:17:25 +08:00
huanghongxun 134f96be08 fix: ModDownloadListPage ListView should fill the blank. 2021-08-04 12:12:20 +08:00
huanghongxun cb03d9841d feat: delete button in version deleting dialog become red. 2021-08-04 01:54:45 +08:00
huanghongxun fe922e0bed fix: Check whether java 16 is chosen when launching 1.17 and javaVersion is not defined in version json. 2021-08-04 01:52:16 +08:00
huanghongxun 53c63db150 fix: ModListPage not loading mods. 2021-08-04 01:29:07 +08:00
huanghongxun 5ddb69077f feat: refine WorldListItem icons 2021-08-04 01:27:34 +08:00
huanghongxun 1fb9fa0af9 fix: icon of selected item should be highlighted. 2021-08-04 00:09:22 +08:00
huanghongxun d35fb9a486 fix: (modpack download): add default category. 2021-08-04 00:08:23 +08:00
Yuhui Huang 37fb9a0d65 feat: (modpack download): category & sort. 2021-08-03 22:47:53 +08:00
Yuhui Huang af7cf393dc feat: download mods and modpacks from CurseForge. 2021-08-03 22:07:19 +08:00
Yuhui Huang 7a20af462b feat: (WIP) Curse mod/modpack download 2021-08-02 23:11:30 +08:00
Yuhui Huang 0939ed819e feat: title bar swipe animation. 2021-08-02 16:46:45 +08:00
Yuhui Huang 5da4ad7a62 feat: refine GameListPage UI. 2021-08-02 16:16:21 +08:00
Yuhui Huang 123c863e3d feat: refine profile list UI. 2021-08-02 16:05:37 +08:00
Yuhui Huang 6fc0b13dc7 feat: refine UI icons. 2021-08-02 15:28:57 +08:00
Yuhui Huang 59bf8d3ebc style: fix checkstyle. 2021-08-02 15:09:34 +08:00
Yuhui Huang 805393b184 fix: GameListItem launch icon -> launchOutline icon. 2021-08-02 15:07:31 +08:00
Yuhui Huang 5d0d77aff4 style: fix checkstyle. 2021-08-02 15:03:39 +08:00
Yuhui Huang d2e4ddc876 fix: inconsistent svg icon size. 2021-08-02 15:02:54 +08:00
huanghongxun 5b7ca8c751 feat: redesign UI of game management. 2021-08-02 13:36:36 +08:00
huanghongxun 16c7558497 fix: active tab indicator animation aligned to Material Design standard. 2021-07-31 23:23:07 +08:00
huanghongxun 9b6b3f8938 fix: no longer validate Forge dependencies since provided checksums are wrong. Closes #913. 2021-07-31 02:10:04 +08:00
huanghongxun 331d8d1bb6 fix: does not remove the old library when updating the library 2021-07-31 02:02:06 +08:00
huanghongxun e5cca24740 fix: Version.jar becomes wrong value after resolvePreservingPatches 2021-07-31 02:01:17 +08:00
huanghongxun 6f40335322 feat: print more effective logs when OptiFine Patcher failed to patch game. 2021-07-31 02:00:13 +08:00
huanghongxun b6de9d4ea2 fix: failed to duplicate version when no jar 2021-07-31 00:56:26 +08:00
Yuhui Huang 5f8f01e0ab Revert "fix: Version.jar should be resolved to id of self if null instead of the id of ancestor.". Closes #934.
This reverts commit 5de150b06e.
2021-07-28 16:15:11 +08:00
xtex 0d69e944f6 fix: #930(Search new Java registry location first) 2021-07-25 17:29:44 +08:00
huanghongxun 1dc4e3f730 fix: 1.17 Forge auto installation 2021-07-25 17:29:00 +08:00
huanghongxun 14d1bccd14 fix: update condition check for whether the game is modded. 2021-07-25 15:58:22 +08:00
huanghongxun 5de150b06e fix: Version.jar should be resolved to id of self if null instead of the id of ancestor. 2021-07-25 01:21:08 +08:00
huanghongxun e34dd39b9a fix: malformed locale name. Closes #925. 2021-07-24 14:56:18 +08:00
Haowei Wen e7d5faeeac fix: DirectoryStream not closed
https://github.com/huanghongxun/HMCL/commit/dc3167472175d023878f9ae32230b93964588bc1#r53347194
2021-07-12 13:19:16 +08:00
huanghongxun 46a7269087 chore: ignore hmcl.json 2021-07-10 23:25:26 +08:00
huanghongxun dc31674721 fix: Do not throw exception when path to cached jvm installations does not exist. 2021-07-10 23:24:31 +08:00
Glavo afe6bc7456 用户 JVM 参数优先于默认参数 (#903)
* close #885: use the GC selected by user by default

* fix: default JVM args conflict with user args

* Options take precedence over JVM args

* compile huge methods by default

* update settings.advanced.jvm_args text

* Using JVM args suppression options

* update settings.advanced.jvm_args text
2021-07-10 23:10:04 +08:00
huanghongxun 378fd04463 feat: placeholders in user customized game arguments. Closes #920 2021-07-10 23:09:11 +08:00
huanghongxun 3e751ba5c6 chore: some comments on "libraries_directory" 2021-07-10 23:08:39 +08:00
Glavo 8fc1ec7bb8 替换对 JDK 内置 Pack200 的依赖,允许在 JDK14 及更高版本上构建项目 (#904)
* fix: Pack200 has been removed

* Automatically add pack200 to class path

* add license header to Pack200Utils.java
2021-07-10 22:44:13 +08:00
huanghongxun e5326a3c4d fix: switch to "library_directory" defined by official launcher 2021-07-10 21:22:57 +08:00
huanghongxun fe279234d8 feat: Support "invalid" hash code in download info 2021-07-10 21:16:34 +08:00
huanghongxun ef9b041310 feat: Support placeholders in version.json. Closes #923 2021-07-10 21:16:33 +08:00
Ghost_chu 9db576d4bc 修复在manifest中的因拼写问题造成的与文档不符的问题 (#921) 2021-07-09 13:02:17 +08:00
Haowei Wen a84971abfc update english translation 2021-06-24 21:05:30 +08:00
Haowei Wen a7a79db842 update copyright year in settings page 2021-06-24 21:00:38 +08:00
Haowei Wen d5da14fb1e remove welcome page (reduce ~300k size) 2021-06-24 20:43:29 +08:00
Haowei Wen 0d2e287f67 load welcome.png only when necessary
This saves ~200ms startup time.
2021-06-23 06:08:11 +08:00
Haowei Wen c329f1cafb log error when background can't be loaded 2021-06-23 04:57:01 +08:00
Haowei Wen ab1a2f6fcb fix: NPE in ModpackInfoPage 2021-06-15 00:34:50 +08:00
Haowei Wen 463dc56702 delete license-header.txt since it's not used 2021-06-14 19:12:38 +08:00
Haowei Wen 88e23cd0d2 feat: speed up openjfx dependencies checking
SHA-1 of dependencies are defined in openjfx-dependencies.json
to avoid network requests during startup.

openjfx-dependencies.json can be generated using
tools/generate-openjfx-dependencies.sh
2021-06-14 14:22:56 +08:00
Haowei Wen 9776aafab6 fix missing license in some files 2021-06-14 02:45:17 +08:00
Haowei Wen fbb5a03e5d feat: select authlib-injector server after added
* The most recently added server will be at the top.
* In Add Account dialog, after adding an authlib-injector server,
  it will be automatically selected.
2021-06-13 22:58:05 +08:00
huanghongxun 7c11ce53f3 fix: NPE. Closes #896. 2021-06-12 18:43:54 +08:00
huanghongxun 66f4c022d5 fix: ask user whether to download java16. 2021-06-12 18:43:53 +08:00
yaoxi-std 4b36450f9a changed language files, fixed #898 2021-06-12 16:53:24 +08:00
huanghongxun 36b58bd1bb feat: Support CurseForge modpack with Fabric as modloader. Closes #897. 2021-06-12 01:25:10 +08:00
Glavo 0296b6f73d fix: export modpack 2021-06-08 19:06:09 +08:00
huanghongxun 447afaa76f fix: typo 2021-05-30 03:39:29 +08:00
huanghongxun a0191aa7c2 fix: lint 2021-05-30 03:37:55 +08:00
huanghongxun e7c9fcf767 feat: checkstyle in GitHub Actions 2021-05-30 03:35:26 +08:00
huanghongxun fc017515c5 feat: check style & format 2021-05-30 03:34:44 +08:00
huanghongxun 6d75c4d34f chore: C++ google style 2021-05-30 02:42:39 +08:00
huanghongxun 258a6628e4 fix: Cannot start application when code page is 65001. Closes #814.
This commit may cause breaking change: changing working directory to the directory where the executable locates in.
2021-05-30 02:25:31 +08:00
huanghongxun 228695d511 fix: NPE 2021-05-29 17:22:06 +08:00
Glavo c2af530abb feat: lookup JRE installation in more places. 2021-05-29 17:18:56 +08:00
huanghongxun 69bc826e30 feat: BMCLAPI now supports downloading Java 16. 2021-05-29 17:17:00 +08:00
huanghongxun 5d0944ed19 feat: lookup JRE installation in more places. 2021-05-29 17:06:08 +08:00
寒兮 43f4da5dd3 fix: GITHUB_SHA build version NullPointException 2021-05-29 17:05:12 +08:00
寒兮 c6ab2cec35 fix: short commit version use lower case 2021-05-29 17:05:12 +08:00
寒兮 b0dc689ba7 fix: use GITHUB_SHA instead of the git command 2021-05-29 17:05:12 +08:00
寒兮 e11b1f37b9 chore: artifacts version use git short commit 2021-05-29 17:05:12 +08:00
huanghongxun 3e5fac4df6 feat: lookup JRE installation in more places. 2021-05-29 16:50:07 +08:00
huanghongxun e63fa40822 feat: run github actions on pull request 2021-05-29 16:49:24 +08:00
huanghongxun e01096bff0 fix: lint 2021-05-29 15:15:40 +08:00
yaoxi-std 2a0cfb1125 fixed bugs 2021-05-29 01:23:58 +08:00
yaoxi-std d4f0a04ccd change NativesDir into LaunchOptions 2021-05-29 01:23:58 +08:00
yaoxi-std 7cbe24b35b add: allow customize LWJGL path (runs on M1) 2021-05-29 01:23:58 +08:00
huanghongxun b7da21e5ce fix: not select downloaded Java automatically. Closes #886. 2021-05-27 00:17:33 +08:00
Glavo 0ae1411f0a fix #861: Unable open zipfs for signed exe file 2021-05-25 15:05:42 +08:00
huanghongxun 66889cac27 chore: version 3.3 2021-05-25 13:25:43 +08:00
huanghongxun 67bd2ac386 fix: typo 2021-05-25 13:23:09 +08:00
huanghongxun 1f278c2a49 feat: publish unsigned artifacts by GitHub Actions 2021-05-25 13:20:33 +08:00
huanghongxun 1674f1828f fix: hmclauncher 2021-05-25 13:14:43 +08:00
huanghongxun 472a0a039f fix: fuck lfs 2021-05-25 13:13:33 +08:00
935 changed files with 114442 additions and 16617 deletions

No files matched your search

+1117
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
*.exe filter=lfs diff=lfs merge=lfs -text
-28
View File
@@ -1,28 +0,0 @@
# 公告
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
**如果你在阅读完本公告后仍然希望在此处发布 issue,请将公告内容删去,并按照下面的模板填入相关信息。**
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 HMCL 3 用户 QQ 群:219177735 中反馈信息,**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
# 问题提交
*完整地填下面的问题提交表对我们很重要,这可以加快我们分析问题原因的速度。*
* 启动器版本:
* 操作系统:
* Java 版本:
* 错误截图(最好请将整个电脑屏幕的截图发上来):
* 游戏版本(如果是启动通过启动器自带的自动安装功能安装的游戏):
* 对游戏做的修改(是否自行通过安装器安装 Rift 等 API,以及 mod):
* 游戏崩溃报告(如果有):
* 启动器崩溃报告(如果有):
* 启动器日志文件(出现问题后点击启动器设置中的【导出启动器日志】按钮):
* 问题描述(如何触发问题):
+49
View File
@@ -0,0 +1,49 @@
name: Bug 反馈 | Bug Report
description:
反馈一个 HMCL 错误。| File a bug report for HMCL.
title: "[Bug] "
labels: bug
body:
- type: markdown
attributes:
value: |
提交前请确认:
* 该问题确实是 **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)/[Discord 服务器](https://discord.gg/jVvC7HfM6U) 获取帮助。
Before submitting, please confirm:
* The issue is indeed **a bug 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 server](https://discord.gg/jVvC7HfM6U) for help.
* Your launcher is the **latest nightly build**. You can click [here](https://zkitefly.github.io/HMCL-Snapshot-Update/en) to download the latest nightly build.
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 server](https://discord.gg/jVvC7HfM6U) for help.
- type: textarea
id: bug-report
attributes:
label: 问题描述 | Bug Description
description: |
请尽可能地详细描述你所遇到的问题,并描述如何重新触发这个问题。
Please describe the bug you met in as much detail as possible. Additionally, describe the steps to reproduce this bug.
placeholder: |
1. 点击 HMCL 上的某个按钮 | Click a button named ...
2. 向下翻页 | Scroll down
3. ...
validations:
required: true
- type: textarea
id: hmcl-crash-report-or-logs
attributes:
label: 启动器崩溃报告 / 启动器日志文件 | Launcher Crash Report / Launcher Log File
description: |
如果你的启动器崩溃了,请将崩溃报告填入(或将文件拖入)下方。
如果你的启动器没有崩溃,请在遇到问题后**不要退出启动器**,在启动器的 “设置 → 通用 → 调试” 一栏中点击 “导出启动器日志”,并将导出的日志拖到下方的输入栏中。
**请注意:启动器崩溃报告或日志文件是诊断问题的重要依据,请务必上传!**
If your launcher crashes, please fill in (or drag the file in) the following input field with the crash report.
If your launcher does not crash, please DO NOT EXIT your launcher, click "Export Launcher Logs" in the "Settings → General → Debug" of the launcher, and drag the exported log to the following input field.
**ATTENTION: The crash report or log file is the key to resolving the bug. Please upload them!**
validations:
required: true
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: QQ 群 | QQ Group
url: https://docs.hmcl.net/groups.html
about: Hello Minecraft! Launcher 的官方 QQ 交流群。| The official QQ group of Hello Minecraft! Launcher.
- name: Discord 服务器 | Discord Server
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 服务器。| The official Discord server of Hello Minecraft! Launcher.
- name: 其他反馈 | Others
url: https://github.com/HMCL-dev/HMCL/discussions/new/choose
about: 通过 Discussions 反馈其他问题。| Report other problems in Discussions.
+35
View File
@@ -0,0 +1,35 @@
name: 新功能 | Feature Request
description: 为 HMCL 提出新功能。| Suggest a new feature or enhancement for HMCL.
title: "[Feature] "
labels: enhancement
body:
- type: markdown
attributes:
value: |
请确认 Issues 列表无重复的项目。
Please make sure that no duplicate issues have already been submitted.
- type: textarea
id: summary
attributes:
label: 概述 | Summary
description: |
请介绍你想加入的新功能。
Please describe the new feature.
validations:
required: true
- type: textarea
id: reason
attributes:
label: 原因 | Reason
description: |
请描述该功能带来的好处及原因。
Please describe why you want to add the feature or enhancement to HMCL.
validations:
required: true
- type: textarea
id: description
attributes:
label: 详情 | Description
description: |
在这里可以补充描述该功能的具体实现方式或建议。(可选)
Describe implementation details or suggestions here. (Optional)
+25
View File
@@ -0,0 +1,25 @@
name: Check Style
on:
push:
paths:
- '**.java'
pull_request:
paths:
- '**.java'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Check style main
run: ./gradlew checkstyleMain --no-daemon
- name: Check style test
run: ./gradlew checkstyleTest --no-daemon
+23
View File
@@ -0,0 +1,23 @@
name: Check Translations
on:
push:
paths:
- '**.properties'
pull_request:
paths:
- '**.properties'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Check style test
run: ./gradlew checkTranslations --no-daemon
+141
View File
@@ -0,0 +1,141 @@
name: Check Update
on:
workflow_dispatch:
schedule:
- cron: '30 * * * *'
permissions:
contents: write
jobs:
dev-check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fetch tags
run: git fetch --all --tags
- name: Install tools
run: sudo apt-get install -y jq
- name: Fetch last version
run: |
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
- name: Check for existing tags
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
if: ${{ env.continue == 'true' }}
run: |
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
env:
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/artifact/HMCL/build/libs
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/dev.html" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "*Notice: changelogs are written in Chinese.*" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
- name: Create release
if: ${{ env.continue == 'true' }}
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTE
files: |
HMCL-${{ env.HMCL_VERSION }}.exe
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.sh
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
prerelease: true
stable-check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
needs: dev-check-update
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Fetch tags
run: git fetch --all --tags
- name: Install tools
run: sudo apt-get install -y jq
- name: Fetch last version
run: |
wget -O ci.json https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/api/json
export HMCL_EXE_FILE_NAME=`cat ci.json | jq -M -r '.artifacts[] | select(.fileName | endswith(".exe")) | .fileName'`
if [ -z `echo $HMCL_EXE_FILE_NAME | grep -E "^HMCL-[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?\.exe\$"` ]; then exit 1; fi
export HMCL_VERSION=`echo "${HMCL_EXE_FILE_NAME%.exe}" | tail -c +6`
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
echo "HMCL_TAG_NAME=release-$HMCL_VERSION" >> $GITHUB_ENV
- name: Check for existing tags
run: if ! git tag -l | grep -q "$HMCL_TAG_NAME"; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
if: ${{ env.continue == 'true' }}
run: |
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha256"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha256"
env:
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/artifact/HMCL/build/libs
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
echo "**This version is a stable version.**" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/stable.html" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "*Notice: changelogs are written in Chinese.*" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/release-$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
- name: Create release
if: ${{ env.continue == 'true' }}
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTE
files: |
HMCL-${{ env.HMCL_VERSION }}.exe
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.sh
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
+21
View File
@@ -0,0 +1,21 @@
name: Sync to Gitee
on:
push
jobs:
run:
if: ${{ github.repository_owner == 'HMCL-dev' }}
runs-on: ubuntu-latest
steps:
- name: Mirror GitHub to Gitee
uses: Yikun/hub-mirror-action@v1.4
with:
src: github/HMCL-dev
dst: gitee/huanghongxun
static_list: 'HMCL'
force_update: true
debug: true
dst_key: ${{ secrets.GITEE_SYNC_BOT_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_SYNC_BOT_TOKEN }}
cache_path: /github/workspace/hub-mirror-cache
+23 -7
View File
@@ -1,6 +1,10 @@
name: Java CI
on: [push]
on:
push:
pull_request:
paths-ignore:
- '**.md'
jobs:
build:
@@ -8,11 +12,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v1
- uses: actions/checkout@v4
- name: Set up JDK 11
uses: actions/setup-java@v4
with:
java-version: 1.8
java-package: jdk+fx
distribution: 'zulu'
java-version: '11'
java-package: 'jdk+fx'
- name: Build with Gradle
run: ./gradlew build
run: ./gradlew build --no-daemon
env:
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
- name: Get short SHA
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: HMCL-${{ env.SHORT_SHA }}
path: HMCL/build/libs
+18 -4
View File
@@ -10,27 +10,30 @@ hs_err_pid*
*.2
*.log
.mine*
/externalgames
NVIDIA
*.json
minecraft-exported-crash-info*
# gradle build
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/buildSrc/build/
# idea
.idea
/out/
/HMCL/out/
/HMCLCore/out/
/HMCLTransformerDiscoveryService/out/
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
/HMCLTransformerDiscoveryService/bin/
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
.classpath
.project
.settings
@@ -38,4 +41,15 @@ NVIDIA
# netbeans
.nb-gradle
*.exe
*.exe
# macos
.DS_Store
# vscode
.vscode/
# test
/hmcl.json
/.hmcl.json
/.hmcl/
+4
View File
@@ -0,0 +1,4 @@
/data.csv
/data.json
/mod.json
/modpack.json
-14
View File
@@ -1,14 +0,0 @@
language: java
jdk:
- oraclejdk8
branches:
only:
- master
before_install:
- chmod +x gradlew
script: "bash ./gradlew clean build --stacktrace"
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
cache:
directories:
- "$HOME/.gradle/caches/"
-210
View File
@@ -1,210 +0,0 @@
buildscript {
repositories {
gradlePluginPortal()
}
dependencies {
classpath 'org.tukaani:xz:1.8'
}
}
plugins {
id 'application'
id 'com.github.johnrengelman.shadow' version '7.0.0'
}
import java.nio.file.FileSystems
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.util.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.jar.Pack200
import java.util.zip.GZIPOutputStream
import java.util.zip.ZipFile
import java.nio.file.Files
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZOutputStream
def buildnumber = System.getenv("BUILD_NUMBER") ?: "SNAPSHOT"
if (System.getenv("BUILD_NUMBER") != null && System.getenv("BUILD_NUMBER_OFFSET") != null)
buildnumber = (Integer.parseInt(System.getenv("BUILD_NUMBER")) - Integer.parseInt(System.getenv("BUILD_NUMBER_OFFSET"))).toString()
def versionroot = System.getenv("VERSION_ROOT") ?: "3.2"
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
implementation project(":HMCLCore")
implementation rootProject.files("lib/JFoenix.jar")
}
def digest(String algorithm, byte[] bytes) {
return MessageDigest.getInstance(algorithm).digest(bytes)
}
def createChecksum(File file) {
def algorithm = "SHA-1"
def suffix = "sha1"
new File(file.parentFile, file.name + "." + suffix).text = digest(algorithm, file.bytes).encodeHex().toString() + "\n"
}
def attachSignature(File jar) {
def keyLocation = System.getenv("HMCL_SIGNATURE_KEY");
if (keyLocation == null)
return
def privatekey = KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(new File(keyLocation).bytes))
def signer = Signature.getInstance("SHA512withRSA")
signer.initSign(privatekey)
new ZipFile(jar).withCloseable { zip ->
zip.stream()
.sorted(Comparator.comparing { it.name })
.filter { it.name != "META-INF/hmcl_signature" }
.forEach {
signer.update(digest("SHA-512", it.name.getBytes("UTF-8")))
signer.update(digest("SHA-512", zip.getInputStream(it).bytes))
}
}
def signature = signer.sign()
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), [:]).withCloseable { zipfs ->
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).withCloseable { it << signature }
}
}
ext.packer = Pack200.newPacker()
packer.properties()["pack.effort"] = "9"
ext.unpacker = Pack200.newUnpacker()
// Pack200 does not guarantee that unpacked .class file is bit-wise same as the .class file before packing
// because of shrinking. So we should pack .class files and unpack it to make sure that after unpacking
// .class files remain the same.
def repack(File file) {
def packed = new ByteArrayOutputStream()
new JarFile(file).withCloseable { packer.pack(it, packed) }
new JarOutputStream(file.newOutputStream()).withCloseable { unpacker.unpack(new ByteArrayInputStream(packed.toByteArray()), it) }
}
sourceSets {
java11 {
java {
srcDirs = ['src/main/java11']
}
}
}
compileJava11Java {
javaCompiler = javaToolchains.compilerFor {
languageVersion = JavaLanguageVersion.of(11)
}
options.compilerArgs.add('--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED')
sourceCompatibility = 11
targetCompatibility = 11
}
jar {
enabled = false
dependsOn shadowJar
}
shadowJar {
classifier = null
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2020 huangyuhui.',
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': version,
'Add-Opens': [
'java.base/java.lang',
'java.base/java.lang.reflect',
'javafx.graphics/javafx.css',
'javafx.base/com.sun.javafx.runtime',
'javafx.controls/com.sun.javafx.scene.control.behavior',
'javafx.controls/javafx.scene.control.skin',
'javafx.controls/com.sun.javafx.scene.control',
'javafx.base/com.sun.javafx.binding',
'javafx.base/com.sun.javafx.event',
'javafx.graphics/com.sun.javafx.stage'
].join(" "),
'Add-Exports': [
'java.base/jdk.internal.loader',
'javafx.controls/com.sun.javafx.scene.control.behavior',
'javafx.controls/javafx.scene.control.skin',
'javafx.controls/com.sun.javafx.scene.control',
'javafx.base/com.sun.javafx.binding',
'javafx.graphics/com.sun.javafx.stage',
'javafx.base/com.sun.javafx.event'
].join(" ")
}
doLast {
repack(jar.archivePath) // see repack()
attachSignature(jar.archivePath)
createChecksum(jar.archivePath)
}
}
def createExecutable(String suffix, String header) {
def output = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + suffix)
output.bytes = new File(project.projectDir, header).bytes
output << jar.archivePath.bytes
createChecksum(output)
}
processResources {
ext.convertToBSS = { String resource ->
// exclude resource
doFirst {
def cssFile = new File(this.projectDir, "src/main/resources/" + resource)
def bssFile = new File(this.projectDir, "build/compiled-resources/" + resource[0..-4] + "bss")
bssFile.parentFile.mkdirs()
exec {
commandLine 'javapackager', '-createbss', '-outdir', bssFile.parent, '-srcfiles', cssFile.path
}
}
}
from "build/compiled-resources"
convertToBSS "assets/css/root.css"
convertToBSS "assets/css/blue.css"
into('META-INF/versions/11') {
from sourceSets.java11.output
}
dependsOn java11Classes
}
task makePack(dependsOn: jar) {
ext.outputPath = new File(jar.archivePath.parentFile, jar.archivePath.name[0..-4] + "pack")
doLast {
outputPath.newOutputStream().withCloseable { out ->
new JarFile(jar.archivePath).withCloseable { jarFile -> packer.pack(jarFile, out) }
}
createChecksum(outputPath)
}
}
task makePackXz(dependsOn: makePack) doLast {
def packXz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".xz")
// Our CI server does not have enough memory space to compress file at highest level.
new XZOutputStream(packXz.newOutputStream(), new LZMA2Options(5)).withCloseable { it << makePack.outputPath.bytes }
createChecksum(packXz)
}
task makePackGz(dependsOn: makePack) doLast {
def packGz = new File(makePack.outputPath.parentFile, makePack.outputPath.name + ".gz")
new GZIPOutputStream(packGz.newOutputStream()).withCloseable { it << makePack.outputPath.bytes }
createChecksum(packGz)
}
task makeExecutables(dependsOn: jar) doLast {
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
}
build.dependsOn makePackXz
build.dependsOn makePackGz
build.dependsOn makeExecutables
+312
View File
@@ -0,0 +1,312 @@
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.Signature
import java.security.spec.PKCS8EncodedKeySpec
import java.util.zip.ZipFile
plugins {
alias(libs.plugins.shadow)
}
val projectConfig = PropertiesUtils.load(rootProject.file("config/project.properties").toPath())
val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt()
if (buildNumber != null) {
version = if (JenkinsUtils.IS_ON_CI && versionType == "dev") {
"$versionRoot.0.$buildNumber"
} else {
"$versionRoot.$buildNumber"
}
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
version = if (shortCommit.isNullOrBlank()) {
"$versionRoot.SNAPSHOT"
} else if (isOfficial) {
"$versionRoot.dev-$shortCommit"
} else {
"$versionRoot.unofficial-$shortCommit"
}
}
dependencies {
implementation(project(":HMCLCore"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.java.info)
if (launcherExe.isBlank()) {
implementation(libs.hmclauncher)
}
}
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
fun createChecksum(file: File) {
val algorithms = linkedMapOf(
"SHA-1" to "sha1",
"SHA-256" to "sha256",
"SHA-512" to "sha512"
)
algorithms.forEach { (algorithm, ext) ->
File(file.parentFile, "${file.name}.$ext").writeText(
digest(algorithm, file.readBytes()).joinToString(separator = "", postfix = "\n") { "%02x".format(it) }
)
}
}
fun attachSignature(jar: File) {
val keyLocation = System.getenv("HMCL_SIGNATURE_KEY")
if (keyLocation == null) {
logger.warn("Missing signature key")
return
}
val privatekey = KeyFactory.getInstance("RSA").generatePrivate(PKCS8EncodedKeySpec(File(keyLocation).readBytes()))
val signer = Signature.getInstance("SHA512withRSA")
signer.initSign(privatekey)
ZipFile(jar).use { zip ->
zip.stream()
.sorted(Comparator.comparing { it.name })
.filter { it.name != "META-INF/hmcl_signature" }
.forEach {
signer.update(digest("SHA-512", it.name.toByteArray()))
signer.update(digest("SHA-512", zip.getInputStream(it).readBytes()))
}
}
val signature = signer.sign()
FileSystems.newFileSystem(URI.create("jar:" + jar.toURI()), emptyMap<String, Any>()).use { zipfs ->
Files.newOutputStream(zipfs.getPath("META-INF/hmcl_signature")).use { it.write(signature) }
}
}
val java11 = sourceSets.create("java11") {
java {
srcDir("src/main/java11")
}
}
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
sourceCompatibility = "11"
targetCompatibility = "11"
}
tasks.jar {
enabled = false
dependsOn(tasks["shadowJar"])
}
val jarPath = tasks.jar.get().archiveFile.get().asFile
tasks.shadowJar {
archiveClassifier.set(null as String?)
exclude("**/package-info.class")
exclude("META-INF/maven/**")
exclude("META-INF/services/javax.imageio.spi.ImageReaderSpi")
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
listOf(
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*","freebsd-*", "linux-*", "darwin-*",
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
).forEach { exclude("com/sun/jna/$it/**") }
minimize {
exclude(dependency("com.google.code.gson:.*:.*"))
exclude(dependency("net.java.dev.jna:jna:.*"))
exclude(dependency("libs:JFoenix:.*"))
}
manifest {
attributes(
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
"Implementation-Version" to project.version,
"Microsoft-Auth-Id" to microsoftAuthId,
"Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"Build-Channel" to versionType,
"Add-Opens" to listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
"java.base/jdk.internal.loader",
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
"javafx.graphics/javafx.css",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
).joinToString(" "),
"Enable-Native-Access" to "ALL-UNNAMED"
)
System.getenv("GITHUB_SHA")?.also {
attributes("GitHub-SHA" to it)
}
}
if (launcherExe.isNotBlank()) {
into("assets") {
from(file(launcherExe))
}
}
doLast {
attachSignature(jarPath)
createChecksum(jarPath)
}
}
tasks.processResources {
into("META-INF/versions/11") {
from(sourceSets["java11"].output)
}
dependsOn(tasks["java11Classes"])
}
val makeExecutables by tasks.registering {
val extensions = listOf("exe", "sh")
dependsOn(tasks.jar)
inputs.file(jarPath)
outputs.files(extensions.map { File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + it) })
doLast {
val jarContent = jarPath.readBytes()
ZipFile(jarPath).use { zipFile ->
for (extension in extensions) {
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + extension)
val entry = zipFile.getEntry("assets/HMCLauncher.$extension")
?: throw GradleException("HMCLauncher.$extension not found")
output.outputStream().use { outputStream ->
zipFile.getInputStream(entry).use { it.copyTo(outputStream) }
outputStream.write(jarContent)
}
createChecksum(output)
}
}
}
}
tasks.build {
dependsOn(makeExecutables)
}
fun parseToolOptions(options: String?): MutableList<String> {
if (options == null)
return mutableListOf()
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.register<JavaExec>("run") {
dependsOn(tasks.jar)
group = "application"
classpath = files(jarPath)
workingDir = rootProject.rootDir
val vmOptions = parseToolOptions(System.getenv("HMCL_JAVA_OPTS"))
if (vmOptions.none { it.startsWith("-Dhmcl.offline.auth.restricted=") })
vmOptions += "-Dhmcl.offline.auth.restricted=false"
jvmArgs(vmOptions)
val hmclJavaHome = System.getenv("HMCL_JAVA_HOME")
if (hmclJavaHome != null) {
this.executable(
file(hmclJavaHome).resolve("bin")
.resolve(if (System.getProperty("os.name").lowercase().startsWith("windows")) "java.exe" else "java")
)
}
doFirst {
logger.quiet("HMCL_JAVA_OPTS: {}", vmOptions)
logger.quiet("HMCL_JAVA_HOME: {}", hmclJavaHome ?: System.getProperty("java.home"))
}
}
// mcmod data
tasks.register<ParseModDataTask>("parseModData") {
inputFile.set(layout.projectDirectory.file("mod.json"))
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/mod_data.txt"))
}
tasks.register<ParseModDataTask>("parseModPackData") {
inputFile.set(layout.projectDirectory.file("modpack.json"))
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/modpack_data.txt"))
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 -28.5 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M216.856339,16.5966031 C200.285002,8.84328665 182.566144,3.2084988 164.041564,0 C161.766523,4.11318106 159.108624,9.64549908 157.276099,14.0464379 C137.583995,11.0849896 118.072967,11.0849896 98.7430163,14.0464379 C96.9108417,9.64549908 94.1925838,4.11318106 91.8971895,0 C73.3526068,3.2084988 55.6133949,8.86399117 39.0420583,16.6376612 C5.61752293,67.146514 -3.4433191,116.400813 1.08711069,164.955721 C23.2560196,181.510915 44.7403634,191.567697 65.8621325,198.148576 C71.0772151,190.971126 75.7283628,183.341335 79.7352139,175.300261 C72.104019,172.400575 64.7949724,168.822202 57.8887866,164.667963 C59.7209612,163.310589 61.5131304,161.891452 63.2445898,160.431257 C105.36741,180.133187 151.134928,180.133187 192.754523,160.431257 C194.506336,161.891452 196.298154,163.310589 198.110326,164.667963 C191.183787,168.842556 183.854737,172.420929 176.223542,175.320965 C180.230393,183.341335 184.861538,190.991831 190.096624,198.16893 C211.238746,191.588051 232.743023,181.531619 254.911949,164.955721 C260.227747,108.668201 245.831087,59.8662432 216.856339,16.5966031 Z M85.4738752,135.09489 C72.8290281,135.09489 62.4592217,123.290155 62.4592217,108.914901 C62.4592217,94.5396472 72.607595,82.7145587 85.4738752,82.7145587 C98.3405064,82.7145587 108.709962,94.5189427 108.488529,108.914901 C108.508531,123.290155 98.3405064,135.09489 85.4738752,135.09489 Z M170.525237,135.09489 C157.88039,135.09489 147.510584,123.290155 147.510584,108.914901 C147.510584,94.5396472 157.658606,82.7145587 170.525237,82.7145587 C183.391518,82.7145587 193.761324,94.5189427 193.539891,108.914901 C193.539891,123.290155 183.391518,135.09489 170.525237,135.09489 Z" fill="#5865F2" fill-rule="nonzero">
␍</path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 -0.5 52 56" shape-rendering="crispEdges" xmlns="http://www.w3.org/2000/svg"><path stroke="#38342a" d="M28 0h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m4 0h4M24 5h4m4 0h4M24 6h4m4 0h4M24 7h4m4 0h4M24 8h4m8 0h4M24 9h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-20 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-24 1h4m4 0h4m8 0h4m-28 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-32 1h4m12 0h4m8 0h4m-36 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8m-40 1h4m20 0h4m4 0h8M8 24h4m28 0h4m4 0h4M8 25h4m28 0h4m4 0h4M8 26h4m28 0h4m4 0h4M8 27h4m28 0h4m4 0h4M4 28h4m32 0h8M4 29h4m32 0h8M4 30h4m32 0h8M4 31h4m32 0h8M0 32h4m32 0h4M0 33h4m32 0h4M0 34h4m32 0h4M0 35h4m32 0h4M0 36h4m28 0h4M0 37h4m28 0h4M0 38h4m28 0h4M0 39h4m28 0h4M4 40h4m20 0h4M4 41h4m20 0h4M4 42h4m20 0h4M4 43h4m20 0h4M8 44h4m12 0h4M8 45h4m12 0h4M8 46h4m12 0h4M8 47h4m12 0h4m-16 1h4m4 0h8m-16 1h4m4 0h8m-16 1h4m4 0h8m-16 1h4m4 0h8m-12 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#dbd0b4" d="M28 4h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m-12 1h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-16 1h4m8 0h4m-16 1h8m8 0h4m-20 1h8m8 0h4m-20 1h8m8 0h4m-20 1h8m8 0h4m-28 1h8m4 0h8m-20 1h8m4 0h8m-20 1h8m4 0h8m-20 1h8m4 0h8m-24 1h16m-16 1h16m-16 1h16m-16 1h16M8 28h8m4 0h12M8 29h8m4 0h12M8 30h8m4 0h12M8 31h8m4 0h12M8 32h12m8 0h4M8 33h12m8 0h4M8 34h12m8 0h4M8 35h12m8 0h4m-20 1h12m-12 1h12m-12 1h12m-12 1h12m-8 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#c6bca5" d="M28 8h4m-4 1h4m-4 1h4m-4 1h4m-12 5h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h8m-8 1h8m-8 1h8m-8 1h8m-20 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h8m-8 1h8m-8 1h8m-8 1h8"/><path stroke="#bcb29c" d="M32 12h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h8m-8 1h8m-8 1h8m-8 1h8m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-12 1h8m-8 1h8m-8 1h8m-8 1h8m-8 1h4m-4 1h4m-4 1h4m-4 1h4m-8 1h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#807a6d" d="M44 24h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#aea694" d="M4 32h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4"/><path stroke="#9a927e" d="M4 36h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4m0 1h4m-4 1h4m-4 1h4m-4 1h4"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

-2
View File
@@ -1,2 +0,0 @@
rootProject.name = 'HMCL'
@@ -19,33 +19,46 @@ package org.jackhuang.hmcl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.*;
import java.lang.management.MemoryPoolMXBean;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedList;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class Launcher extends Application {
@@ -57,12 +70,47 @@ public final class Launcher extends Application {
CookieHandler.setDefault(COOKIE_MANAGER);
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.warning("Failed to get prism pipeline", e);
}
try {
try {
ConfigHolder.init();
} catch (SambaException ignored) {
Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to load config", e);
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
LOG.error("Failed to load config", e);
checkConfigInTempDir();
checkConfigOwner();
Main.showErrorAndExit(i18n("fatal.config_loading_failure", ConfigHolder.configLocation().getParent()));
}
// https://lapcatsoftware.com/articles/app-translocation.html
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& ConfigHolder.isNewlyCreated()
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
return;
} else {
checkConfigInTempDir();
}
if (ConfigHolder.isOwnerChanged()) {
if (showAlert(AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
return;
}
if (ConfigHolder.isUnsupportedVersion()) {
showAlert(AlertType.WARNING, i18n("fatal.config_unsupported_version"));
}
if (Metadata.HMCL_CURRENT_DIRECTORY.toString().indexOf('=') >= 0) {
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
}
// runLater to ensure ConfigHolder.init() finished initialization
@@ -72,8 +120,6 @@ public final class Launcher extends Application {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
initIcon();
UpdateChecker.init();
primaryStage.show();
@@ -83,20 +129,103 @@ public final class Launcher extends Application {
}
}
@Override
public void stop() throws Exception {
super.stop();
Controllers.onApplicationStop();
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
}
private void initIcon() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
AwtUtils.setAppleIcon(image);
private static boolean isConfigInTempDir() {
String configPath = ConfigHolder.configLocation().toString();
String tmpdir = System.getProperty("java.io.tmpdir");
if (StringUtils.isNotBlank(tmpdir) && configPath.startsWith(tmpdir))
return true;
String[] tempFolderNames = {"Temp", "Cache", "Caches"};
for (String name : tempFolderNames) {
if (configPath.contains(File.separator + name + File.separator))
return true;
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
return configPath.contains("\\Temporary Internet Files\\")
|| configPath.contains("\\INetCache\\")
|| configPath.contains("\\$Recycle.Bin\\")
|| configPath.contains("\\recycler\\");
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
return configPath.startsWith("/tmp/")
|| configPath.startsWith("/var/tmp/")
|| configPath.startsWith("/var/cache/")
|| configPath.startsWith("/dev/shm/")
|| configPath.contains("/Trash/");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
return configPath.startsWith("/var/folders/")
|| configPath.startsWith("/private/var/folders/")
|| configPath.startsWith("/tmp/")
|| configPath.startsWith("/private/tmp/")
|| configPath.startsWith("/var/tmp/")
|| configPath.startsWith("/private/var/tmp/")
|| configPath.contains("/.Trash/");
} else {
return false;
}
}
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
Main.exit(0);
}
}
private static void checkConfigOwner() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
return;
String userName = System.getProperty("user.name");
String owner;
try {
owner = Files.getOwner(ConfigHolder.configLocation()).getName();
} catch (IOException ioe) {
LOG.warning("Failed to get file owner", ioe);
return;
}
if (Files.isWritable(ConfigHolder.configLocation()) || userName.equals("root") || userName.equals(owner))
return;
ArrayList<String> files = new ArrayList<>();
files.add(ConfigHolder.configLocation().toString());
if (Files.exists(Metadata.HMCL_GLOBAL_DIRECTORY))
files.add(Metadata.HMCL_GLOBAL_DIRECTORY.toString());
if (Files.exists(Metadata.HMCL_CURRENT_DIRECTORY))
files.add(Metadata.HMCL_CURRENT_DIRECTORY.toString());
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
if (Files.exists(mcDir))
files.add(mcDir.toString());
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
if (showAlert(AlertType.ERROR,
i18n("fatal.config_loading_failure.unix", owner, command),
copyAndExit, ButtonType.CLOSE) == copyAndExit) {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
Main.exit(1);
}
@Override
public void stop() throws Exception {
Controllers.onApplicationStop();
FileSaver.shutdown();
LOG.shutdown();
}
public static void main(String[] args) {
if (UpdateHandler.processArguments(args)) {
LOG.shutdown();
return;
}
@@ -105,17 +234,47 @@ public final class Launcher extends Application {
try {
LOG.info("*** " + Metadata.TITLE + " ***");
LOG.info("Operating System: " + System.getProperty("os.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 + ')'));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
}
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName());
LOG.info("Native Encoding: " + OperatingSystem.NATIVE_CHARSET);
LOG.info("JNU Encoding: " + System.getProperty("sun.jnu.encoding"));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
LOG.info("Code Page: " + OperatingSystem.CODE_PAGE);
}
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName());
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
LOG.info("Current Directory: " + Metadata.CURRENT_DIRECTORY);
LOG.info("HMCL Global Directory: " + Metadata.HMCL_GLOBAL_DIRECTORY);
LOG.info("HMCL Current Directory: " + Metadata.HMCL_CURRENT_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("JVM Max Memory: " + MEGABYTES.formatBytes(Runtime.getRuntime().maxMemory()));
try {
for (MemoryPoolMXBean bean : ManagementFactory.getMemoryPoolMXBeans()) {
if ("Metaspace".equals(bean.getName())) {
long bytes = bean.getUsage().getUsed();
LOG.info("Metaspace: " + MEGABYTES.formatBytes(bytes));
break;
}
}
} catch (NoClassDefFoundError ignored) {
}
LOG.info("Native Backend: " + (NativeUtils.USE_JNA ? "JNA" : "None"));
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
LOG.info("XDG Session Type: " + System.getenv("XDG_SESSION_TYPE"));
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
}
launch(args);
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
launch(Launcher.class, args);
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
}
@@ -143,33 +302,9 @@ public final class Launcher extends Application {
Controllers.getStage().close();
Schedulers.shutdown();
Controllers.shutdown();
Lang.executeDelayed(OperatingSystem::forceGC, TimeUnit.SECONDS, 5, true);
Lang.executeDelayed(System::gc, TimeUnit.SECONDS, 5, true);
});
}
public static List<File> getCurrentJarFiles() {
List<File> result = new LinkedList<>();
if (Launcher.class.getClassLoader() instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) Launcher.class.getClassLoader()).getURLs();
for (URL u : urls)
try {
File f = new File(u.toURI());
if (f.isFile() && (f.getName().endsWith(".exe") || f.getName().endsWith(".jar")))
result.add(f);
} catch (URISyntaxException e) {
return null;
}
} else {
File jarFile = new File(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath());
String ext = FileUtils.getExtension(jarFile);
if ("jar".equals(ext) || "exe".equals(ext))
result.add(jarFile);
}
if (result.isEmpty())
return null;
else
return result;
}
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
}
+149 -44
View File
@@ -17,14 +17,23 @@
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.Logging;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.ModuleHelper;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.*;
import javax.swing.*;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -33,39 +42,95 @@ import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.logging.Level;
import java.util.concurrent.CancellationException;
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 {
private Main() {
}
public static void main(String[] args) {
System.setProperty("java.net.useSystemProxies", "true");
System.setProperty("http.agent", "HMCL/" + Metadata.VERSION);
System.setProperty("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU")))
System.getProperties().putIfAbsent("prism.forceGPU", "true");
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
if (animationFrameRate != null) {
try {
if (Integer.parseInt(animationFrameRate) <= 0)
throw new NumberFormatException(animationFrameRate);
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
} catch (NumberFormatException e) {
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
}
}
checkDirectoryPath();
// This environment check will take ~300ms
thread(() -> {
fixLetsEncrypt();
checkDSTRootCAX3();
}, "CA Certificate Check", true);
if (JavaRuntime.CURRENT_VERSION < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
initIcon();
checkJavaFX();
// Fix title bar not displaying in GTK systems
System.setProperty("jdk.gtk.version", "2");
verifyJavaFX();
addEnableNativeAccess();
enableUnsafeMemoryAccess();
Launcher.main(args);
}
public static void exit(int exitCode) {
FileSaver.shutdown();
LOG.shutdown();
System.exit(exitCode);
}
private static void createHMCLDirectories() {
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
try {
Files.createDirectories(Metadata.HMCL_CURRENT_DIRECTORY);
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(Metadata.HMCL_CURRENT_DIRECTORY, "dos:hidden", true);
} catch (IOException e) {
LOG.warning("Failed to set hidden attribute of " + Metadata.HMCL_CURRENT_DIRECTORY, e);
}
}
} catch (IOException e) {
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.create_hmcl_current_directory_failure", Metadata.HMCL_CURRENT_DIRECTORY));
}
}
if (!Files.isDirectory(Metadata.HMCL_GLOBAL_DIRECTORY)) {
try {
Files.createDirectories(Metadata.HMCL_GLOBAL_DIRECTORY);
} catch (IOException e) {
LOG.warning("Failed to create HMCL global directory " + Metadata.HMCL_GLOBAL_DIRECTORY, e);
}
}
}
private static void initIcon() {
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon-mac.png"));
AwtUtils.setAppleIcon(image);
}
private static void checkDirectoryPath() {
String currentDirectory = new File("").getAbsolutePath();
if (currentDirectory.contains("!")) {
@@ -79,34 +144,54 @@ public final class Main {
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.error("User cancels downloading JavaFX", e);
exit(0);
}
}
private static void checkDSTRootCAX3() {
TrustManagerFactory tmf;
/**
* Check if JavaFX exists but is incomplete
*/
private static void verifyJavaFX() {
try {
tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
} catch (NoSuchAlgorithmException | KeyStoreException e) {
LOG.log(Level.WARNING, "Failed to init TrustManagerFactory", e);
// don't know what to do here
return;
Class.forName("javafx.beans.binding.Binding"); // javafx.base
Class.forName("javafx.stage.Stage"); // javafx.graphics
Class.forName("javafx.scene.control.Skin"); // javafx.controls
} catch (Exception e) {
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
for (X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) {
if ("CN=DST Root CA X3, O=Digital Signature Trust Co.".equals((cert.getSubjectDN().getName()))) {
return;
}
}
}
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
ModuleHelper.addEnableNativeAccess(Class.forName("javafx.stage.Stage")); // javafx.graphics
} catch (ClassNotFoundException e) {
e.printStackTrace(System.err);
showErrorAndExit(i18n("fatal.javafx.incomplete"));
}
}
}
private static void enableUnsafeMemoryAccess() {
// https://openjdk.org/jeps/498
if (JavaRuntime.CURRENT_VERSION == 24 || JavaRuntime.CURRENT_VERSION == 25) {
try {
Class<?> clazz = Class.forName("sun.misc.Unsafe");
Method trySetMemoryAccessWarned = clazz.getDeclaredMethod("trySetMemoryAccessWarned");
trySetMemoryAccessWarned.setAccessible(true);
trySetMemoryAccessWarned.invoke(null);
} catch (Throwable e) {
e.printStackTrace(System.err);
}
}
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
}
/**
@@ -115,8 +200,17 @@ public final class Main {
static void showErrorAndExit(String message) {
System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting.");
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
System.exit(1);
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.ERROR, message).showAndWait();
exit(1);
}
} catch (Throwable ignored) {
}
SwingUtils.showErrorDialog(message);
exit(1);
}
/**
@@ -125,10 +219,19 @@ public final class Main {
static void showWarningAndContinue(String message) {
System.err.println(message);
System.err.println("Potential issues have been detected.");
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.WARNING, message).showAndWait();
return;
}
} catch (Throwable ignored) {
}
SwingUtils.showWarningDialog(message);
}
static void fixLetsEncrypt() {
private static void fixLetsEncrypt() {
try {
KeyStore defaultKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
Path ksPath = Paths.get(System.getProperty("java.home"), "lib", "security", "cacerts");
@@ -138,8 +241,9 @@ public final class Main {
}
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks");
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
}
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
merged.load(null, new char[0]);
@@ -154,8 +258,9 @@ public final class Main {
tls.init(null, instance.getTrustManagers(), null);
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
LOG.info("Added Lets Encrypt root certificates as additional trust");
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | KeyManagementException e) {
LOG.log(Level.SEVERE, "Failed to load lets encrypt certificate. Expect problems", e);
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
KeyManagementException e) {
LOG.error("Failed to load lets encrypt certificate. Expect problems", e);
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,37 +17,114 @@
*/
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumSet;
/**
* Stores metadata about this application.
*/
public final class Metadata {
private Metadata() {}
private Metadata() {
}
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
public static final String NAME = "HMCL";
public static final String FULL_NAME = "Hello Minecraft! Launcher";
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 8;
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
public static final String TITLE = NAME + " " + VERSION;
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
public static final String CONTACT_URL = "https://hmcl.huangyuhui.net/contact";
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
public static final String CHANGELOG_URL = "https://hmcl.huangyuhui.net/changelog/";
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
public static final String ABOUT_URL = PUBLISH_URL + "/about";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String DOCS_URL = "https://docs.hmcl.net";
public static final String CONTACT_URL = DOCS_URL + "/help.html";
public static final String CHANGELOG_URL = DOCS_URL + "/changelog/";
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
public static final String GITHUB_SHA = JarUtils.getManifestAttribute("GitHub-SHA", null);
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
public static final Path HMCL_GLOBAL_DIRECTORY;
public static final Path HMCL_CURRENT_DIRECTORY;
public static final Path DEPENDENCIES_DIRECTORY;
private static Path getHMCLDirectory() {
String home = System.getProperty("user.home", ".");
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
// to fulfill XDG standard.
return Paths.get(home, ".cache", "hmcl");
static {
String hmclHome = System.getProperty("hmcl.home");
if (hmclHome == null) {
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
String xdgData = System.getenv("XDG_DATA_HOME");
if (StringUtils.isNotBlank(xdgData)) {
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
} else {
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
}
} else {
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
} else {
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
}
String hmclCurrentDir = System.getProperty("hmcl.dir");
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
: CURRENT_DIRECTORY.resolve(".hmcl");
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
}
public static boolean isStable() {
return "stable".equals(BUILD_CHANNEL);
}
public static boolean isDev() {
return "dev".equals(BUILD_CHANNEL);
}
public static boolean isNightly() {
return !isStable() && !isDev();
}
public static @Nullable String getSuggestedJavaDownloadLink() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
return "https://www.loongnix.cn/zh/api/java/downloads-jdk21/index.html";
else {
EnumSet<Architecture> supportedArchitectures;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.X86, Architecture.ARM64);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX)
supportedArchitectures = EnumSet.of(
Architecture.X86_64, Architecture.X86,
Architecture.ARM64, Architecture.ARM32,
Architecture.RISCV64, Architecture.LOONGARCH64
);
else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
supportedArchitectures = EnumSet.of(Architecture.X86_64, Architecture.ARM64);
else
supportedArchitectures = EnumSet.noneOf(Architecture.class);
if (supportedArchitectures.contains(Architecture.SYSTEM_ARCH))
return String.format("https://docs.hmcl.net/downloads/%s/%s.html",
OperatingSystem.CURRENT_OS.getCheckedName(),
Architecture.SYSTEM_ARCH.getCheckedName()
);
else
return null;
}
return OperatingSystem.getWorkingDirectory("hmcl");
}
}
@@ -0,0 +1,54 @@
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class CrashReport {
private final Thread thread;
private final Throwable throwable;
private final String stackTrace;
public CrashReport(Thread thread, Throwable throwable) {
this.thread = thread;
this.throwable = throwable;
stackTrace = StringUtils.getStackTrace(throwable);
}
public Throwable getThrowable() {
return this.throwable;
}
public boolean shouldBeReport() {
if (!stackTrace.contains("org.jackhuang"))
return false;
if (throwable instanceof VirtualMachineError)
return false;
return true;
}
public String getDisplayText() {
return "---- Hello Minecraft! Crash Report ----\n" +
" Version: " + Metadata.VERSION + "\n" +
" Time: " + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) + "\n" +
" Thread: " + thread + "\n" +
"\n Content: \n " +
stackTrace + "\n\n" +
"-- System Details --\n" +
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName() + "\n" +
" Java Architecture: " + Architecture.CURRENT_ARCH.getDisplayName() + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
}
}
@@ -21,16 +21,16 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author huangyuhui
@@ -59,15 +59,60 @@ public final class HMCLGameLauncher extends DefaultLauncher {
private void generateOptionsTxt() {
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
if (optionsFile.exists())
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
if (optionsFile.exists()) {
return;
try {
// TODO: Dirty implementation here
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA)
FileUtils.writeText(optionsFile, "lang:zh_cn\nforceUnicodeFont:true\n");
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to generate options.txt", e);
}
if (configFolder.isDirectory()) {
if (findFiles(configFolder, "options.txt")) {
return;
}
}
if (!I18n.isUseChinese()) {
return;
}
String lang;
/*
1.0- :没有语言选项,遇到这些版本时不设置
1.1 ~ 5 :zh_CN 时正常,zh_cn 时崩溃(最后两位字母必须大写,否则将会 NPE 崩溃)
1.6 ~ 10 :zh_CN 时正常,zh_cn 时自动切换为英文
1.11 ~ 12:zh_cn 时正常,zh_CN 时虽然显示了中文但语言设置会错误地显示选择英文
1.13+ :zh_cn 时正常,zh_CN 时自动切换为英文
*/
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version));
if (gameVersion.compareTo("1.1") < 0) {
lang = null;
} else if (gameVersion.compareTo("1.11") < 0) {
lang = "zh_CN";
} else {
lang = "zh_cn";
}
if (lang != null) {
try {
FileUtils.writeText(optionsFile, String.format("lang:%s\n", lang));
} catch (IOException e) {
LOG.warning("Unable to generate options.txt", e);
}
}
}
private boolean findFiles(File folder, String fileName) {
File[] fs = folder.listFiles();
if (fs != null) {
for (File f : fs) {
if (f.isDirectory())
if (f.listFiles((dir, name) -> name.equals(fileName)) != null)
return true;
if (f.getName().equals(fileName))
return true;
}
}
return false;
}
@Override
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -20,32 +20,42 @@ package org.jackhuang.hmcl.game;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.ProxyManager;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Logging;
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.platform.SystemInfo;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
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.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
@@ -54,6 +64,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
private final Map<String, VersionSetting> localVersionSettings = new HashMap<>();
private final Set<String> beingModpackVersions = new HashSet<>();
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
public HMCLGameRepository(Profile profile, File baseDirectory) {
super(baseDirectory);
this.profile = profile;
@@ -88,6 +100,13 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
}
public Stream<Version> getDisplayVersions() {
return getVersions().stream()
.filter(v -> !v.isHidden())
.sorted(Comparator.comparing((Version v) -> Lang.requireNonNullElse(v.getReleaseTime(), Instant.EPOCH))
.thenComparing(v -> VersionNumber.asVersion(v.getId())));
}
@Override
protected void refreshVersionsImpl() {
localVersionSettings.clear();
@@ -104,8 +123,11 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (!file.exists() && !versions.isEmpty())
FileUtils.writeText(file, PROFILE);
} catch (IOException ex) {
Logging.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
System.gc();
}
public void changeDirectory(File newDirectory) {
@@ -124,13 +146,32 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
File srcDir = getVersionRoot(srcId);
File dstDir = getVersionRoot(dstId);
Path srcDir = getVersionRoot(srcId).toPath();
Path dstDir = getVersionRoot(dstId).toPath();
Version fromVersion = getVersion(srcId);
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
blackList.add("saves");
if (Files.exists(dstDir)) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null));
Path fromJson = srcDir.resolve(srcId + ".json");
Path fromJar = srcDir.resolve(srcId + ".jar");
Path toJson = dstDir.resolve(dstId + ".json");
Path toJar = dstDir.resolve(dstId + ".jar");
if (Files.exists(fromJar)) {
Files.copy(fromJar, toJar);
}
Files.copy(fromJson, toJson);
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
if (dstDir.exists()) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
oldVersionSetting.setUsesGlobal(false);
@@ -141,22 +182,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
List<String> blackList = new ArrayList<>(Arrays.asList(
"regex:(.*?)\\.log",
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"backup", "pack.json", "launcher.jar", "cache", // HMCL
".curseclient", // Curse
".fabric", ".mixin.out", // Fabric
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
));
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
blackList.add("saves");
if (originalGameDirType != GameDirectoryType.VERSION_FOLDER)
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
}
@@ -179,6 +204,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
/**
* Create new version setting if version id has no version setting.
*
* @param id the version id.
* @return new version setting, null if given version does not exist.
*/
@@ -193,7 +219,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
private VersionSetting initLocalVersionSetting(String id, VersionSetting vs) {
localVersionSettings.put(id, vs);
vs.addPropertyChangedListener(a -> saveVersionSetting(id));
vs.addListener(a -> saveVersionSetting(id));
return vs;
}
@@ -201,9 +227,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
* Get the version setting for version id.
*
* @param id version id
*
* @return corresponding version setting, null if the version has no its own version setting.
*/
@Nullable
public VersionSetting getLocalVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
loadLocalVersionSetting(id);
@@ -213,55 +239,113 @@ public class HMCLGameRepository extends DefaultGameRepository {
return setting;
}
@Nullable
public VersionSetting getLocalVersionSettingOrCreate(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null) {
vs = createLocalVersionSetting(id);
}
return vs;
}
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
profile.getGlobal().setGlobal(true); // always keep global.isGlobal = true
profile.getGlobal().setUsesGlobal(true);
return profile.getGlobal();
} else
return vs;
}
public File getVersionIconFile(String id) {
return new File(getVersionRoot(id), "icon.png");
public Optional<File> getVersionIconFile(String id) {
File root = getVersionRoot(id);
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
File file = new File(root, "icon." + extension);
if (file.exists()) {
return Optional.of(file);
}
}
return Optional.empty();
}
public void setVersionIconFile(String id, File iconFile) throws IOException {
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
if (!FXUtils.IMAGE_EXTENSIONS.contains(ext)) {
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);
for (String extension : FXUtils.IMAGE_EXTENSIONS) {
new File(root, "icon." + extension).delete();
}
}
public Image getVersionIconImage(String id) {
if (id == null || !isLoaded())
return newImage("/assets/img/grass.png");
return VersionIconType.DEFAULT.getIcon();
Version version = getVersion(id).resolve(this);
File iconFile = getVersionIconFile(id);
if (iconFile.exists())
return new Image("file:" + iconFile.getAbsolutePath());
else if (version.getMainClass() != null &&
(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN.equals(version.getMainClass())
|| version.getMainClass().startsWith("net.fabricmc")
|| LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(version.getMainClass())))
return newImage("/assets/img/furnace.png");
else
return newImage("/assets/img/grass.png");
VersionSetting vs = getLocalVersionSettingOrCreate(id);
VersionIconType iconType = vs != null ? Lang.requireNonNullElse(vs.getVersionIcon(), VersionIconType.DEFAULT) : VersionIconType.DEFAULT;
if (iconType == VersionIconType.DEFAULT) {
Version version = getVersion(id).resolve(this);
Optional<File> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try {
return FXUtils.loadImage(iconFile.get().toPath());
} catch (Exception e) {
LOG.warning("Failed to load version icon of " + id, e);
}
}
if (LibraryAnalyzer.isModded(this, version)) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null);
if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC))
return VersionIconType.FABRIC.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE))
return VersionIconType.FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE))
return VersionIconType.NEO_FORGE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT))
return VersionIconType.QUILT.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE))
return VersionIconType.OPTIFINE.getIcon();
else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER))
return VersionIconType.CHICKEN.getIcon();
else
return VersionIconType.FURNACE.getIcon();
}
return VersionIconType.DEFAULT.getIcon();
} else {
return iconType.getIcon();
}
}
public boolean saveVersionSetting(String id) {
public void saveVersionSetting(String id) {
if (!localVersionSettings.containsKey(id))
return false;
File file = getLocalVersionSettingFile(id);
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
return false;
return;
Path file = getLocalVersionSettingFile(id).toPath().toAbsolutePath().normalize();
try {
FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
return true;
Files.createDirectories(file.getParent());
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
return false;
LOG.warning("Failed to create directory: " + file.getParent(), e);
}
FileSaver.save(file, GSON.toJson(localVersionSettings.get(id)));
}
/**
* Make version use self version settings instead of the global one.
*
* @param id the version id.
* @return specialized version setting, null if given version does not exist.
*/
@@ -271,7 +355,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs = createLocalVersionSetting(id);
if (vs == null)
return null;
vs.setUsesGlobal(false);
if (vs.isUsesGlobal()) {
vs.setUsesGlobal(false);
}
return vs;
}
@@ -281,34 +367,57 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public boolean forbidsVersion(String id) {
return FORBIDDEN.contains(id);
}
public LaunchOptions getLaunchOptions(String version, File gameDir, boolean checkJava) throws InterruptedException {
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, File gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
JavaVersion javaVersion = Optional.ofNullable(vs.getJavaVersion(checkJava)).orElse(JavaVersion.fromCurrentEnvironment());
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setGameDir(gameDir)
.setJava(javaVersion)
.setVersionName(Metadata.TITLE)
.setVersionType(Metadata.TITLE)
.setVersionName(version)
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.getMaxMemory())
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
SystemInfo.getPhysicalMemoryStatus().getAvailable(),
vs.isAutoMemory()
) / 1024 / 1024))
.setMinMemory(vs.getMinMemory())
.setMetaspace(Lang.toIntOrNull(vs.getPermSize()))
.setEnvironmentVariables(
Lang.mapOf(StringUtils.tokenize(vs.getEnvironmentVariables())
.stream()
.map(it -> {
int idx = it.indexOf('=');
return idx >= 0 ? pair(it.substring(0, idx), it.substring(idx + 1)) : pair(it, "");
})
.collect(Collectors.toList())
)
)
.setWidth(vs.getWidth())
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPrecalledCommand(vs.getPreLaunchCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs());
.setPreLaunchCommand(vs.getPreLaunchCommand())
.setPostExitCommand(vs.getPostExitCommand())
.setNoGeneratedJVMArgs(vs.isNoJVMArgs())
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setRenderer(vs.getRenderer())
.setUseNativeGLFW(vs.isUseNativeGLFW())
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
.setJavaAgents(javaAgents)
.setJavaArguments(javaArguments);
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
builder.setProxyType(config().getProxyType());
builder.setProxyHost(config().getProxyHost());
builder.setProxyPort(config().getProxyPort());
if (config().hasProxyAuth()) {
builder.setProxyUser(config().getProxyUser());
builder.setProxyPass(config().getProxyPass());
@@ -320,16 +429,16 @@ public class HMCLGameRepository extends DefaultGameRepository {
try {
String jsonText = FileUtils.readText(json);
ModpackConfiguration<?> modpackConfiguration = JsonUtils.GSON.fromJson(jsonText, ModpackConfiguration.class);
if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(modpackConfiguration.getType())) {
ModpackConfiguration<McbbsModpackManifest> config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<McbbsModpackManifest>>() {
}.getType());
config.getManifest().injectLaunchOptions(builder);
}
ModpackProvider provider = ModpackHelper.getProviderByType(modpackConfiguration.getType());
if (provider != null) provider.injectLaunchOptions(jsonText, builder);
} catch (IOException | JsonParseException e) {
e.printStackTrace();
}
}
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
builder.setMaxMemory(null);
return builder.create();
}
@@ -364,7 +473,56 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setPrettyPrinting()
.create();
private static final HashSet<String> FORBIDDEN = new HashSet<>(Arrays.asList("modpack", "minecraftinstance", "manifest"));
private static final String PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}";
// These version ids are forbidden because they may conflict with modpack configuration filenames
private static final Set<String> FORBIDDEN_VERSION_IDS = new HashSet<>(Arrays.asList(
"modpack", "minecraftinstance", "manifest"));
public static boolean isValidVersionId(String id) {
if (FORBIDDEN_VERSION_IDS.contains(id))
return false;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS &&
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
return false;
return OperatingSystem.isNameValid(id);
}
/**
* Returns true if the given version id conflicts with an existing version.
*/
public boolean versionIdConflicts(String id) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
// on Windows, filenames are case-insensitive
for (String existingId : versions.keySet()) {
if (existingId.equalsIgnoreCase(id)) {
return true;
}
}
return false;
} else {
return versions.containsKey(id);
}
}
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
if (auto) {
available -= 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
16L * 1024 * 1024 * 1024);
return Math.max(minimum, suggested);
} else {
return minimum;
}
}
}
@@ -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;
@@ -33,10 +32,9 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public final class HMCLModpackInstallTask extends Task<Void> {
private final File zipFile;
@@ -44,8 +42,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependency;
private final Modpack modpack;
private final List<Task<?>> dependencies = new LinkedList<>();
private final List<Task<?>> dependents = new LinkedList<>();
private final List<Task<?>> dependencies = new ArrayList<>(1);
private final List<Task<?>> dependents = new ArrayList<>(4);
public HMCLModpackInstallTask(Profile profile, File zipFile, Modpack modpack, String name) {
dependency = profile.getDependency();
@@ -68,16 +66,15 @@ 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 (!MODPACK_TYPE.equals(config.getType()))
if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType()))
throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version.");
}
} catch (JsonParseException | IOException ignore) {
}
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/minecraft", it -> !"pack.json".equals(it), config));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/minecraft", modpack, MODPACK_TYPE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
}
@Override
@@ -94,7 +91,7 @@ public final class HMCLModpackInstallTask extends Task<Void> {
public void execute() throws Exception {
String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json");
Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion, null);
Task<Version> libraryTask = Task.supplyAsync(() -> originalVersion);
// reinstall libraries
// libraries of Forge and OptiFine should be obtained by installation.
@@ -106,14 +103,4 @@ public final class HMCLModpackInstallTask extends Task<Void> {
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
}
@Override
public List<String> getStages() {
return Stream.concat(
dependents.stream().flatMap(task -> task.getStages().stream()),
Stream.of("hmcl.modpack")
).collect(Collectors.toList());
}
public static final String MODPACK_TYPE = "HMCL";
}
@@ -1,57 +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.game;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
/**
* @author huangyuhui
*/
public final class HMCLModpackManager {
/**
* Read the manifest in a HMCL modpack.
*
* @param file a HMCL modpack file.
* @param encoding encoding of modpack zip file.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
* @return the manifest of HMCL modpack.
*/
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, Modpack.class).setEncoding(encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
if (game.getJar() == null)
if (StringUtils.isBlank(manifest.getVersion()))
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
else
return manifest.setManifest(HMCLModpackManifest.INSTANCE);
else
return manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
}
}
@@ -17,8 +17,16 @@
*/
package org.jackhuang.hmcl.game;
public final class HMCLModpackManifest {
import org.jackhuang.hmcl.mod.ModpackManifest;
import org.jackhuang.hmcl.mod.ModpackProvider;
public final class HMCLModpackManifest implements ModpackManifest {
public static final HMCLModpackManifest INSTANCE = new HMCLModpackManifest();
private HMCLModpackManifest() {}
@Override
public ModpackProvider getProvider() {
return HMCLModpackProvider.INSTANCE;
}
}
@@ -0,0 +1,89 @@
/*
* 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.game;
import com.google.gson.JsonParseException;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
public final class HMCLModpackProvider implements ModpackProvider {
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
@Override
public String getName() {
return "HMCL";
}
@Override
public Task<?> createCompletionTask(DefaultDependencyManager dependencyManager, String version) {
return null;
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, String name, File zipFile, Modpack modpack) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
if (!(dependencyManager.getGameRepository() instanceof HMCLGameRepository)) {
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
}
HMCLGameRepository repository = (HMCLGameRepository) dependencyManager.getGameRepository();
Profile profile = repository.getProfile();
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
}
@Override
public Modpack readManifest(ZipArchiveReader file, Path path, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
if (game.getJar() == null)
if (StringUtils.isBlank(manifest.getVersion()))
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
else
manifest.setManifest(HMCLModpackManifest.INSTANCE);
else
manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar());
return manifest;
}
private static class HMCLModpack extends Modpack {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,130 @@
/*
* 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.game;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.mod.RemoteModRepository;
import org.jackhuang.hmcl.ui.versions.ModTranslations;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public abstract class LocalizedRemoteModRepository implements RemoteModRepository {
private static final int CONTAIN_CHINESE_WEIGHT = 10;
private static final int INITIAL_CAPACITY = 16;
protected abstract RemoteModRepository getBackedRemoteModRepository();
protected abstract SortType getBackedRemoteModRepositorySortOrder();
@Override
public SearchResult search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
if (!StringUtils.containsChinese(searchFilter)) {
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
}
Set<String> englishSearchFiltersSet = new HashSet<>(INITIAL_CAPACITY);
int count = 0;
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByRepositoryType(getType()).searchMod(searchFilter)) {
for (String englishWord : StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName())) {
if (englishSearchFiltersSet.contains(englishWord)) {
continue;
}
englishSearchFiltersSet.add(englishWord);
}
count++;
if (count >= 3) break;
}
RemoteMod[] searchResultArray = new RemoteMod[pageSize];
int totalPages, chineseIndex = 0, englishIndex = pageSize - 1;
{
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.warning("Too many search results! Are the backed remote mod repository broken? Or are the API broken?");
continue;
}
RemoteMod remoteMod = iterator.next();
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) {
searchResultArray[chineseIndex++] = remoteMod;
} else {
searchResultArray[englishIndex--] = remoteMod;
}
}
totalPages = searchResult.getTotalPages();
}
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByRepositoryType(getType()).getModByCurseForgeId(remoteMod.getSlug());
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) {
return Pair.pair(remoteMod, Integer.MAX_VALUE);
}
String chineseRemoteModName = chineseRemoteMod.getName();
if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) {
return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length()));
}
int l = levCalculator.calc(searchFilter, chineseRemoteModName);
for (int i = 0; i < searchFilter.length(); i++) {
if (chineseRemoteModName.indexOf(searchFilter.charAt(i)) >= 0) {
l -= CONTAIN_CHINESE_WEIGHT;
}
}
return Pair.pair(remoteMod, l);
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), Arrays.stream(searchResultArray, englishIndex + 1, searchResultArray.length)), totalPages);
}
@Override
public Stream<Category> getCategories() throws IOException {
return getBackedRemoteModRepository().getCategories();
}
@Override
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file);
}
@Override
public RemoteMod getModById(String id) throws IOException {
return getBackedRemoteModRepository().getModById(id);
}
@Override
public RemoteMod.File getModFile(String modId, String fileId) throws IOException {
return getBackedRemoteModRepository().getModFile(modId, fileId);
}
@Override
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
return getBackedRemoteModRepository().getRemoteVersionsById(id);
}
}
@@ -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;
}
}
@@ -0,0 +1,109 @@
/*
* 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.game;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.management.ManagementFactory;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class LogExporter {
private LogExporter() {
}
public static CompletableFuture<Void> exportLogs(Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript) {
Path runDirectory = gameRepository.getRunDirectory(versionId).toPath();
Path baseDirectory = gameRepository.getBaseDirectory().toPath();
List<String> versions = new ArrayList<>();
String currentVersionId = versionId;
HashSet<String> resolvedSoFar = new HashSet<>();
while (true) {
if (resolvedSoFar.contains(currentVersionId)) break;
resolvedSoFar.add(currentVersionId);
Version currentVersion = gameRepository.getVersion(currentVersionId);
versions.add(currentVersionId);
if (StringUtils.isNotBlank(currentVersion.getInheritsFrom())) {
currentVersionId = currentVersion.getInheritsFrom();
} else {
break;
}
}
return CompletableFuture.runAsync(() -> {
try (Zipper zipper = new Zipper(zipFile)) {
processLogs(runDirectory.resolve("liteconfig"), "*.log", "liteconfig", zipper);
processLogs(runDirectory.resolve("logs"), "*.log", "logs", zipper);
processLogs(runDirectory, "*.log", "runDirectory", zipper);
processLogs(runDirectory.resolve("crash-reports"), "*.txt", "crash-reports", zipper);
zipper.putTextFile(LOG.getLogs(), "hmcl.log");
zipper.putTextFile(logs, "minecraft.log");
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");
if (Files.exists(versionJson)) {
zipper.putFile(versionJson, id + ".json");
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
private static void processLogs(Path directory, String fileExtension, String logDirectory, Zipper zipper) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, fileExtension)) {
long processStartTime = ManagementFactory.getRuntimeMXBean().getStartTime();
for (Path file : stream) {
if (Files.isRegularFile(file)) {
FileTime time = Files.readAttributes(file, BasicFileAttributes.class).lastModifiedTime();
if (time.toMillis() >= processStartTime) {
try (BufferedReader reader = IOUtils.newBufferedReaderMaybeNativeEncoding(file)) {
zipper.putLines(reader.lines().map(Logger::filterForbiddenToken), file.getFileName().toString());
} catch (IOException e) {
LOG.warning("Failed to read log file: " + file, e);
}
}
}
}
} catch (Throwable e) {
LOG.warning("Failed to find any log on " + logDirectory, e);
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -15,21 +15,18 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.javafx;
package org.jackhuang.hmcl.game;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import java.nio.file.Path;
class ReferenceHolder implements InvalidationListener {
@SuppressWarnings("unused")
private Object ref;
public class ManuallyCreatedModpackException extends Exception {
private final Path path;
public ReferenceHolder(Object ref) {
this.ref = ref;
public ManuallyCreatedModpackException(Path path) {
this.path = path;
}
@Override
public void invalidated(Observable observable) {
// no-op
public Path getPath() {
return path;
}
}
@@ -0,0 +1,61 @@
/*
* 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.game;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.Unzipper;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
private final Profile profile;
private final Path zipFile;
private final Charset charset;
private final String name;
public ManuallyCreatedModpackInstallTask(Profile profile, Path zipFile, Charset charset, String name) {
this.profile = profile;
this.zipFile = zipFile;
this.charset = charset;
this.name = name;
}
@Override
public void execute() throws Exception {
Path subdirectory;
try (FileSystem fs = CompressingUtils.readonly(zipFile).setEncoding(charset).build()) {
subdirectory = ModpackHelper.findMinecraftDirectoryInManuallyCreatedModpack(zipFile.toString(), fs);
}
Path dest = Paths.get("externalgames").resolve(name);
setResult(dest);
new Unzipper(zipFile, dest)
.setSubDirectory(subdirectory.toString())
.setTerminateIfSubDirectoryNotExists()
.setEncoding(charset)
.unzip();
}
}
@@ -18,19 +18,19 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import kala.compress.archivers.zip.ZipArchiveReader;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.curse.CurseManifest;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.curse.CurseModpackProvider;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackProvider;
import org.jackhuang.hmcl.mod.modrinth.ModrinthModpackProvider;
import org.jackhuang.hmcl.mod.multimc.MultiMCInstanceConfiguration;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackProvider;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.mod.server.ServerModpackProvider;
import org.jackhuang.hmcl.mod.server.ServerModpackRemoteInstallTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -38,78 +38,115 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.toIterable;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class ModpackHelper {
private ModpackHelper() {}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException {
try {
return McbbsModpackManifest.readManifest(file, charset);
} catch (Exception ignored) {
ignored.printStackTrace();
// ignore it, not a valid MCBBS modpack.
private static final Map<String, ModpackProvider> providers = mapOf(
pair(CurseModpackProvider.INSTANCE.getName(), CurseModpackProvider.INSTANCE),
pair(McbbsModpackProvider.INSTANCE.getName(), McbbsModpackProvider.INSTANCE),
pair(ModrinthModpackProvider.INSTANCE.getName(), ModrinthModpackProvider.INSTANCE),
pair(MultiMCModpackProvider.INSTANCE.getName(), MultiMCModpackProvider.INSTANCE),
pair(ServerModpackProvider.INSTANCE.getName(), ServerModpackProvider.INSTANCE),
pair(HMCLModpackProvider.INSTANCE.getName(), HMCLModpackProvider.INSTANCE)
);
@Nullable
public static ModpackProvider getProviderByType(String type) {
return providers.get(type);
}
public static boolean isFileModpackByExtension(File file) {
String ext = FileUtils.getExtension(file);
return "zip".equals(ext) || "mrpack".equals(ext);
}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try (ZipArchiveReader zipFile = CompressingUtils.openZipFile(file, charset)) {
// Order for trying detecting manifest is necessary here.
// Do not change to iterating providers.
for (ModpackProvider provider : new ModpackProvider[]{
McbbsModpackProvider.INSTANCE,
CurseModpackProvider.INSTANCE,
ModrinthModpackProvider.INSTANCE,
HMCLModpackProvider.INSTANCE,
MultiMCModpackProvider.INSTANCE,
ServerModpackProvider.INSTANCE}) {
try {
return provider.readManifest(zipFile, file, charset);
} catch (Exception ignored) {
}
}
} catch (IOException ignored) {
}
try {
return CurseManifest.readCurseForgeModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid CurseForge modpack.
}
try {
return HMCLModpackManager.readHMCLModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid HMCL modpack.
}
try {
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid MultiMC modpack.
}
try {
return ServerModpackManifest.readManifest(file, charset);
} catch (Exception e) {
// ignore it, not a valid Server modpack.
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
findMinecraftDirectoryInManuallyCreatedModpack(file.toString(), fs);
throw new ManuallyCreatedModpackException(file);
} catch (IOException e) {
// ignore it
}
throw new UnsupportedModpackException(file.toString());
}
public static Path findMinecraftDirectoryInManuallyCreatedModpack(String modpackName, FileSystem fs) throws IOException, UnsupportedModpackException {
Path root = fs.getPath("/");
if (isMinecraftDirectory(root)) return root;
try (Stream<Path> firstLayer = Files.list(root)) {
for (Path dir : toIterable(firstLayer)) {
if (isMinecraftDirectory(dir)) return dir;
try (Stream<Path> secondLayer = Files.list(dir)) {
for (Path subdir : toIterable(secondLayer)) {
if (isMinecraftDirectory(subdir)) return subdir;
}
} catch (IOException ignored) {
}
}
} catch (IOException ignored) {
}
throw new UnsupportedModpackException(modpackName);
}
private static boolean isMinecraftDirectory(Path path) {
return Files.isDirectory(path.resolve("versions")) &&
(path.getFileName() == null || ".minecraft".equals(FileUtils.getName(path)));
}
public static ModpackConfiguration<?> readModpackConfiguration(File file) throws IOException {
if (!file.exists())
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");
}
}
private static String getManifestType(Object manifest) throws UnsupportedModpackException {
if (manifest instanceof HMCLModpackManifest)
return HMCLModpackInstallTask.MODPACK_TYPE;
else if (manifest instanceof MultiMCInstanceConfiguration)
return MultiMCModpackInstallTask.MODPACK_TYPE;
else if (manifest instanceof CurseManifest)
return CurseInstallTask.MODPACK_TYPE;
else
throw new UnsupportedModpackException();
}
public static Task<Void> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
@@ -122,17 +159,36 @@ public final class ModpackHelper {
};
ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
}
};
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
}
public static Task<Void> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
public static boolean isExternalGameNameConflicts(String name) {
return Files.exists(Paths.get("externalgames").resolve(name));
}
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, File zipFile, String name, Charset charset) {
if (isExternalGameNameConflicts(name)) {
throw new IllegalArgumentException("name existing");
}
return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name)
.thenAcceptAsync(Schedulers.javafx(), location -> {
Profile newProfile = new Profile(name, location.toFile());
newProfile.setUseRelativePath(true);
Profiles.getProfiles().add(newProfile);
Profiles.setSelectedProfile(newProfile);
});
}
public static Task<?> getInstallTask(Profile profile, File zipFile, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
@@ -145,73 +201,42 @@ public final class ModpackHelper {
};
ExceptionalConsumer<Exception, ?> failure = ex -> {
if (ex instanceof CurseCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
if (ex instanceof ModpackCompletionException && !(ex.getCause() instanceof FileNotFoundException)) {
success.run();
// This is tolerable and we will not delete the game
}
};
if (modpack.getManifest() instanceof CurseManifest)
return new CurseInstallTask(profile.getDependency(), zipFile, modpack, ((CurseManifest) modpack.getManifest()), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof HMCLModpackManifest)
return new HMCLModpackInstallTask(profile, zipFile, modpack, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name)
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
else if (modpack.getManifest() instanceof ServerModpackManifest)
return new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name)
.whenComplete(Schedulers.defaultScheduler(), success, failure);
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name)
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name));
else throw new IllegalArgumentException("Unrecognized modpack: " + modpack.getManifest());
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name)
.whenComplete(Schedulers.javafx(), success, failure);
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
switch (configuration.getType()) {
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name));
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
.withStagesHint(Arrays.asList("hmcl.modpack", "hmcl.modpack.download"));
default:
throw new UnsupportedModpackException();
}
}
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, MismatchedModpackTypeException {
public static Task<?> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile.toPath(), charset);
switch (configuration.getType()) {
case CurseInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof CurseManifest))
throw new MismatchedModpackTypeException(CurseInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new CurseInstallTask(profile.getDependency(), zipFile, modpack, (CurseManifest) modpack.getManifest(), name));
case MultiMCModpackInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration))
throw new MismatchedModpackTypeException(MultiMCModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new MultiMCModpackInstallTask(profile.getDependency(), zipFile, modpack, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
case HMCLModpackInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(HMCLModpackInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
case McbbsModpackLocalInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof McbbsModpackManifest))
throw new MismatchedModpackTypeException(McbbsModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new McbbsModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (McbbsModpackManifest) modpack.getManifest(), name));
case ServerModpackLocalInstallTask.MODPACK_TYPE:
if (!(modpack.getManifest() instanceof ServerModpackManifest))
throw new MismatchedModpackTypeException(ServerModpackLocalInstallTask.MODPACK_TYPE, getManifestType(modpack.getManifest()));
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackLocalInstallTask(profile.getDependency(), zipFile, modpack, (ServerModpackManifest) modpack.getManifest(), name));
default:
throw new UnsupportedModpackException();
ModpackProvider provider = getProviderByType(configuration.getType());
if (provider == null) {
throw new UnsupportedModpackException();
}
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
}
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
@@ -0,0 +1,212 @@
/*
* 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.game;
import fi.iki.elonen.NanoHTTPD;
import org.jackhuang.hmcl.auth.AuthenticationException;
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.StringUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
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 {
private final int port;
private final CompletableFuture<String> future = new CompletableFuture<>();
public static String lastlyOpenedURL;
private String idToken;
private OAuthServer(int port) {
super(port);
this.port = port;
}
@Override
public String getRedirectURI() {
return String.format("http://localhost:%d/auth-response", port);
}
@Override
public String waitFor() throws InterruptedException, ExecutionException {
return future.get();
}
@Override
public String getIdToken() {
return idToken;
}
@Override
public Response serve(IHTTPSession session) {
if (!"/auth-response".equals(session.getUri())) {
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
}
if (session.getMethod() == Method.POST) {
Map<String, String> files = new HashMap<>();
try {
session.parseBody(files);
} catch (IOException 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());
}
} else if (session.getMethod() == Method.GET) {
// do nothing
} else {
return newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "");
}
String parameters = session.getQueryParameterString();
Map<String, String> query = mapOf(NetworkUtils.parseQuery(parameters));
if (query.containsKey("code")) {
idToken = query.get("id_token");
future.complete(query.get("code"));
} else {
LOG.warning("Error: " + parameters);
future.completeExceptionally(new AuthenticationException("failed to authenticate"));
}
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
LOG.error("Failed to load html", e);
return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, MIME_HTML, "");
}
thread(() -> {
try {
Thread.sleep(1000);
stop();
} catch (InterruptedException e) {
LOG.error("Failed to sleep for 1 second");
}
});
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
}
public static class Factory implements OAuth.Callback {
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
@Override
public OAuth.Session startServer() throws IOException, AuthenticationException {
if (StringUtils.isBlank(getClientId())) {
throw new MicrosoftAuthenticationNotSupportedException();
}
IOException exception = null;
for (int port : new int[]{29111, 29112, 29113, 29114, 29115}) {
try {
OAuthServer server = new OAuthServer(port);
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
return server;
} catch (IOException e) {
exception = e;
}
}
throw exception;
}
@Override
public void grantDeviceCode(String userCode, String verificationURI) {
onGrantDeviceCode.fireEvent(new GrantDeviceCodeEvent(this, userCode, verificationURI));
}
@Override
public void openBrowser(String url) throws IOException {
lastlyOpenedURL = url;
FXUtils.openLink(url);
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
}
@Override
public String getClientId() {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
}
@Override
public boolean isPublicClient() {
return true; // We have turned on the device auth flow.
}
}
public static class GrantDeviceCodeEvent extends Event {
private final String userCode;
private final String verificationUri;
public GrantDeviceCodeEvent(Object source, String userCode, String verificationUri) {
super(source);
this.userCode = userCode;
this.verificationUri = verificationUri;
}
public String getUserCode() {
return userCode;
}
public String getVerificationUri() {
return verificationUri;
}
}
public static class OpenBrowserEvent extends Event {
private final String url;
public OpenBrowserEvent(Object source, String url) {
super(source);
this.url = url;
}
public String getUrl() {
return url;
}
}
public static class MicrosoftAuthenticationNotSupportedException extends AuthenticationException {
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,31 +17,34 @@
*/
package org.jackhuang.hmcl.game;
import javafx.beans.binding.Bindings;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.Image;
import javafx.scene.image.PixelReader;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -49,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
@@ -67,15 +69,15 @@ public final class TexturesLoader {
// ==== Texture Loading ====
public static class LoadedTexture {
private final BufferedImage image;
private final Image image;
private final Map<String, String> metadata;
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
public LoadedTexture(Image image, Map<String, String> metadata) {
this.image = requireNonNull(image);
this.metadata = requireNonNull(metadata);
}
public BufferedImage getImage() {
public Image getImage() {
return image;
}
@@ -85,7 +87,7 @@ public final class TexturesLoader {
}
private static final ThreadPoolExecutor POOL = threadPool("TexturesDownload", true, 2, 10, TimeUnit.SECONDS);
private static final Path TEXTURES_DIR = Metadata.MINECRAFT_DIRECTORY.resolve("assets").resolve("skins");
private static final Path TEXTURES_DIR = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("skins");
private static Path getTexturePath(Texture texture) {
String url = texture.getUrl();
@@ -99,7 +101,7 @@ public final class TexturesLoader {
return TEXTURES_DIR.resolve(prefix).resolve(hash);
}
public static LoadedTexture loadTexture(Texture texture) throws IOException {
public static LoadedTexture loadTexture(Texture texture) throws Throwable {
if (StringUtils.isBlank(texture.getUrl())) {
throw new IOException("Texture url is empty");
}
@@ -113,19 +115,20 @@ 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());
}
}
}
BufferedImage img;
Image img;
try (InputStream in = Files.newInputStream(file)) {
img = ImageIO.read(in);
img = new Image(in);
}
if (img == null)
throw new IOException("Texture is malformed");
if (img.isError())
throw img.getException();
Map<String, String> metadata = texture.getMetadata();
if (metadata == null) {
@@ -136,34 +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/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/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) {
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
} catch (Throwable e) {
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
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();
}
})
@@ -175,8 +186,8 @@ public final class TexturesLoader {
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
} catch (Throwable e) {
LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
@@ -186,81 +197,178 @@ public final class TexturesLoader {
}, uuidFallback);
}
public static ObjectBinding<LoadedTexture> skinBinding(Account account) {
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
return BindingMapping.of(account.getTextures())
.map(textures -> textures
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN)))
.filter(it -> StringUtils.isNotBlank(it.getUrl())))
.asyncMap(it -> {
if (it.isPresent()) {
Texture texture = it.get();
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
public static ObservableValue<LoadedTexture> skinBinding(Account account) {
LoadedTexture uuidFallback = getDefaultSkin(account.getUUID());
if (account instanceof OfflineAccount) {
OfflineAccount offlineAccount = (OfflineAccount) account;
SimpleObjectProperty<LoadedTexture> binding = new SimpleObjectProperty<>();
InvalidationListener listener = o -> {
Skin skin = offlineAccount.getSkin();
String username = offlineAccount.getUsername();
binding.set(uuidFallback);
if (skin != null) {
skin.load(username).setExecutor(POOL).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
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) {
metadata = singletonMap("model", result.getModel().modelName);
} else {
metadata = emptyMap();
}
}, POOL);
} else {
binding.set(new LoadedTexture(result.getSkin().getImage(), metadata));
}
}).start();
}
};
listener.invalidated(offlineAccount);
binding.addListener(new Holder<>(listener));
offlineAccount.addListener(new WeakInvalidationListener(listener));
return binding;
} else {
return BindingMapping.of(account.getTextures())
.asyncMap(textures -> {
if (textures.isPresent()) {
Texture texture = textures.get().get(TextureType.SKIN);
if (texture != null && StringUtils.isNotBlank(texture.getUrl())) {
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (Throwable e) {
LOG.warning("Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
}, POOL);
}
}
return CompletableFuture.completedFuture(uuidFallback);
}
}, uuidFallback);
}, uuidFallback);
}
}
// ====
// ==== Avatar ====
public static BufferedImage toAvatar(BufferedImage skin, int size) {
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = avatar.createGraphics();
public static void drawAvatar(Canvas canvas, Image skin) {
GraphicsContext g = canvas.getGraphicsContext2D();
g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
int scale = skin.getWidth() / 64;
int size = (int) canvas.getWidth();
int scale = (int) skin.getWidth() / 64;
int faceOffset = (int) Math.round(size / 18.0);
g.drawImage(skin,
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
8 * scale, 8 * scale, 16 * scale, 16 * scale,
null);
g.drawImage(skin,
0, 0, size, size,
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
g.dispose();
return avatar;
try {
g.setImageSmoothing(false);
drawAvatar(g, skin, size, scale, faceOffset);
} catch (NoSuchMethodError ignored) {
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing
// In order to prevent the blurring caused by bilinear interpolation,
// we use the self-implemented nearest neighbor interpolation to scale
drawAvatarSlow(g, skin, size, scale, faceOffset);
}
}
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size))
.map(TexturesLoader::toFXImage);
private static void drawAvatar(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
g.drawImage(skin,
8 * scale, 8 * scale, 8 * scale, 8 * scale,
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
g.drawImage(skin,
40 * scale, 8 * scale, 8 * scale, 8 * scale,
0, 0, size, size);
}
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
return BindingMapping.of(skinBinding(account))
.map(it -> toAvatar(it.image, size))
.map(TexturesLoader::toFXImage);
} else {
return Bindings.createObjectBinding(
() -> toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
private static void drawAvatarSlow(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
PixelReader reader = skin.getPixelReader();
PixelWriter writer = g.getPixelWriter();
drawImage(writer, reader,
8 * scale, 8 * scale, 8 * scale, 8 * scale,
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
drawImage(writer, reader,
40 * scale, 8 * scale, 8 * scale, 8 * scale,
0, 0, size, size);
}
/*
* Scale and draw image using nearest-neighbor interpolation
*/
private static void drawImage(PixelWriter writer, PixelReader reader,
int sx, int sy, int sw, int sh,
int dx, int dy, int dw, int dh) {
double xScale = ((double) sw) / dw;
double yScale = ((double) sh) / dh;
for (int xOffset = 0; xOffset < dw; xOffset++) {
for (int yOffset = 0; yOffset < dh; yOffset++) {
int color = reader.getArgb(sx + (int) (xOffset * xScale), sy + (int) (yOffset * yScale));
// Draw only non-transparent pixels
if ((color >>> 24) != 0)
writer.setArgb(dx + xOffset, dy + yOffset, color);
}
}
}
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
final WeakReference<Canvas> canvasRef;
final ObservableValue<LoadedTexture> binding;
SkinBindingChangeListener(Canvas canvas, ObservableValue<LoadedTexture> binding) {
this.canvasRef = new WeakReference<>(canvas);
this.binding = binding;
}
@Override
public void changed(ObservableValue<? extends LoadedTexture> observable,
LoadedTexture oldValue, LoadedTexture loadedTexture) {
Canvas canvas = canvasRef.get();
if (canvas != null)
drawAvatar(canvas, loadedTexture.image);
}
}
public static void fxAvatarBinding(Canvas canvas, ObservableValue<LoadedTexture> skinBinding) {
synchronized (SkinBindingChangeListener.hole) {
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
if (oldListener != null)
oldListener.binding.removeListener(oldListener);
SkinBindingChangeListener listener = new SkinBindingChangeListener(canvas, skinBinding);
listener.changed(skinBinding, null, skinBinding.getValue());
skinBinding.addListener(listener);
SkinBindingChangeListener.hole.put(canvas, listener);
}
}
public static void bindAvatar(Canvas canvas, YggdrasilService service, UUID uuid) {
fxAvatarBinding(canvas, skinBinding(service, uuid));
}
public static void bindAvatar(Canvas canvas, Account account) {
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount || account instanceof OfflineAccount)
fxAvatarBinding(canvas, skinBinding(account));
else {
unbindAvatar(canvas);
drawAvatar(canvas, getDefaultSkin(account.getUUID()).image);
}
}
public static void unbindAvatar(Canvas canvas) {
synchronized (SkinBindingChangeListener.hole) {
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
if (oldListener != null)
oldListener.binding.removeListener(oldListener);
}
}
// ====
// Based on https://stackoverflow.com/a/57552025
// Fix #874: Use it instead of SwingFXUtils.toFXImage
private static WritableImage toFXImage(BufferedImage image) {
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
PixelWriter pw = wr.getPixelWriter();
final int iw = image.getWidth();
final int ih = image.getHeight();
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
pw.setArgb(x, y, image.getRGB(x, y));
}
}
return wr;
}
}
}
@@ -0,0 +1,232 @@
/*
* 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.MACOS) {
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());
}
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(platformRoot)) {
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.MACOS)
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(), isManaged));
}
}
} catch (Throwable e) {
LOG.warning("Failed to parse " + file, e);
}
}
} catch (IOException ignored) {
}
}
@Override
public Collection<JavaRuntime> getAllJava(Platform platform) {
Path platformRoot = getPlatformRoot(platform);
if (!Files.isDirectory(platformRoot))
return Collections.emptyList();
ArrayList<JavaRuntime> list = new ArrayList<>();
getAllJava(list, platform, platformRoot, true);
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
platformRoot = root.resolve(platform.getOperatingSystem().getMojangName() + "-" + platform.getArchitecture().getCheckedName());
if (Files.isDirectory(platformRoot))
getAllJava(list, platform, platformRoot, false);
}
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.MACOS)
executable = JavaManager.getMacExecutable(javaDir).toRealPath();
else
throw e;
}
JavaInfo info;
if (JavaManager.isCompatible(platform))
info = JavaInfoUtils.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) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.java;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
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.SystemUtils;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* @author Glavo
* @see <a href="https://github.com/Glavo/java-info">Glavo/java-info</a>
*/
public final class JavaInfoUtils {
private JavaInfoUtils() {
}
private static Path tryFindReleaseFile(Path executable) {
Path parent = executable.getParent();
if (parent != null && parent.getFileName() != null && parent.getFileName().toString().equals("bin")) {
Path javaHome = parent.getParent();
if (javaHome != null && javaHome.getFileName() != null) {
Path releaseFile = javaHome.resolve("release");
String javaHomeName = javaHome.getFileName().toString();
if ((javaHomeName.contains("jre") || javaHomeName.contains("jdk") || javaHomeName.contains("openj9"))
&& Files.isRegularFile(releaseFile)) {
return releaseFile;
}
}
}
return null;
}
public static @NotNull JavaInfo fromExecutable(Path executable, boolean tryFindReleaseFile) throws IOException {
assert executable.isAbsolute();
Path releaseFile;
if (tryFindReleaseFile && (releaseFile = tryFindReleaseFile(executable)) != null) {
try {
return JavaInfo.fromReleaseFile(releaseFile);
} catch (IOException ignored) {
}
}
Path thisPath = JarUtils.thisJarPath();
if (thisPath == null) {
throw new IOException("Failed to find current HMCL location");
}
try {
Result result = JsonUtils.GSON.fromJson(SystemUtils.run(
executable.toString(),
"-classpath",
thisPath.toString(),
org.glavo.info.Main.class.getName()
), Result.class);
if (result == null) {
throw new IOException("Failed to get Java info from " + executable);
}
if (result.javaVersion == null) {
throw new IOException("Failed to get Java version from " + executable);
}
Architecture architecture = Architecture.parseArchName(result.osArch);
Platform platform = Platform.getPlatform(OperatingSystem.CURRENT_OS,
architecture != Architecture.UNKNOWN
? architecture
: Architecture.SYSTEM_ARCH);
return new JavaInfo(platform, result.javaVersion, result.javaVendor);
} catch (IOException e) {
throw e;
} catch (Throwable e) {
throw new IOException(e);
}
}
private static final class Result {
@SerializedName("os.name")
public String osName;
@SerializedName("os.arch")
public String osArch;
@SerializedName("java.version")
public String javaVersion;
@SerializedName("java.vendor")
public String javaVendor;
}
}
@@ -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 kala.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,660 @@
/*
* 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.io.FileUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.windows.WinReg;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
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_GLOBAL_DIRECTORY.resolve("java"));
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
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.MACOS) {
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 MACOS:
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 = JavaInfoUtils.fromExecutable(executable, true);
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 JavaRuntime chooseJava(@Nullable JavaRuntime java1, JavaRuntime java2) {
if (java1 == null)
return java2;
if (java1.getParsedVersion() != java2.getParsedVersion())
// Prefer the Java version that is closer to the game's recommended Java version
return java1.getParsedVersion() < java2.getParsedVersion() ? java1 : java2;
else
return java1.getVersionNumber().compareTo(java2.getVersionNumber()) >= 0 ? java1 : java2;
}
@Nullable
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.MACOS)
&& (gameVersion == null || gameVersion.compareTo("1.6") < 0);
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) {
mandatory = chooseJava(mandatory, java);
if (!violationSuggested)
suggested = chooseJava(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 MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searchAllJavaInRepository(javaRuntimes, Platform.MACOS_X86_64);
break;
}
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
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 MACOS:
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines"));
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
// 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.MACOS) {
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(File.pathSeparator);
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(File.pathSeparator);
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 = JavaInfoUtils.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 = JavaInfoUtils.fromExecutable(executable, true);
} catch (IOException e) {
LOG.warning("Failed to lookup Java executable at " + executable, e);
}
if (info != null && isCompatible(info.getPlatform())) {
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
}
}
private static void tryAddJavaInComponentDir(Map<Path, JavaRuntime> javaRuntimes, String platform, Path component, boolean verify) {
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
if (!Files.isRegularFile(sha1File))
return;
Path dir = component.resolve(platform).resolve(component.getFileName());
if (verify) {
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isEmpty()) continue;
int idx = line.indexOf(" /#//");
if (idx <= 0)
throw new IOException("Illegal line: " + line);
Path file = dir.resolve(line.substring(0, idx));
// Should we check the sha1 of files? This will take a lot of time.
if (Files.notExists(file))
throw new NoSuchFileException(file.toAbsolutePath().toString());
}
} catch (IOException e) {
LOG.warning("Failed to verify Java in " + component, e);
return;
}
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Path macPath = dir.resolve("jre.bundle/Contents/Home");
if (Files.exists(macPath)) {
tryAddJavaHome(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);
}
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
javaRuntimes.put(java.getBinary(), java);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
if (!Files.isDirectory(directory))
return;
// Examples:
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
if (javaPlatform != null) {
searchAllOfficialJava(javaRuntimes, directory, javaPlatform, verify);
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
}
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
}
}
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, String platform, boolean verify) {
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
for (Path component : dir) {
tryAddJavaInComponentDir(javaRuntimes, platform, component, verify);
}
} 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));
}
}
private static void searchJavaInMacJavaVirtualMachines(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
if (!Files.isDirectory(directory)) {
return;
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
for (Path subDir : stream) {
tryAddJavaHome(javaRuntimes, subDir.resolve("Contents/Home"));
}
} catch (IOException e) {
LOG.warning("Failed to find Java in " + directory, e);
}
}
// ==== Windows Registry Support ====
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return;
for (String java : reg.querySubKeys(hkey, location)) {
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
continue;
Object home = reg.queryValue(hkey, java, "JavaHome");
if (home instanceof String) {
try {
tryAddJavaHome(javaRuntimes, Paths.get((String) home));
} catch (InvalidPathException e) {
LOG.warning("Invalid Java path in system registry: " + home);
}
}
}
}
}
@@ -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);
}
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,82 +17,82 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloader;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.*;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import javax.net.ssl.SSLException;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.*;
import static java.util.stream.Collectors.toList;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
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);
}
});
}
}
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(MicrosoftAccountLoginStage.INSTANCE));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_AUTHLIB_INJECTOR, FACTORY_MICROSOFT);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
// ==== 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("yggdrasil", FACTORY_MOJANG);
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
type2factory.put("microsoft", FACTORY_MICROSOFT);
@@ -100,14 +100,24 @@ public final class Accounts {
}
public static String getLoginType(AccountFactory<?> factory) {
return Optional.ofNullable(factory2type.get(factory))
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory"));
String type = factory2type.get(factory);
if (type != null) return type;
if (factory instanceof BoundAuthlibInjectorAccountFactory) {
return factory2type.get(FACTORY_AUTHLIB_INJECTOR);
}
throw new IllegalArgumentException("Unrecognized account factory");
}
public static AccountFactory<?> getAccountFactory(String loginType) {
return Optional.ofNullable(type2factory.get(loginType))
.orElseThrow(() -> new IllegalArgumentException("Unrecognized login type"));
}
public static BoundAuthlibInjectorAccountFactory getAccountFactoryByAuthlibInjectorServer(AuthlibInjectorServer server) {
return new BoundAuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, server);
}
// ====
public static AccountFactory<?> getAccountFactory(Account account) {
@@ -115,67 +125,26 @@ public final class Accounts {
return FACTORY_OFFLINE;
else if (account instanceof AuthlibInjectorAccount)
return FACTORY_AUTHLIB_INJECTOR;
else if (account instanceof YggdrasilAccount)
return FACTORY_MOJANG;
else if (account instanceof MicrosoftAccount)
return FACTORY_MICROSOFT;
else
throw new IllegalArgumentException("Failed to determine account type: " + account);
}
private static ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
private static final String GLOBAL_PREFIX = "$GLOBAL:";
private static final ObservableList<Map<Object, Object>> globalAccountStorages = FXCollections.observableArrayList();
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
// this methods first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account selected = get();
if (accounts.isEmpty()) {
if (selected == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
set(null);
return;
}
} else {
if (accounts.contains(selected)) {
// valid
} else {
// the previously selected account is gone
set(accounts.get(0));
return;
}
}
// selection is valid, store it
if (!initialized)
return;
updateAccountStorages();
}
};
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[]{account});
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
private static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", getLoginType(getAccountFactory(account)));
if (account == selectedAccount.get()) {
storage.put("selected", true);
}
return storage;
}
@@ -185,7 +154,51 @@ public final class Accounts {
if (!initialized)
return;
// update storage
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
ArrayList<Map<Object, Object>> global = new ArrayList<>();
ArrayList<Map<Object, Object>> portable = new ArrayList<>();
for (Account account : accounts) {
Map<Object, Object> storage = getAccountStorage(account);
if (account.isPortable())
portable.add(storage);
else
global.add(storage);
}
if (!global.equals(globalAccountStorages))
globalAccountStorages.setAll(global);
if (!portable.equals(config().getAccountStorages()))
config().getAccountStorages().setAll(portable);
}
private static void loadGlobalAccountStorages() {
Path globalAccountsFile = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("accounts.json");
if (Files.exists(globalAccountsFile)) {
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
globalAccountStorages.setAll(Config.CONFIG_GSON.fromJson(reader, listTypeOf(mapTypeOf(Object.class, Object.class))));
} catch (Throwable e) {
LOG.warning("Failed to load global accounts", e);
}
}
globalAccountStorages.addListener(onInvalidating(() ->
FileSaver.save(globalAccountsFile, Config.CONFIG_GSON.toJson(globalAccountStorages))));
}
private static Account parseAccount(Map<Object, Object> storage) {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return null;
}
try {
return factory.fromStorage(storage);
} catch (Exception e) {
LOG.warning("Failed to load account: " + storage, e);
return null;
}
}
/**
@@ -195,45 +208,136 @@ public final class Accounts {
if (initialized)
throw new IllegalStateException("Already initialized");
// load accounts
config().getAccountStorages().forEach(storage -> {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return;
}
Account account;
try {
account = factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return;
}
accounts.add(account);
if (!config().isAddedLittleSkin()) {
AuthlibInjectorServer littleSkin = new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/");
if (Boolean.TRUE.equals(storage.get("selected"))) {
selectedAccount.set(account);
if (config().getAuthlibInjectorServers().stream().noneMatch(it -> littleSkin.getUrl().equals(it.getUrl()))) {
config().getAuthlibInjectorServers().add(0, littleSkin);
}
});
config().setAddedLittleSkin(true);
}
loadGlobalAccountStorages();
// load accounts
Account selected = null;
for (Map<Object, Object> storage : config().getAccountStorages()) {
Account account = parseAccount(storage);
if (account != null) {
account.setPortable(true);
accounts.add(account);
if (Boolean.TRUE.equals(storage.get("selected"))) {
selected = account;
}
}
}
for (Map<Object, Object> storage : globalAccountStorages) {
Account account = parseAccount(storage);
if (account != null) {
accounts.add(account);
}
}
String selectedAccountIdentifier = config().getSelectedAccount();
if (selected == null && selectedAccountIdentifier != null) {
boolean portable = true;
if (selectedAccountIdentifier.startsWith(GLOBAL_PREFIX)) {
portable = false;
selectedAccountIdentifier = selectedAccountIdentifier.substring(GLOBAL_PREFIX.length());
}
for (Account account : accounts) {
if (selectedAccountIdentifier.equals(account.getIdentifier())) {
if (portable == account.isPortable()) {
selected = account;
break;
} else if (selected == null) {
selected = account;
}
}
}
}
if (selected == null && !accounts.isEmpty()) {
selected = accounts.get(0);
}
if (!globalConfig().isEnableOfflineAccount())
for (Account account : accounts) {
if (account instanceof MicrosoftAccount) {
globalConfig().setEnableOfflineAccount(true);
break;
}
}
if (!globalConfig().isEnableOfflineAccount())
accounts.addListener(new ListChangeListener<Account>() {
@Override
public void onChanged(Change<? extends Account> change) {
while (change.next()) {
for (Account account : change.getAddedSubList()) {
if (account instanceof MicrosoftAccount) {
accounts.removeListener(this);
globalConfig().setEnableOfflineAccount(true);
return;
}
}
}
}
});
selectedAccount.set(selected);
InvalidationListener listener = o -> {
// this method first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account account = selectedAccount.get();
if (accounts.isEmpty()) {
if (account == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
selectedAccount.set(null);
}
} else {
if (accounts.contains(account)) {
// valid
} else {
// the previously selected account is gone
selectedAccount.set(accounts.get(0));
}
}
};
selectedAccount.addListener(listener);
selectedAccount.addListener(onInvalidating(() -> {
Account account = selectedAccount.get();
if (account != null)
config().setSelectedAccount(account.isPortable() ? account.getIdentifier() : GLOBAL_PREFIX + account.getIdentifier());
else
config().setSelectedAccount(null);
}));
accounts.addListener(listener);
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
initialized = true;
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
Account selected = selectedAccount.get();
if (selected != null) {
Account finalSelected = selected;
Schedulers.io().execute(() -> {
try {
selected.logIn();
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
finalSelected.logIn();
} catch (Throwable e) {
LOG.warning("Failed to log " + finalSelected + " in", e);
}
});
}
if (!config().getAuthlibInjectorServers().isEmpty()) {
triggerAuthlibInjectorUpdateCheck();
}
triggerAuthlibInjectorUpdateCheck();
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
@@ -242,7 +346,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);
}
});
}
@@ -252,10 +356,6 @@ public final class Accounts {
return accounts;
}
public static ReadOnlyListProperty<Account> accountsProperty() {
return accountsWrapper.getReadOnlyProperty();
}
public static Account getSelectedAccount() {
return selectedAccount.get();
}
@@ -273,7 +373,7 @@ public final class Accounts {
String authlibinjectorLocation = System.getProperty("hmcl.authlibinjector.location");
if (authlibinjectorLocation == null) {
return new AuthlibInjectorDownloader(
Metadata.HMCL_DIRECTORY.resolve("authlib-injector.jar"),
Metadata.DEPENDENCIES_DIRECTORY.resolve("universal").resolve("authlib-injector.jar"),
DownloadProviders::getDownloadProvider) {
@Override
public Optional<AuthlibInjectorArtifactInfo> getArtifactInfoImmediately() {
@@ -282,7 +382,7 @@ public final class Accounts {
return local;
}
// search authlib-injector.jar in current directory, it's used as a fallback
return parseArtifact(Paths.get("authlib-injector.jar"));
return parseArtifact(Metadata.CURRENT_DIRECTORY.resolve("authlib-injector.jar"));
}
};
} else {
@@ -317,9 +417,8 @@ public final class Accounts {
// ====
// ==== Login type name i18n ===
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
private static final Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"),
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"),
pair(Accounts.FACTORY_MICROSOFT, "account.methods.microsoft"));
@@ -328,4 +427,72 @@ public final class Accounts {
.orElseThrow(() -> new IllegalArgumentException("Unrecognized account factory")));
}
// ====
public static String localizeErrorMessage(Exception exception) {
if (exception instanceof NoCharacterException) {
return i18n("account.failed.no_character");
} else if (exception instanceof ServerDisconnectException) {
if (exception.getCause() instanceof SSLException) {
return i18n("account.failed.ssl");
} else {
return i18n("account.failed.connect_authentication_server");
}
} else if (exception instanceof ServerResponseMalformedException) {
return i18n("account.failed.server_response_malformed");
} else if (exception instanceof RemoteAuthenticationException) {
RemoteAuthenticationException remoteException = (RemoteAuthenticationException) exception;
String remoteMessage = remoteException.getRemoteMessage();
if ("ForbiddenOperationException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("Invalid credentials")) {
return i18n("account.failed.invalid_credentials");
} else if (remoteMessage.contains("Invalid token")) {
return i18n("account.failed.invalid_token");
} else if (remoteMessage.contains("Invalid username or password")) {
return i18n("account.failed.invalid_password");
} else {
return remoteMessage;
}
} else if ("ResourceException".equals(remoteException.getRemoteName()) && remoteMessage != null) {
if (remoteMessage.contains("The requested resource is no longer available")) {
return i18n("account.failed.migration");
} else {
return remoteMessage;
}
}
return exception.getMessage();
} else if (exception instanceof AuthlibInjectorDownloadException) {
return i18n("account.failed.injector_download_failure");
} else if (exception instanceof CharacterDeletedException) {
return i18n("account.failed.character_deleted");
} else if (exception instanceof InvalidSkinException) {
return i18n("account.skin.invalid_skin");
} else if (exception instanceof MicrosoftService.XboxAuthorizationException) {
long errorCode = ((MicrosoftService.XboxAuthorizationException) exception).getErrorCode();
if (errorCode == MicrosoftService.XboxAuthorizationException.ADD_FAMILY) {
return i18n("account.methods.microsoft.error.add_family");
} else if (errorCode == MicrosoftService.XboxAuthorizationException.COUNTRY_UNAVAILABLE) {
return i18n("account.methods.microsoft.error.country_unavailable");
} else if (errorCode == MicrosoftService.XboxAuthorizationException.MISSING_XBOX_ACCOUNT) {
return i18n("account.methods.microsoft.error.missing_xbox_account");
} else if (errorCode == MicrosoftService.XboxAuthorizationException.BANNED) {
return i18n("account.methods.microsoft.error.banned");
} 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) {
return i18n("account.methods.microsoft.error.add_family_probably");
} else if (exception instanceof OAuthServer.MicrosoftAuthenticationNotSupportedException) {
return i18n("account.methods.microsoft.snapshot");
} else if (exception instanceof OAuthAccount.WrongAccountException) {
return i18n("account.failed.wrong_account");
} else if (exception.getClass() == AuthenticationException.class) {
return exception.getLocalizedMessage();
} else {
return exception.getClass().getName() + ": " + exception.getLocalizedMessage();
}
}
}
@@ -22,70 +22,75 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
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 class AuthlibInjectorServers implements Validation {
public final class AuthlibInjectorServers implements Validation {
public static final String CONFIG_FILENAME = "authlib-injectors.json";
private static final Set<AuthlibInjectorServer> servers = new CopyOnWriteArraySet<>();
public static Set<AuthlibInjectorServer> getServers() {
return servers;
}
private final List<String> urls;
public AuthlibInjectorServers(List<String> urls) {
private AuthlibInjectorServers(List<String> urls) {
this.urls = urls;
}
public List<String> getUrls() {
return urls;
}
@Override
public void validate() throws JsonParseException {
if (urls == null)
throw new JsonParseException("authlib-injectors.json -> urls cannot be null");
public void validate() throws JsonParseException, TolerableValidationException {
if (this.urls == null) {
throw new JsonParseException("authlib-injectors.json -> urls cannot be null.");
}
}
private static final Path configLocation = Paths.get(CONFIG_FILENAME);
private static AuthlibInjectorServers configInstance;
public synchronized static void init() {
if (configInstance != null) {
throw new IllegalStateException("AuthlibInjectorServers is already loaded");
public static void init() {
Path configLocation;
Path jarPath = JarUtils.thisJarPath();
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
configLocation = jarPath.getParent().resolve(CONFIG_FILENAME);
} else {
configLocation = Paths.get(CONFIG_FILENAME);
}
configInstance = new AuthlibInjectorServers(Collections.emptyList());
if (Files.exists(configLocation)) {
if (ConfigHolder.isNewlyCreated() && Files.exists(configLocation)) {
AuthlibInjectorServers configInstance;
try {
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;
}
if (!configInstance.urls.isEmpty()) {
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
for (String url : configInstance.urls) {
Task.supplyAsync(Schedulers.io(), () -> AuthlibInjectorServer.locateServer(url))
.thenAcceptAsync(Schedulers.javafx(), server -> {
config().getAuthlibInjectorServers().add(server);
servers.add(server);
})
.start();
}
}
}
if (ConfigHolder.isNewlyCreated() && !AuthlibInjectorServers.getConfigInstance().getUrls().isEmpty()) {
config().setPreferredLoginType(Accounts.getLoginType(Accounts.FACTORY_AUTHLIB_INJECTOR));
for (String url : AuthlibInjectorServers.getConfigInstance().getUrls()) {
Task.supplyAsync(Schedulers.io(), () -> AuthlibInjectorServer.locateServer(url))
.thenAcceptAsync(Schedulers.javafx(), server -> config().getAuthlibInjectorServers().add(server))
.start();
}
}
}
public static AuthlibInjectorServers getConfigInstance() {
return configInstance;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.ToNumberPolicy;
import com.google.gson.annotations.SerializedName;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
@@ -34,7 +35,6 @@ import org.hildan.fxgson.creators.ObservableSetCreator;
import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.util.gson.EnumOrdinalDeserializer;
import org.jackhuang.hmcl.util.gson.FileTypeAdapter;
import org.jackhuang.hmcl.util.i18n.Locales;
@@ -48,11 +48,12 @@ import java.net.Proxy;
import java.util.Map;
import java.util.TreeMap;
public final class Config implements Cloneable, Observable {
public final class Config implements Observable {
public static final int CURRENT_VERSION = 2;
public static final int CURRENT_UI_VERSION = 0;
private static final Gson CONFIG_GSON = new GsonBuilder()
public static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
@@ -61,6 +62,7 @@ public final class Config implements Cloneable, Observable {
.registerTypeAdapter(EnumBackgroundImage.class, new EnumOrdinalDeserializer<>(EnumBackgroundImage.class)) // backward compatibility for backgroundType
.registerTypeAdapter(Proxy.Type.class, new EnumOrdinalDeserializer<>(Proxy.Type.class)) // backward compatibility for hasProxy
.setPrettyPrinting()
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
.create();
@Nullable
@@ -83,6 +85,9 @@ public final class Config implements Cloneable, Observable {
@SerializedName("bgpath")
private StringProperty backgroundImage = new SimpleStringProperty();
@SerializedName("bgurl")
private StringProperty backgroundImageUrl = new SimpleStringProperty();
@SerializedName("commonDirType")
private ObjectProperty<EnumCommonDirectory> commonDirType = new SimpleObjectProperty<>(EnumCommonDirectory.DEFAULT);
@@ -110,6 +115,12 @@ public final class Config implements Cloneable, Observable {
@SerializedName("proxyPassword")
private StringProperty proxyPass = new SimpleStringProperty();
@SerializedName("x")
private DoubleProperty x = new SimpleDoubleProperty();
@SerializedName("y")
private DoubleProperty y = new SimpleDoubleProperty();
@SerializedName("width")
private DoubleProperty width = new SimpleDoubleProperty();
@@ -117,34 +128,61 @@ public final class Config implements Cloneable, Observable {
private DoubleProperty height = new SimpleDoubleProperty();
@SerializedName("theme")
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>(Theme.BLUE);
private ObjectProperty<Theme> theme = new SimpleObjectProperty<>();
@SerializedName("localization")
private ObjectProperty<SupportedLocale> localization = new SimpleObjectProperty<>(Locales.DEFAULT);
@SerializedName("autoDownloadThreads")
private BooleanProperty autoDownloadThreads = new SimpleBooleanProperty(true);
@SerializedName("downloadThreads")
private IntegerProperty downloadThreads = new SimpleIntegerProperty(64);
@SerializedName("downloadType")
private StringProperty downloadType = new SimpleStringProperty("bmclapi");
private StringProperty downloadType = new SimpleStringProperty(DownloadProviders.DEFAULT_RAW_PROVIDER_ID);
@SerializedName("autoChooseDownloadType")
private BooleanProperty autoChooseDownloadType = new SimpleBooleanProperty(true);
@SerializedName("versionListSource")
private StringProperty versionListSource = new SimpleStringProperty("balanced");
@SerializedName("configurations")
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
private SimpleMapProperty<String, Profile> configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>()));
@SerializedName("selectedAccount")
private StringProperty selectedAccount = new SimpleStringProperty();
@SerializedName("accounts")
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@SerializedName("fontFamily")
private StringProperty fontFamily = new SimpleStringProperty("Consolas");
private StringProperty fontFamily = new SimpleStringProperty();
@SerializedName("fontSize")
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
@SerializedName("launcherFontFamily")
private StringProperty launcherFontFamily = new SimpleStringProperty();
@SerializedName("logLines")
private IntegerProperty logLines = new SimpleIntegerProperty(100);
private ObjectProperty<Integer> logLines = new SimpleObjectProperty<>();
@SerializedName("titleTransparent")
private BooleanProperty titleTransparent = new SimpleBooleanProperty(false);
@SerializedName("authlibInjectorServers")
private ObservableList<AuthlibInjectorServer> authlibInjectorServers = FXCollections.observableArrayList(server -> new Observable[] { server });
@SerializedName("updateChannel")
private ObjectProperty<UpdateChannel> updateChannel = new SimpleObjectProperty<>(UpdateChannel.STABLE);
@SerializedName("addedLittleSkin")
private BooleanProperty addedLittleSkin = new SimpleBooleanProperty(false);
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@SerializedName("acceptPreviewUpdate")
private BooleanProperty acceptPreviewUpdate = new SimpleBooleanProperty(false);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -165,6 +203,12 @@ public final class Config implements Cloneable, Observable {
@SerializedName("preferredLoginType")
private StringProperty preferredLoginType = new SimpleStringProperty();
@SerializedName("animationDisabled")
private BooleanProperty animationDisabled = new SimpleBooleanProperty();
@SerializedName("shownTips")
private ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
private transient ObservableHelper helper = new ObservableHelper(this);
public Config() {
@@ -185,11 +229,6 @@ public final class Config implements Cloneable, Observable {
return CONFIG_GSON.toJson(this);
}
@Override
public Config clone() {
return fromJson(this.toJson());
}
// Getters & Setters & Properties
public String getSelectedProfile() {
return selectedProfile.get();
@@ -227,6 +266,18 @@ public final class Config implements Cloneable, Observable {
return backgroundImage;
}
public String getBackgroundImageUrl() {
return backgroundImageUrl.get();
}
public StringProperty backgroundImageUrlProperty() {
return backgroundImageUrl;
}
public void setBackgroundImageUrl(String backgroundImageUrl) {
this.backgroundImageUrl.set(backgroundImageUrl);
}
public EnumCommonDirectory getCommonDirType() {
return commonDirType.get();
}
@@ -335,6 +386,30 @@ public final class Config implements Cloneable, Observable {
return proxyPass;
}
public double getX() {
return x.get();
}
public DoubleProperty xProperty() {
return x;
}
public void setX(double x) {
this.x.set(x);
}
public double getY() {
return y.get();
}
public DoubleProperty yProperty() {
return y;
}
public void setY(double y) {
this.y.set(y);
}
public double getWidth() {
return width.get();
}
@@ -383,6 +458,30 @@ public final class Config implements Cloneable, Observable {
return localization;
}
public boolean getAutoDownloadThreads() {
return autoDownloadThreads.get();
}
public BooleanProperty autoDownloadThreadsProperty() {
return autoDownloadThreads;
}
public void setAutoDownloadThreads(boolean autoDownloadThreads) {
this.autoDownloadThreads.set(autoDownloadThreads);
}
public int getDownloadThreads() {
return downloadThreads.get();
}
public IntegerProperty downloadThreadsProperty() {
return downloadThreads;
}
public void setDownloadThreads(int downloadThreads) {
this.downloadThreads.set(downloadThreads);
}
public String getDownloadType() {
return downloadType.get();
}
@@ -395,10 +494,46 @@ public final class Config implements Cloneable, Observable {
return downloadType;
}
public ObservableMap<String, Profile> getConfigurations() {
public boolean isAutoChooseDownloadType() {
return autoChooseDownloadType.get();
}
public BooleanProperty autoChooseDownloadTypeProperty() {
return autoChooseDownloadType;
}
public void setAutoChooseDownloadType(boolean autoChooseDownloadType) {
this.autoChooseDownloadType.set(autoChooseDownloadType);
}
public String getVersionListSource() {
return versionListSource.get();
}
public void setVersionListSource(String versionListSource) {
this.versionListSource.set(versionListSource);
}
public StringProperty versionListSourceProperty() {
return versionListSource;
}
public MapProperty<String, Profile> getConfigurations() {
return configurations;
}
public String getSelectedAccount() {
return selectedAccount.get();
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public StringProperty selectedAccountProperty() {
return selectedAccount;
}
public ObservableList<Map<Object, Object>> getAccountStorages() {
return accountStorages;
}
@@ -427,15 +562,27 @@ public final class Config implements Cloneable, Observable {
return fontSize;
}
public int getLogLines() {
public String getLauncherFontFamily() {
return launcherFontFamily.get();
}
public StringProperty launcherFontFamilyProperty() {
return launcherFontFamily;
}
public void setLauncherFontFamily(String launcherFontFamily) {
this.launcherFontFamily.set(launcherFontFamily);
}
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;
}
@@ -443,16 +590,16 @@ public final class Config implements Cloneable, Observable {
return authlibInjectorServers;
}
public UpdateChannel getUpdateChannel() {
return updateChannel.get();
public boolean isAddedLittleSkin() {
return addedLittleSkin.get();
}
public ObjectProperty<UpdateChannel> updateChannelProperty() {
return updateChannel;
public BooleanProperty addedLittleSkinProperty() {
return addedLittleSkin;
}
public void setUpdateChannel(UpdateChannel updateChannel) {
this.updateChannel.set(updateChannel);
public void setAddedLittleSkin(boolean addedLittleSkin) {
this.addedLittleSkin.set(addedLittleSkin);
}
public int getConfigVersion() {
@@ -490,4 +637,56 @@ public final class Config implements Cloneable, Observable {
public StringProperty preferredLoginTypeProperty() {
return preferredLoginType;
}
public boolean isAnimationDisabled() {
return animationDisabled.get();
}
public BooleanProperty animationDisabledProperty() {
return animationDisabled;
}
public void setAnimationDisabled(boolean animationDisabled) {
this.animationDisabled.set(animationDisabled);
}
public boolean isTitleTransparent() {
return titleTransparent.get();
}
public BooleanProperty titleTransparentProperty() {
return titleTransparent;
}
public void setTitleTransparent(boolean titleTransparent) {
this.titleTransparent.set(titleTransparent);
}
public String getPromptedVersion() {
return promptedVersion.get();
}
public StringProperty promptedVersionProperty() {
return promptedVersion;
}
public void setPromptedVersion(String promptedVersion) {
this.promptedVersion.set(promptedVersion);
}
public boolean isAcceptPreviewUpdate() {
return acceptPreviewUpdate.get();
}
public BooleanProperty acceptPreviewUpdateProperty() {
return acceptPreviewUpdate;
}
public void setAcceptPreviewUpdate(boolean acceptPreviewUpdate) {
this.acceptPreviewUpdate.set(acceptPreviewUpdate);
}
public ObservableMap<String, Object> getShownTips() {
return shownTips;
}
}
@@ -17,33 +17,35 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
import java.util.logging.Level;
import java.nio.file.*;
import java.util.Locale;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ConfigHolder {
private ConfigHolder() {}
private ConfigHolder() {
}
public static final String CONFIG_FILENAME = "hmcl.json";
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
public static final Path GLOBAL_CONFIG_PATH = Metadata.HMCL_GLOBAL_DIRECTORY.resolve("config.json");
private static Path configLocation;
private static Config configInstance;
private static GlobalConfig globalConfigInstance;
private static boolean newlyCreated;
private static boolean ownerChanged = false;
private static boolean unsupportedVersion = false;
public static Config config() {
if (configInstance == null) {
@@ -52,52 +54,78 @@ public final class ConfigHolder {
return configInstance;
}
public static GlobalConfig globalConfig() {
if (globalConfigInstance == null) {
throw new IllegalStateException("Configuration hasn't been loaded");
}
return globalConfigInstance;
}
public static Path configLocation() {
return configLocation;
}
public static boolean isNewlyCreated() {
return newlyCreated;
}
public synchronized static void init() throws IOException {
public static boolean isOwnerChanged() {
return ownerChanged;
}
public static boolean isUnsupportedVersion() {
return unsupportedVersion;
}
public static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
}
configLocation = locateConfig();
LOG.log(Level.INFO, "Config location: " + configLocation);
LOG.info("Config location: " + configLocation);
configInstance = loadConfig();
configInstance.addListener(source -> markConfigDirty());
if (!unsupportedVersion)
configInstance.addListener(source -> FileSaver.save(configLocation, configInstance.toJson()));
globalConfigInstance = loadGlobalConfig();
globalConfigInstance.addListener(source -> FileSaver.save(GLOBAL_CONFIG_PATH, globalConfigInstance.toJson()));
Locale.setDefault(config().getLocalization().getLocale());
I18n.setLocale(configInstance.getLocalization());
LOG.setLogRetention(globalConfig().getLogRetention());
Settings.init();
if (newlyCreated) {
saveConfigSync();
// hide the config file on windows
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Files.setAttribute(configLocation, "dos:hidden", true);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to set hidden attribute of " + configLocation, e);
}
}
LOG.info("Creating config file " + configLocation);
FileUtils.saveSafely(configLocation, configInstance.toJson());
}
if (!Files.isWritable(configLocation)) {
// the config cannot be saved
// throw up the error now to prevent further data loss
throw new IOException("Config at " + configLocation + " is not writable");
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
&& configLocation.getFileSystem() == FileSystems.getDefault()
&& configLocation.toFile().canWrite()) {
// There are some serious problems with the implementation of Samba or OpenJDK
throw new SambaException();
} else {
// the config cannot be saved
// throw up the error now to prevent further data loss
throw new IOException("Config at " + configLocation + " is not writable");
}
}
}
private static Path locateConfig() {
Path exePath = Paths.get("");
Path defaultConfigFile = Metadata.HMCL_CURRENT_DIRECTORY.resolve(CONFIG_FILENAME);
if (Files.isRegularFile(defaultConfigFile))
return defaultConfigFile;
try {
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
.toURI()).toAbsolutePath();
if (Files.isRegularFile(jarPath)) {
Path jarPath = JarUtils.thisJarPath();
if (jarPath != null && Files.isRegularFile(jarPath) && Files.isWritable(jarPath)) {
jarPath = jarPath.getParent();
exePath = jarPath;
Path config = jarPath.resolve(CONFIG_FILENAME);
if (Files.isRegularFile(config))
@@ -120,51 +148,64 @@ public final class ConfigHolder {
return dotConfig;
// create new
return exePath.resolve(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? CONFIG_FILENAME : CONFIG_FILENAME_LINUX);
return defaultConfigFile;
}
private static Config loadConfig() throws IOException {
if (Files.exists(configLocation)) {
try {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
&& "root".equals(System.getProperty("user.name"))
&& !"root".equals(Files.getOwner(configLocation).getName())) {
ownerChanged = true;
}
} catch (IOException e1) {
LOG.warning("Failed to get owner");
}
try {
String content = FileUtils.readText(configLocation);
Config deserialized = Config.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
Map<?, ?> raw = new Gson().fromJson(content, Map.class);
ConfigUpgrader.upgradeConfig(deserialized, raw);
int configVersion = deserialized.getConfigVersion();
if (configVersion < Config.CURRENT_VERSION) {
ConfigUpgrader.upgradeConfig(deserialized, content);
} else if (configVersion > Config.CURRENT_VERSION) {
unsupportedVersion = true;
LOG.warning(String.format("Current HMCL only support the configuration version up to %d. However, the version now is %d.", Config.CURRENT_VERSION, configVersion));
}
return deserialized;
}
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Malformed config.", e);
LOG.warning("Malformed config.", e);
}
}
LOG.info("Creating an empty config");
newlyCreated = true;
return new Config();
}
private static InvocationDispatcher<String> configWriter = InvocationDispatcher.runOn(Lang::thread, content -> {
try {
writeToConfig(content);
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save config", e);
// Global Config
private static GlobalConfig loadGlobalConfig() throws IOException {
if (Files.exists(GLOBAL_CONFIG_PATH)) {
try {
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
GlobalConfig deserialized = GlobalConfig.fromJson(content);
if (deserialized == null) {
LOG.info("Config is empty");
} else {
return deserialized;
}
} catch (JsonParseException e) {
LOG.warning("Malformed config.", e);
}
}
});
private static void writeToConfig(String content) throws IOException {
LOG.info("Saving config");
synchronized (configLocation) {
Files.write(configLocation, content.getBytes(UTF_8));
}
LOG.info("Creating an empty global config");
return new GlobalConfig();
}
static void markConfigDirty() {
configWriter.accept(configInstance.toJson());
}
private static void saveConfigSync() throws IOException {
writeToConfig(configInstance.toJson());
}
}
@@ -17,113 +17,80 @@
*/
package org.jackhuang.hmcl.setting;
import com.google.gson.Gson;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
final class ConfigUpgrader {
private static final int VERSION = 0;
private ConfigUpgrader() {
}
/**
* This method is for the compatibility with old HMCL 3.x as well as HMCL 2.x.
* This method is for the compatibility with old HMCL versions.
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
* @return true if config version is upgraded
* @param rawContent raw json content of the config settings without modification
*/
static boolean upgradeConfig(Config deserialized, Map<?, ?> rawJson) {
boolean upgraded;
if (deserialized.getConfigVersion() < VERSION) {
deserialized.setConfigVersion(VERSION);
// TODO: Add upgrade code here.
upgraded = true;
} else {
upgraded = false;
}
static void upgradeConfig(Config deserialized, String rawContent) {
int configVersion = deserialized.getConfigVersion();
upgradeV2(deserialized, rawJson);
upgradeV3(deserialized, rawJson);
if (configVersion >= Config.CURRENT_VERSION)
return;
return upgraded;
}
LOG.info(String.format("Updating configuration from %d to %d.", configVersion, Config.CURRENT_VERSION));
Map<?, ?> rawJson = Collections.unmodifiableMap(new Gson().<Map<?, ?>>fromJson(rawContent, Map.class));
/**
* Upgrade configuration of HMCL 2.x
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
private static void upgradeV2(Config deserialized, Map<?, ?> rawJson) {
// Convert OfflineAccounts whose stored uuid is important.
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
String selected = rawJson.containsKey("selectedAccount") ? null
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
if (configVersion < 1) {
// Upgrade configuration of HMCL 2.x: Convert OfflineAccounts whose stored uuid is important.
tryCast(rawJson.get("auth"), Map.class).ifPresent(auth -> {
tryCast(auth.get("offline"), Map.class).ifPresent(offline -> {
String selected = rawJson.containsKey("selectedAccount") ? null
: tryCast(offline.get("IAuthenticator_UserName"), String.class).orElse(null);
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
((Map<?, ?>) uuidMap).forEach((key, value) -> {
Map<Object, Object> storage = new HashMap<>();
storage.put("type", "offline");
storage.put("username", key);
storage.put("uuid", value);
if (key.equals(selected)) {
storage.put("selected", true);
}
deserialized.getAccountStorages().add(storage);
tryCast(offline.get("uuidMap"), Map.class).ifPresent(uuidMap -> {
((Map<?, ?>) uuidMap).forEach((key, value) -> {
Map<Object, Object> storage = new HashMap<>();
storage.put("type", "offline");
storage.put("username", key);
storage.put("uuid", value);
if (key.equals(selected)) {
storage.put("selected", true);
}
deserialized.getAccountStorages().add(storage);
});
});
});
});
});
}
/**
* Upgrade configuration of HMCL earlier than 3.1.70
*
* @param deserialized deserialized config settings
* @param rawJson raw json structure of the config settings without modification
*/
private static void upgradeV3(Config deserialized, Map<?, ?> rawJson) {
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
if (!rawJson.containsKey("backgroundType"))
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
if (!rawJson.containsKey("hasProxy"))
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
if (!rawJson.containsKey("hasProxyAuth"))
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
// Upgrade configuration of HMCL earlier than 3.1.70
if (!rawJson.containsKey("commonDirType"))
deserialized.setCommonDirType(deserialized.getCommonDirectory().equals(Settings.getDefaultCommonDirectory()) ? EnumCommonDirectory.DEFAULT : EnumCommonDirectory.CUSTOM);
if (!rawJson.containsKey("backgroundType"))
deserialized.setBackgroundImageType(StringUtils.isNotBlank(deserialized.getBackgroundImage()) ? EnumBackgroundImage.CUSTOM : EnumBackgroundImage.DEFAULT);
if (!rawJson.containsKey("hasProxy"))
deserialized.setHasProxy(StringUtils.isNotBlank(deserialized.getProxyHost()));
if (!rawJson.containsKey("hasProxyAuth"))
deserialized.setHasProxyAuth(StringUtils.isNotBlank(deserialized.getProxyUser()));
if (!rawJson.containsKey("downloadType")) {
tryCast(rawJson.get("downloadtype"), Number.class)
.map(Number::intValue)
.ifPresent(id -> {
if (id == 0) {
deserialized.setDownloadType("mojang");
} else if (id == 1) {
deserialized.setDownloadType("bmclapi");
}
});
if (!rawJson.containsKey("downloadType")) {
tryCast(rawJson.get("downloadtype"), Number.class)
.map(Number::intValue)
.ifPresent(id -> {
if (id == 0) {
deserialized.setDownloadType("mojang");
} else if (id == 1) {
deserialized.setDownloadType("bmclapi");
}
});
}
}
tryCast(rawJson.get("selectedAccount"), String.class)
.ifPresent(selected -> {
deserialized.getAccountStorages().stream()
.filter(storage -> {
Object type = storage.get("type");
if ("offline".equals(type)) {
return selected.equals(storage.get("username") + ":" + storage.get("username"));
} else if ("yggdrasil".equals(type) || "authlibInjector".equals(type)) {
return selected.equals(storage.get("username") + ":" + storage.get("displayName"));
} else {
return false;
}
})
.findFirst()
.ifPresent(storage -> storage.put("selected", true));
});
deserialized.setConfigVersion(Config.CURRENT_VERSION);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,49 +17,99 @@
*/
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.download.AdaptedDownloadProvider;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MojangDownloadProvider;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class DownloadProviders {
private DownloadProviders() {}
private static final AdaptedDownloadProvider DOWNLOAD_PROVIDER = new AdaptedDownloadProvider();
private static DownloadProvider currentDownloadProvider;
public static final Map<String, DownloadProvider> providersById;
public static final Map<String, DownloadProvider> rawProviders;
private static final AdaptedDownloadProvider fileDownloadProvider = new AdaptedDownloadProvider();
public static final String DEFAULT_PROVIDER_ID = "mcbbs";
private static final MojangDownloadProvider MOJANG;
private static final BMCLAPIDownloadProvider BMCLAPI;
public static final String DEFAULT_PROVIDER_ID = "balanced";
public static final String DEFAULT_RAW_PROVIDER_ID = "bmclapi";
private static final InvalidationListener observer;
static {
String bmclapiRoot = "https://bmclapi2.bangbang93.com";
String bmclapiRootOverride = System.getProperty("hmcl.bmclapi.override");
if (bmclapiRootOverride != null) bmclapiRoot = bmclapiRootOverride;
MOJANG = new MojangDownloadProvider();
BMCLAPI = new BMCLAPIDownloadProvider(bmclapiRoot);
rawProviders = mapOf(
pair("mojang", MOJANG),
pair("bmclapi", BMCLAPI)
);
AdaptedDownloadProvider fileProvider = new AdaptedDownloadProvider();
fileProvider.setDownloadProviderCandidates(Arrays.asList(BMCLAPI, MOJANG));
BalancedDownloadProvider balanced = new BalancedDownloadProvider(MOJANG, BMCLAPI);
providersById = mapOf(
pair("mojang", new MojangDownloadProvider()),
pair("bmclapi", new BMCLAPIDownloadProvider(bmclapiRoot)),
pair("mcbbs", new BMCLAPIDownloadProvider("https://download.mcbbs.net")));
pair("official", new AutoDownloadProvider(MOJANG, fileProvider)),
pair("balanced", new AutoDownloadProvider(balanced, fileProvider)),
pair("mirror", new AutoDownloadProvider(BMCLAPI, fileProvider)));
observer = FXUtils.observeWeak(() -> {
FetchTask.setDownloadExecutorConcurrency(
config().getAutoDownloadThreads() ? DEFAULT_CONCURRENCY : config().getDownloadThreads());
}, config().autoDownloadThreadsProperty(), config().downloadThreadsProperty());
}
static void init() {
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
DownloadProvider primary = Optional.ofNullable(providersById.get(config().getDownloadType()))
FXUtils.onChangeAndOperate(config().versionListSourceProperty(), versionListSource -> {
if (!providersById.containsKey(versionListSource)) {
config().setVersionListSource(DEFAULT_PROVIDER_ID);
return;
}
currentDownloadProvider = Optional.ofNullable(providersById.get(versionListSource))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
DOWNLOAD_PROVIDER.setDownloadProviderCandidates(
});
if (!rawProviders.containsKey(config().getDownloadType())) {
config().setDownloadType(DEFAULT_RAW_PROVIDER_ID);
}
FXUtils.onChangeAndOperate(config().downloadTypeProperty(), downloadType -> {
DownloadProvider primary = Optional.ofNullable(rawProviders.get(downloadType))
.orElse(rawProviders.get(DEFAULT_RAW_PROVIDER_ID));
fileDownloadProvider.setDownloadProviderCandidates(
Stream.concat(
Stream.of(primary),
providersById.values().stream().filter(x -> x != primary)
rawProviders.values().stream().filter(x -> x != primary)
).collect(Collectors.toList())
);
});
@@ -73,23 +123,46 @@ public final class DownloadProviders {
return DEFAULT_PROVIDER_ID;
}
public static AdaptedDownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
AdaptedDownloadProvider adaptedDownloadProvider = new AdaptedDownloadProvider();
DownloadProvider primary = Optional.ofNullable(providersById.get(primaryId))
public static DownloadProvider getDownloadProviderByPrimaryId(String primaryId) {
return Optional.ofNullable(providersById.get(primaryId))
.orElse(providersById.get(DEFAULT_PROVIDER_ID));
adaptedDownloadProvider.setDownloadProviderCandidates(
Stream.concat(
Stream.of(primary),
providersById.values().stream().filter(x -> x != primary)
).collect(Collectors.toList())
);
return adaptedDownloadProvider;
}
/**
* Get current primary preferred download provider
*/
public static AdaptedDownloadProvider getDownloadProvider() {
return DOWNLOAD_PROVIDER;
public static DownloadProvider getDownloadProvider() {
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
}
public static String localizeErrorMessage(Throwable exception) {
if (exception instanceof DownloadException) {
URL url = ((DownloadException) exception).getUrl();
if (exception.getCause() instanceof SocketTimeoutException) {
return i18n("install.failed.downloading.timeout", url);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
return i18n("download.code." + responseCodeException.getResponseCode(), url);
} else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception.getCause() instanceof FileNotFoundException) {
return i18n("download.code.404", url);
} else if (exception.getCause() instanceof AccessDeniedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException) {
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.ssl_handshake");
} else {
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception instanceof ArtifactMalformedException) {
return i18n("exception.artifact_malformed");
} else if (exception instanceof CancellationException) {
return i18n("message.cancelled");
}
return StringUtils.getStackTrace(exception);
}
}
@@ -19,5 +19,8 @@ package org.jackhuang.hmcl.setting;
public enum EnumBackgroundImage {
DEFAULT,
CUSTOM
CUSTOM,
CLASSIC,
NETWORK,
TRANSLUCENT
}
@@ -0,0 +1,261 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.text.Font;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Locale;
import java.util.Objects;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class FontManager {
public static final String[] FONT_EXTENSIONS = {
"ttf", "otf", "woff"
};
public static final double DEFAULT_FONT_SIZE = 12.0f;
private static final Lazy<Font> DEFAULT_FONT = new Lazy<>(() -> {
Font font = tryLoadDefaultFont(Metadata.HMCL_CURRENT_DIRECTORY);
if (font != null)
return font;
font = tryLoadDefaultFont(Metadata.CURRENT_DIRECTORY);
if (font != null)
return font;
font = tryLoadDefaultFont(Metadata.HMCL_GLOBAL_DIRECTORY);
if (font != null)
return font;
Path thisJar = JarUtils.thisJarPath();
if (thisJar != null && thisJar.getParent() != null) {
font = tryLoadDefaultFont(thisJar.getParent());
if (font != null)
return font;
}
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()
&& Locale.getDefault() != Locale.ROOT
&& !"en".equals(Locale.getDefault().getLanguage()))
return findByFcMatch();
else
return null;
});
private static final ObjectProperty<FontReference> fontProperty;
static {
String fontFamily = config().getLauncherFontFamily();
if (fontFamily == null)
fontFamily = System.getProperty("hmcl.font.override");
if (fontFamily == null)
fontFamily = System.getenv("HMCL_FONT");
FontReference fontReference;
if (fontFamily == null) {
Font defaultFont = DEFAULT_FONT.get();
fontReference = defaultFont != null ? new FontReference(defaultFont) : null;
} else
fontReference = new FontReference(fontFamily);
fontProperty = new SimpleObjectProperty<>(fontReference);
LOG.info("Font: " + (fontReference != null ? fontReference.getFamily() : "System"));
fontProperty.addListener((obs, oldValue, newValue) -> {
if (newValue != null)
config().setLauncherFontFamily(newValue.getFamily());
else
config().setLauncherFontFamily(null);
});
}
private static Font tryLoadDefaultFont(Path dir) {
for (String extension : FONT_EXTENSIONS) {
Path path = dir.resolve("font." + extension);
if (Files.isRegularFile(path)) {
try {
Font font = Font.loadFont(path.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (font != null) {
return font;
}
} catch (MalformedURLException ignored) {
}
LOG.warning("Failed to load font " + path);
}
}
return null;
}
public static Font findByFcMatch() {
Path fcMatch = SystemUtils.which("fc-match");
if (fcMatch == null)
return null;
try {
String result = SystemUtils.run(fcMatch.toString(),
":lang=" + Locale.getDefault().toLanguageTag(),
"--format", "%{family}\\n%{file}").trim();
String[] results = result.split("\\n");
if (results.length != 2 || results[0].isEmpty() || results[1].isEmpty()) {
LOG.warning("Unexpected output from fc-match: " + result);
return null;
}
String family = results[0].trim();
String path = results[1];
Path file = Paths.get(path).toAbsolutePath().normalize();
if (!Files.isRegularFile(file)) {
LOG.warning("Font file does not exist: " + path);
return null;
}
if (JavaRuntime.CURRENT_VERSION >= 9) {
try {
MethodHandle methodHandle = MethodHandles.publicLookup().findStatic(Font.class, "loadFonts",
MethodType.methodType(Font[].class, String.class, double.class));
Font[] fonts = (Font[]) methodHandle.invokeExact(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (fonts == null) {
LOG.warning("Failed to load font from " + path);
return null;
} else if (fonts.length == 0) {
LOG.warning("No fonts loaded from " + path);
return null;
}
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(family)) {
return font;
}
}
if (family.indexOf(',') >= 0) {
for (String candidateFamily : family.split(",")) {
for (Font font : fonts) {
if (font.getFamily().equalsIgnoreCase(candidateFamily)) {
return font;
}
}
}
}
LOG.warning(String.format("Family '%s' not found in font file '%s'", family, path));
return fonts[0];
} catch (NoSuchMethodException | IllegalAccessException ignored) {
}
}
Font font = Font.loadFont(file.toUri().toURL().toExternalForm(), DEFAULT_FONT_SIZE);
if (font == null)
LOG.warning("Failed to load font from " + path);
return font;
} catch (Throwable e) {
LOG.warning("Failed to get default font with fc-match", e);
return null;
}
}
public static ObjectProperty<FontReference> fontProperty() {
return fontProperty;
}
public static FontReference getFont() {
return fontProperty.get();
}
public static void setFont(FontReference font) {
fontProperty.set(font);
}
public static void setFontFamily(String fontFamily) {
setFont(fontFamily != null ? new FontReference(fontFamily) : null);
}
// https://github.com/HMCL-dev/HMCL/issues/4072
public static final class FontReference {
private final @NotNull String family;
private final @Nullable String style;
public FontReference(@NotNull String family) {
this.family = Objects.requireNonNull(family);
this.style = null;
}
public FontReference(@NotNull Font font) {
this.family = font.getFamily();
this.style = font.getStyle();
}
public @NotNull String getFamily() {
return family;
}
public @Nullable String getStyle() {
return style;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof FontReference))
return false;
FontReference that = (FontReference) o;
return Objects.equals(family, that.family) && Objects.equals(style, that.style);
}
@Override
public int hashCode() {
return Objects.hash(family, style);
}
@Override
public String toString() {
return String.format("FontReference[family='%s', style='%s']", family, style);
}
}
private FontManager() {
}
}
@@ -0,0 +1,213 @@
/*
* 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.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Type;
import java.util.*;
@JsonAdapter(GlobalConfig.Serializer.class)
public final class GlobalConfig implements Observable {
@Nullable
public static GlobalConfig fromJson(String json) throws JsonParseException {
GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
if (loaded == null) {
return null;
}
GlobalConfig instance = new GlobalConfig();
PropertyUtils.copyProperties(loaded, instance);
instance.unknownFields.putAll(loaded.unknownFields);
return instance;
}
private final IntegerProperty agreementVersion = new SimpleIntegerProperty();
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
private final IntegerProperty logRetention = new SimpleIntegerProperty();
private final BooleanProperty enableOfflineAccount = new SimpleBooleanProperty(false);
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);
public GlobalConfig() {
PropertyUtils.attachListener(this, helper);
}
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
helper.removeListener(listener);
}
public String toJson() {
return Config.CONFIG_GSON.toJson(this);
}
public int getAgreementVersion() {
return agreementVersion.get();
}
public IntegerProperty agreementVersionProperty() {
return agreementVersion;
}
public void setAgreementVersion(int agreementVersion) {
this.agreementVersion.set(agreementVersion);
}
public int getPlatformPromptVersion() {
return platformPromptVersion.get();
}
public IntegerProperty platformPromptVersionProperty() {
return platformPromptVersion;
}
public void setPlatformPromptVersion(int platformPromptVersion) {
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 boolean isEnableOfflineAccount() {
return enableOfflineAccount.get();
}
public BooleanProperty enableOfflineAccountProperty() {
return enableOfflineAccount;
}
public void setEnableOfflineAccount(boolean value) {
enableOfflineAccount.set(value);
}
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",
"logRetention",
"userJava",
"disabledJava",
"enableOfflineAccount"
));
@Override
public JsonElement serialize(GlobalConfig src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null) {
return JsonNull.INSTANCE;
}
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.enableOfflineAccount.get())
jsonObject.addProperty("enableOfflineAccount", true);
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()));
}
return jsonObject;
}
@Override
public GlobalConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!(json instanceof JsonObject)) return null;
JsonObject obj = (JsonObject) json;
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));
config.setEnableOfflineAccount(Optional.ofNullable(obj.get("enableOfflineAccount")).map(JsonElement::getAsBoolean).orElse(false));
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())) {
config.unknownFields.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
}
}
return config;
}
}
}
@@ -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
}
@@ -43,5 +43,9 @@ public enum LauncherVisibility {
/**
* Hide the launcher and reopen it when game closes.
*/
HIDE_AND_REOPEN
HIDE_AND_REOPEN;
public boolean isDaemon() {
return this != CLOSE;
}
}
@@ -188,7 +188,7 @@ public final class Profile implements Observable {
global.addListener(listener);
gameDir.addListener(listener);
useRelativePath.addListener(listener);
global.get().addPropertyChangedListener(listener);
global.get().addListener(listener);
selectedVersion.addListener(listener);
}
@@ -208,6 +208,24 @@ public final class Profile implements Observable {
Platform.runLater(observableHelper::invalidate);
}
public static class ProfileVersion {
private final Profile profile;
private final String version;
public ProfileVersion(Profile profile, String version) {
this.profile = profile;
this.version = version;
}
public Profile getProfile() {
return profile;
}
public String getVersion() {
return version;
}
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@Override
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
@@ -20,17 +20,18 @@ package org.jackhuang.hmcl.setting;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static javafx.collections.FXCollections.observableArrayList;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@@ -130,8 +131,11 @@ public final class Profiles {
if (!initialized)
return;
// update storage
config().getConfigurations().clear();
config().getConfigurations().putAll(profiles.stream().collect(Collectors.toMap(Profile::getName, it -> it)));
TreeMap<String, Profile> newConfigurations = new TreeMap<>();
for (Profile profile : profiles) {
newConfigurations.put(profile.getName(), profile);
}
config().getConfigurations().setValue(FXCollections.observableMap(newConfigurations));
}
/**
@@ -204,7 +208,7 @@ public final class Profiles {
return selectedVersion.get();
}
private static final List<Consumer<Profile>> versionsListeners = new LinkedList<>();
private static final List<Consumer<Profile>> versionsListeners = new ArrayList<>(4);
public static void registerVersionsListener(Consumer<Profile> listener) {
Profile profile = getSelectedProfile();
@@ -17,100 +17,121 @@
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableObjectValue;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.io.IOException;
import java.net.*;
import java.util.Collections;
import java.util.List;
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() {
private static final ProxySelector NO_PROXY = new SimpleProxySelector(Proxy.NO_PROXY);
private static final ProxySelector SYSTEM_DEFAULT = Lang.requireNonNullElse(ProxySelector.getDefault(), NO_PROXY);
private static ProxySelector getProxySelector() {
if (config().hasProxy()) {
Proxy.Type proxyType = config().getProxyType();
String host = config().getProxyHost();
int port = config().getProxyPort();
if (proxyType == Proxy.Type.DIRECT || StringUtils.isBlank(host)) {
return NO_PROXY;
} else if (port < 0 || port > 0xFFFF) {
LOG.warning("Illegal proxy port: " + port);
return NO_PROXY;
} else {
return new SimpleProxySelector(new Proxy(proxyType, new InetSocketAddress(host, port)));
}
} else {
return ProxyManager.SYSTEM_DEFAULT;
}
}
private static ObjectBinding<Proxy> proxyProperty;
private static Authenticator getAuthenticator() {
if (config().hasProxy() && config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
public static Proxy getProxy() {
return proxyProperty.get();
}
public static ObservableObjectValue<Proxy> proxyProperty() {
return proxyProperty;
if (username != null || password != null)
return new SimpleAuthenticator(
Lang.requireNonNullElse(username, ""),
Lang.requireNonNullElse(password, "").toCharArray()
);
else
return null;
} else
return null;
}
static void init() {
proxyProperty = Bindings.createObjectBinding(
() -> {
String host = config().getProxyHost();
int port = config().getProxyPort();
if (!config().hasProxy() || StringUtils.isBlank(host) || config().getProxyType() == Proxy.Type.DIRECT) {
return Proxy.NO_PROXY;
} else {
if (port < 0 || port > 0xFFFF) {
LOG.warning("Illegal proxy port: " + port);
return Proxy.NO_PROXY;
}
return new Proxy(config().getProxyType(), new InetSocketAddress(host, port));
}
},
config().proxyTypeProperty(),
config().proxyHostProperty(),
config().proxyPortProperty(),
config().hasProxyProperty());
ProxySelector.setDefault(getProxySelector());
InvalidationListener updateProxySelector = observable -> ProxySelector.setDefault(getProxySelector());
config().proxyTypeProperty().addListener(updateProxySelector);
config().proxyHostProperty().addListener(updateProxySelector);
config().proxyPortProperty().addListener(updateProxySelector);
config().hasProxyProperty().addListener(updateProxySelector);
proxyProperty.addListener(any -> updateSystemProxy());
updateSystemProxy();
Authenticator.setDefault(new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
if (config().hasProxyAuth()) {
String username = config().getProxyUser();
String password = config().getProxyPass();
if (username != null && password != null) {
return new PasswordAuthentication(username, password.toCharArray());
}
}
return null;
}
});
Authenticator.setDefault(getAuthenticator());
InvalidationListener updateAuthenticator = observable -> Authenticator.setDefault(getAuthenticator());
config().hasProxyProperty().addListener(updateAuthenticator);
config().hasProxyAuthProperty().addListener(updateAuthenticator);
config().proxyUserProperty().addListener(updateAuthenticator);
config().proxyPassProperty().addListener(updateAuthenticator);
}
private static void updateSystemProxy() {
Proxy proxy = proxyProperty.get();
if (proxy.type() == Proxy.Type.DIRECT) {
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
} else {
InetSocketAddress address = (InetSocketAddress) proxy.address();
String host = address.getHostString();
String port = String.valueOf(address.getPort());
if (proxy.type() == Type.HTTP) {
System.clearProperty("socksProxyHost");
System.clearProperty("socksProxyPort");
System.setProperty("http.proxyHost", host);
System.setProperty("http.proxyPort", port);
System.setProperty("https.proxyHost", host);
System.setProperty("https.proxyPort", port);
} else if (proxy.type() == Type.SOCKS) {
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
System.setProperty("socksProxyHost", host);
System.setProperty("socksProxyPort", port);
private static final class SimpleProxySelector extends ProxySelector {
private final List<Proxy> proxies;
SimpleProxySelector(Proxy proxy) {
this(Collections.singletonList(proxy));
}
SimpleProxySelector(List<Proxy> proxies) {
this.proxies = proxies;
}
@Override
public List<Proxy> select(URI uri) {
if (uri == null)
throw new IllegalArgumentException("URI can't be null.");
return proxies;
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
if (uri == null || sa == null || ioe == null) {
throw new IllegalArgumentException("Arguments can't be null.");
}
}
@Override
public String toString() {
return "SimpleProxySelector" + proxies;
}
}
private static final class SimpleAuthenticator extends Authenticator {
private final String username;
private final char[] password;
private SimpleAuthenticator(String username, char[] password) {
this.username = username;
this.password = password;
}
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return getRequestorType() == RequestorType.PROXY ? new PasswordAuthentication(username, password) : null;
}
}
private ProxyManager() {
}
}
@@ -0,0 +1,18 @@
package org.jackhuang.hmcl.setting;
public final class SambaException extends RuntimeException {
public SambaException() {
}
public SambaException(String message) {
super(message);
}
public SambaException(String message, Throwable cause) {
super(message, cause);
}
public SambaException(Throwable cause) {
super(cause);
}
}
@@ -20,14 +20,13 @@ package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.io.FileUtils;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import java.util.Locale;
public class Settings {
public final class Settings {
private static Settings instance;
@@ -46,14 +45,12 @@ public class Settings {
}
private Settings() {
config().localizationProperty().addListener(unused -> updateSystemLocale());
updateSystemLocale();
DownloadProviders.init();
ProxyManager.init();
Accounts.init();
Profiles.init();
AuthlibInjectorServers.init();
AnimationUtils.init();
CacheRepository.setInstance(HMCLCacheRepository.REPOSITORY);
HMCLCacheRepository.REPOSITORY.directoryProperty().bind(Bindings.createStringBinding(() -> {
@@ -79,8 +76,4 @@ public class Settings {
return null;
}
}
private static void updateSystemLocale() {
Locale.setDefault(config().getLocalization().getLocale());
}
}
@@ -0,0 +1,163 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class StyleSheets {
private static final int FONT_STYLE_SHEET_INDEX = 0;
private static final int THEME_STYLE_SHEET_INDEX = 1;
private static final ObservableList<String> stylesheets;
static {
String[] array = new String[]{
getFontStyleSheet(),
getThemeStyleSheet(),
"/assets/css/root.css"
};
stylesheets = FXCollections.observableList(Arrays.asList(array));
FontManager.fontProperty().addListener(o -> stylesheets.set(FONT_STYLE_SHEET_INDEX, getFontStyleSheet()));
config().themeProperty().addListener(o -> stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet()));
}
private static String toStyleSheetUri(String styleSheet, String fallback) {
if (FXUtils.JAVAFX_MAJOR_VERSION >= 17)
// JavaFX 17+ support loading stylesheets from data URIs
// https://bugs.openjdk.org/browse/JDK-8267554
return "data:text/css;charset=UTF-8;base64," + Base64.getEncoder().encodeToString(styleSheet.getBytes(StandardCharsets.UTF_8));
else
try {
Path temp = Files.createTempFile("hmcl", ".css");
// For JavaFX 17 or earlier, CssParser uses the default charset
// https://bugs.openjdk.org/browse/JDK-8279328
FileUtils.writeText(temp, styleSheet, Charset.defaultCharset());
temp.toFile().deleteOnExit();
return temp.toUri().toString();
} catch (IOException | NullPointerException e) {
LOG.error("Unable to create stylesheet, fallback to " + fallback, e);
return fallback;
}
}
private static String getFontStyleSheet() {
final String defaultCss = "/assets/css/font.css";
final FontManager.FontReference font = FontManager.getFont();
if (font == null || "System".equals(font.getFamily()))
return defaultCss;
String fontFamily = font.getFamily();
String style = font.getStyle();
String weight = null;
String posture = null;
if (style != null) {
style = style.toLowerCase(Locale.ROOT);
if (style.contains("thin"))
weight = "100";
else if (style.contains("extralight") || style.contains("extra light") || style.contains("ultralight") | style.contains("ultra light"))
weight = "200";
else if (style.contains("medium"))
weight = "500";
else if (style.contains("semibold") || style.contains("semi bold") || style.contains("demibold") || style.contains("demi bold"))
weight = "600";
else if (style.contains("extrabold") || style.contains("extra bold") || style.contains("ultrabold") || style.contains("ultra bold"))
weight = "800";
else if (style.contains("black") || style.contains("heavy"))
weight = "900";
else if (style.contains("light"))
weight = "lighter";
else if (style.contains("bold"))
weight = "bold";
posture = style.contains("italic") || style.contains("oblique") ? "italic" : null;
}
StringBuilder builder = new StringBuilder();
builder.append(".root {");
builder.append("-fx-font-family:\"").append(fontFamily).append("\";");
if (weight != null)
builder.append("-fx-font-weight:").append(weight).append(";");
if (posture != null)
builder.append("-fx-font-style:").append(posture).append(";");
builder.append('}');
return toStyleSheetUri(builder.toString(), fontFamily);
}
private static String rgba(Color color, double opacity) {
return String.format("rgba(%d, %d, %d, %.1f)",
(int) Math.ceil(color.getRed() * 256),
(int) Math.ceil(color.getGreen() * 256),
(int) Math.ceil(color.getBlue() * 256),
opacity);
}
private static String getThemeStyleSheet() {
final String blueCss = "/assets/css/blue.css";
Theme theme = config().getTheme();
if (theme == null || theme.getPaint().equals(Theme.BLUE.getPaint()))
return blueCss;
return toStyleSheetUri(".root {" +
"-fx-base-color:" + theme.getColor() + ';' +
"-fx-base-darker-color: derive(-fx-base-color, -10%);" +
"-fx-base-check-color: derive(-fx-base-color, 30%);" +
"-fx-rippler-color:" + rgba(theme.getPaint(), 0.3) + ';' +
"-fx-base-rippler-color: derive(" + rgba(theme.getPaint(), 0.3) + ", 100%);" +
"-fx-base-disabled-text-fill:" + rgba(theme.getForegroundColor(), 0.7) + ";" +
"-fx-base-text-fill:" + Theme.getColorDisplayName(theme.getForegroundColor()) + ";" +
"-theme-thumb:" + rgba(theme.getPaint(), 0.7) + ";" +
'}', blueCss);
}
public static void init(Scene scene) {
Bindings.bindContent(scene.getStylesheets(), stylesheets);
}
private StyleSheets() {
}
}
@@ -23,25 +23,20 @@ import com.google.gson.stream.JsonWriter;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ResourceNotFoundError;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.File;
import java.io.IOException;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@JsonAdapter(Theme.TypeAdapter.class)
public class Theme {
public final class Theme {
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
public static final Color BLACK = Color.web("#292929");
public static final Color[] SUGGESTED_COLORS = new Color[]{
Color.web("#5C6BC0"), // blue
Color.web("#3D6DA3"), // blue
Color.web("#283593"), // dark blue
Color.web("#43A047"), // green
Color.web("#E67E22"), // orange
@@ -49,13 +44,18 @@ public class Theme {
Color.web("#B71C1C") // red
};
public static Theme getTheme() {
Theme theme = config().getTheme();
return theme == null ? BLUE : theme;
}
private final Color paint;
private final String color;
private final String name;
Theme(String name, String color) {
this.name = name;
this.color = color;
this.color = Objects.requireNonNull(color);
this.paint = Color.web(color);
}
@@ -67,42 +67,22 @@ public class Theme {
return color;
}
public Color getPaint() {
return paint;
}
public boolean isCustom() {
return name.startsWith("#");
}
public boolean isLight() {
return Color.web(color).grayscale().getRed() >= 0.5;
return paint.grayscale().getRed() >= 0.5;
}
public Color getForegroundColor() {
return isLight() ? Color.BLACK : Color.WHITE;
}
public String[] getStylesheets() {
Color textFill = getForegroundColor();
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(ResourceNotFoundError.getResourceAsStream("/assets/css/custom.css"))
.replace("%base-color%", color)
.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(System.getProperty("hmcl.font.override")).map(fontFamily -> "-fx-font-family: " + fontFamily + ";").orElse("")));
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
css = "/assets/css/blue.css";
}
return new String[]{
css,
"/assets/css/root.css"
};
}
public static Theme custom(String color) {
if (!color.startsWith("#"))
throw new IllegalArgumentException();
@@ -112,50 +92,67 @@ public class Theme {
public static Optional<Theme> getTheme(String name) {
if (name == null)
return Optional.empty();
else if (name.equalsIgnoreCase("blue"))
return Optional.of(custom("#5C6BC0"));
else if (name.equalsIgnoreCase("darker_blue"))
return Optional.of(custom("#283593"));
else if (name.equalsIgnoreCase("green"))
return Optional.of(custom("#43A047"));
else if (name.equalsIgnoreCase("orange"))
return Optional.of(custom("#E67E22"));
else if (name.equalsIgnoreCase("purple"))
return Optional.of(custom("#9C27B0"));
else if (name.equalsIgnoreCase("red"))
return Optional.of(custom("#F44336"));
if (name.startsWith("#"))
else if (name.startsWith("#"))
try {
Color.web(name);
return Optional.of(custom(name));
} catch (IllegalArgumentException ignore) {
}
else {
String color = null;
switch (name.toLowerCase(Locale.ROOT)) {
case "blue":
return Optional.of(BLUE);
case "darker_blue":
color = "#283593";
break;
case "green":
color = "#43A047";
break;
case "orange":
color = "#E67E22";
break;
case "purple":
color = "#9C27B0";
break;
case "red":
color = "#F44336";
}
if (color != null)
return Optional.of(new Theme(name, color));
}
return Optional.empty();
}
public static String getColorDisplayName(Color c) {
return c != null ? String.format("#%02x%02x%02x", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)).toUpperCase() : null;
return c != null ? String.format("#%02X%02X%02X", Math.round(c.getRed() * 255.0D), Math.round(c.getGreen() * 255.0D), Math.round(c.getBlue() * 255.0D)) : null;
}
private static ObjectBinding<Color> FOREGROUND_FILL;
public static ObjectBinding<Color> foregroundFillBinding() {
return BindingMapping.of(config().themeProperty())
.map(Theme::getForegroundColor);
if (FOREGROUND_FILL == null)
FOREGROUND_FILL = Bindings.createObjectBinding(
() -> Theme.getTheme().getForegroundColor(),
config().themeProperty()
);
return FOREGROUND_FILL;
}
public static ObjectBinding<Color> blackFillBinding() {
return Bindings.createObjectBinding(() -> BLACK);
public static Color blackFill() {
return BLACK;
}
public static ObjectBinding<Color> whiteFillBinding() {
return Bindings.createObjectBinding(() -> Color.WHITE);
public static Color whiteFill() {
return Color.WHITE;
}
public static class TypeAdapter extends com.google.gson.TypeAdapter<Theme> {
@Override
public void write(JsonWriter out, Theme value) throws IOException {
out.value(value.getName().toLowerCase());
out.value(value.getName().toLowerCase(Locale.ROOT));
}
@Override
@@ -0,0 +1,49 @@
/*
* 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.setting;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.ui.FXUtils;
public enum VersionIconType {
DEFAULT("/assets/img/grass.png"),
GRASS("/assets/img/grass.png"),
CHEST("/assets/img/chest.png"),
CHICKEN("/assets/img/chicken.png"),
COMMAND("/assets/img/command.png"),
OPTIFINE("/assets/img/optifine.png"),
CRAFT_TABLE("/assets/img/craft_table.png"),
FABRIC("/assets/img/fabric.png"),
FORGE("/assets/img/forge.png"),
NEO_FORGE("/assets/img/neoforge.png"),
FURNACE("/assets/img/furnace.png"),
QUILT("/assets/img/quilt.png");
// Please append new items at last
private final String resourceUrl;
VersionIconType(String resourceUrl) {
this.resourceUrl = resourceUrl;
}
public Image getIcon() {
return FXUtils.newBuiltinImage(resourceUrl);
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -20,42 +20,50 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.*;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.SystemInfo;
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.Optional;
import java.util.*;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
*
* @author huangyuhui
*/
@JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting implements Cloneable {
public final class VersionSetting implements Cloneable, Observable {
public transient String id;
private static final int SUGGESTED_MEMORY;
private boolean global = false;
public boolean isGlobal() {
return global;
static {
double totalMemoryMB = MEGABYTES.convertFromBytes(SystemInfo.getTotalMemorySize());
SUGGESTED_MEMORY = totalMemoryMB >= 32768
? 8192
: Integer.max((int) (Math.round(totalMemoryMB / 4.0 / 128.0) * 128), 256);
}
public void setGlobal(boolean global) {
this.global = global;
private final transient ObservableHelper helper = new ObservableHelper(this);
public VersionSetting() {
PropertyUtils.attachListener(this, helper);
}
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", false);
private final BooleanProperty usesGlobalProperty = new SimpleBooleanProperty(this, "usesGlobal", true);
public BooleanProperty usesGlobalProperty() {
return usesGlobalProperty;
@@ -66,7 +74,7 @@ public final class VersionSetting implements Cloneable {
* 1. Global settings.
* 2. Version settings.
* If a version claims that it uses global settings, its version setting will be disabled.
*
* <p>
* Defaults false because if one version uses global first, custom version file will not be generated.
*/
public boolean isUsesGlobal() {
@@ -79,29 +87,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 void setJavaAutoSelected() {
setJavaVersionType(JavaVersionType.AUTO);
setJavaVersion("");
setDefaultJavaPath(null);
}
@@ -115,10 +137,47 @@ public final class VersionSetting implements Cloneable {
return defaultJavaPathProperty.get();
}
public StringProperty defaultJavaPathPropertyProperty() {
return defaultJavaPathProperty;
}
public void setDefaultJavaPath(String defaultJavaPath) {
defaultJavaPathProperty.set(defaultJavaPath);
}
/**
* 0 - .minecraft/versions/&lt;version&gt;/natives/<br/>
*/
private final ObjectProperty<NativesDirectoryType> nativesDirTypeProperty = new SimpleObjectProperty<>(this, "nativesDirType", NativesDirectoryType.VERSION_FOLDER);
public ObjectProperty<NativesDirectoryType> nativesDirTypeProperty() {
return nativesDirTypeProperty;
}
public NativesDirectoryType getNativesDirType() {
return nativesDirTypeProperty.get();
}
public void setNativesDirType(NativesDirectoryType nativesDirType) {
nativesDirTypeProperty.set(nativesDirType);
}
// Path to lwjgl natives directory
private final StringProperty nativesDirProperty = new SimpleStringProperty(this, "nativesDirProperty", "");
public StringProperty nativesDirProperty() {
return nativesDirProperty;
}
public String getNativesDir() {
return nativesDirProperty.get();
}
public void setNativesDir(String nativesDir) {
nativesDirProperty.set(nativesDir);
}
private final StringProperty javaDirProperty = new SimpleStringProperty(this, "javaDir", "");
public StringProperty javaDirProperty() {
@@ -170,7 +229,7 @@ public final class VersionSetting implements Cloneable {
permSizeProperty.set(permSize);
}
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY);
private final IntegerProperty maxMemoryProperty = new SimpleIntegerProperty(this, "maxMemory", SUGGESTED_MEMORY);
public IntegerProperty maxMemoryProperty() {
return maxMemoryProperty;
@@ -204,6 +263,20 @@ public final class VersionSetting implements Cloneable {
minMemoryProperty.set(minMemory);
}
private final BooleanProperty autoMemory = new SimpleBooleanProperty(this, "autoMemory", true);
public boolean isAutoMemory() {
return autoMemory.get();
}
public BooleanProperty autoMemoryProperty() {
return autoMemory;
}
public void setAutoMemory(boolean autoMemory) {
this.autoMemory.set(autoMemory);
}
private final StringProperty preLaunchCommandProperty = new SimpleStringProperty(this, "precalledCommand", "");
public StringProperty preLaunchCommandProperty() {
@@ -222,6 +295,24 @@ public final class VersionSetting implements Cloneable {
preLaunchCommandProperty.set(preLaunchCommand);
}
private final StringProperty postExitCommand = new SimpleStringProperty(this, "postExitCommand", "");
public StringProperty postExitCommandProperty() {
return postExitCommand;
}
/**
* The command that will be executed after game exits.
* Operating system relevant.
*/
public String getPostExitCommand() {
return postExitCommand.get();
}
public void setPostExitCommand(String postExitCommand) {
this.postExitCommand.set(postExitCommand);
}
// options
private final StringProperty javaArgsProperty = new SimpleStringProperty(this, "javaArgs", "");
@@ -258,6 +349,20 @@ public final class VersionSetting implements Cloneable {
minecraftArgsProperty.set(minecraftArgs);
}
private final StringProperty environmentVariablesProperty = new SimpleStringProperty(this, "environmentVariables", "");
public StringProperty environmentVariablesProperty() {
return environmentVariablesProperty;
}
public String getEnvironmentVariables() {
return environmentVariablesProperty.get();
}
public void setEnvironmentVariables(String env) {
environmentVariablesProperty.set(env);
}
private final BooleanProperty noJVMArgsProperty = new SimpleBooleanProperty(this, "noJVMArgs", false);
public BooleanProperty noJVMArgsProperty() {
@@ -309,6 +414,20 @@ public final class VersionSetting implements Cloneable {
notCheckGameProperty.set(notCheckGame);
}
private final BooleanProperty notPatchNativesProperty = new SimpleBooleanProperty(this, "notPatchNatives", false);
public BooleanProperty notPatchNativesProperty() {
return notPatchNativesProperty;
}
public boolean isNotPatchNatives() {
return notPatchNativesProperty.get();
}
public void setNotPatchNatives(boolean notPatchNatives) {
notPatchNativesProperty.set(notPatchNatives);
}
private final BooleanProperty showLogsProperty = new SimpleBooleanProperty(this, "showLogs", false);
public BooleanProperty showLogsProperty() {
@@ -336,7 +455,7 @@ public final class VersionSetting implements Cloneable {
/**
* The server ip that will be entered after Minecraft successfully loaded ly.
*
* <p>
* Format: ip:port or without port.
*/
public String getServerIp() {
@@ -373,7 +492,7 @@ public final class VersionSetting implements Cloneable {
/**
* The width of Minecraft window, defaults 800.
*
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
@@ -386,7 +505,6 @@ public final class VersionSetting implements Cloneable {
widthProperty.set(width);
}
private final IntegerProperty heightProperty = new SimpleIntegerProperty(this, "height", 480);
public IntegerProperty heightProperty() {
@@ -395,7 +513,7 @@ public final class VersionSetting implements Cloneable {
/**
* The height of Minecraft window, defaults 480.
*
* <p>
* The field saves int value.
* String type prevents unexpected value from JsonParseException.
* We can only reset this field instead of recreating the whole setting file.
@@ -443,6 +561,76 @@ public final class VersionSetting implements Cloneable {
gameDirProperty.set(gameDir);
}
private final ObjectProperty<ProcessPriority> processPriorityProperty = new SimpleObjectProperty<>(this, "processPriority", ProcessPriority.NORMAL);
public ObjectProperty<ProcessPriority> processPriorityProperty() {
return processPriorityProperty;
}
public ProcessPriority getProcessPriority() {
return processPriorityProperty.get();
}
public void setProcessPriority(ProcessPriority processPriority) {
processPriorityProperty.set(processPriority);
}
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
public Renderer getRenderer() {
return rendererProperty.get();
}
public ObjectProperty<Renderer> rendererProperty() {
return rendererProperty;
}
public void setRenderer(Renderer renderer) {
this.rendererProperty.set(renderer);
}
private final BooleanProperty useNativeGLFW = new SimpleBooleanProperty(this, "nativeGLFW", false);
public boolean isUseNativeGLFW() {
return useNativeGLFW.get();
}
public BooleanProperty useNativeGLFWProperty() {
return useNativeGLFW;
}
public void setUseNativeGLFW(boolean useNativeGLFW) {
this.useNativeGLFW.set(useNativeGLFW);
}
private final BooleanProperty useNativeOpenAL = new SimpleBooleanProperty(this, "nativeOpenAL", false);
public boolean isUseNativeOpenAL() {
return useNativeOpenAL.get();
}
public BooleanProperty useNativeOpenALProperty() {
return useNativeOpenAL;
}
public void setUseNativeOpenAL(boolean useNativeOpenAL) {
this.useNativeOpenAL.set(useNativeOpenAL);
}
private final ObjectProperty<VersionIconType> versionIcon = new SimpleObjectProperty<>(this, "versionIcon", VersionIconType.DEFAULT);
public VersionIconType getVersionIcon() {
return versionIcon.get();
}
public ObjectProperty<VersionIconType> versionIconProperty() {
return versionIcon;
}
public void setVersionIcon(VersionIconType versionIcon) {
this.versionIcon.set(versionIcon);
}
// launcher settings
/**
@@ -464,96 +652,86 @@ public final class VersionSetting implements Cloneable {
launcherVisibilityProperty.set(launcherVisibility);
}
public JavaVersion getJavaVersion() throws InterruptedException {
return getJavaVersion(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 JavaVersion getJavaVersion(boolean checkJava) throws InterruptedException {
// TODO: lazy initialization may result in UI suspension.
if (StringUtils.isBlank(getJava()))
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
else if (isUsesCustomJavaDir()) {
try {
if (checkJava)
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
else
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.PLATFORM);
} catch (IOException | InvalidPathException e) {
return null; // Custom Java Directory not found,
int majorVersion = -1;
try {
majorVersion = Integer.parseInt(javaVersion);
} catch (NumberFormatException ignored) {
}
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);
}
} else if (StringUtils.isNotBlank(getJava())) {
List<JavaVersion> matchedJava = JavaVersion.getJavas().stream()
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
setJava("Default");
return JavaVersion.fromCurrentEnvironment();
} else {
return matchedJava.stream()
.filter(java -> java.getBinary().toString().equals(getDefaultJavaPath()))
.findFirst()
.orElse(matchedJava.get(0));
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;
}
} else throw new Error();
default:
throw new AssertionError("JavaVersionType: " + getJavaVersionType());
}
}
public void setJavaVersion(JavaVersion java) {
setJava(java.getVersion());
setDefaultJavaPath(java.getBinary().toString());
@Override
public void addListener(InvalidationListener listener) {
helper.addListener(listener);
}
public void addPropertyChangedListener(InvalidationListener listener) {
usesGlobalProperty.addListener(listener);
javaProperty.addListener(listener);
javaDirProperty.addListener(listener);
wrapperProperty.addListener(listener);
permSizeProperty.addListener(listener);
maxMemoryProperty.addListener(listener);
minMemoryProperty.addListener(listener);
preLaunchCommandProperty.addListener(listener);
javaArgsProperty.addListener(listener);
minecraftArgsProperty.addListener(listener);
noJVMArgsProperty.addListener(listener);
notCheckGameProperty.addListener(listener);
notCheckJVMProperty.addListener(listener);
showLogsProperty.addListener(listener);
serverIpProperty.addListener(listener);
fullscreenProperty.addListener(listener);
widthProperty.addListener(listener);
heightProperty.addListener(listener);
gameDirTypeProperty.addListener(listener);
gameDirProperty.addListener(listener);
launcherVisibilityProperty.addListener(listener);
defaultJavaPathProperty.addListener(listener);
@Override
public void removeListener(InvalidationListener listener) {
helper.removeListener(listener);
}
@Override
public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting();
versionSetting.setUsesGlobal(isUsesGlobal());
versionSetting.setJava(getJava());
versionSetting.setDefaultJavaPath(getDefaultJavaPath());
versionSetting.setJavaDir(getJavaDir());
versionSetting.setWrapper(getWrapper());
versionSetting.setPermSize(getPermSize());
versionSetting.setMaxMemory(getMaxMemory());
versionSetting.setMinMemory(getMinMemory());
versionSetting.setPreLaunchCommand(getPreLaunchCommand());
versionSetting.setJavaArgs(getJavaArgs());
versionSetting.setMinecraftArgs(getMinecraftArgs());
versionSetting.setNoJVMArgs(isNoJVMArgs());
versionSetting.setNotCheckGame(isNotCheckGame());
versionSetting.setNotCheckJVM(isNotCheckJVM());
versionSetting.setShowLogs(isShowLogs());
versionSetting.setServerIp(getServerIp());
versionSetting.setFullscreen(isFullscreen());
versionSetting.setWidth(getWidth());
versionSetting.setHeight(getHeight());
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setLauncherVisibility(getLauncherVisibility());
return versionSetting;
VersionSetting cloned = new VersionSetting();
PropertyUtils.copyProperties(this, cloned);
return cloned;
}
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
@@ -565,62 +743,143 @@ public final class VersionSetting implements Cloneable {
obj.addProperty("usesGlobal", src.isUsesGlobal());
obj.addProperty("javaArgs", src.getJavaArgs());
obj.addProperty("minecraftArgs", src.getMinecraftArgs());
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? OperatingSystem.SUGGESTED_MEMORY : src.getMaxMemory());
obj.addProperty("environmentVariables", src.getEnvironmentVariables());
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? SUGGESTED_MEMORY : src.getMaxMemory());
obj.addProperty("minMemory", src.getMinMemory());
obj.addProperty("autoMemory", src.isAutoMemory());
obj.addProperty("permSize", src.getPermSize());
obj.addProperty("width", src.getWidth());
obj.addProperty("height", src.getHeight());
obj.addProperty("javaDir", src.getJavaDir());
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());
obj.addProperty("notCheckGame", src.isNotCheckGame());
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
obj.addProperty("notPatchNatives", src.isNotPatchNatives());
obj.addProperty("showLogs", src.isShowLogs());
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("processPriority", src.getProcessPriority().ordinal());
obj.addProperty("useNativeGLFW", src.isUseNativeGLFW());
obj.addProperty("useNativeOpenAL", src.isUseNativeOpenAL());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
obj.addProperty("defaultJavaPath", src.getDefaultJavaPath());
obj.addProperty("nativesDir", src.getNativesDir());
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);
return obj;
}
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
if (index == null)
return defaultValue;
int idx = index.getAsInt();
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
}
@Override
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject))
if (!(json instanceof JsonObject))
return null;
JsonObject obj = (JsonObject) json;
int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), OperatingSystem.SUGGESTED_MEMORY);
if (maxMemoryN <= 0) maxMemoryN = OperatingSystem.SUGGESTED_MEMORY;
int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), SUGGESTED_MEMORY);
if (maxMemoryN <= 0) maxMemoryN = SUGGESTED_MEMORY;
VersionSetting vs = new VersionSetting();
vs.setUsesGlobal(Optional.ofNullable(obj.get("usesGlobal")).map(JsonElement::getAsBoolean).orElse(false));
vs.setJavaArgs(Optional.ofNullable(obj.get("javaArgs")).map(JsonElement::getAsString).orElse(""));
vs.setMinecraftArgs(Optional.ofNullable(obj.get("minecraftArgs")).map(JsonElement::getAsString).orElse(""));
vs.setEnvironmentVariables(Optional.ofNullable(obj.get("environmentVariables")).map(JsonElement::getAsString).orElse(""));
vs.setMaxMemory(maxMemoryN);
vs.setMinMemory(Optional.ofNullable(obj.get("minMemory")).map(JsonElement::getAsInt).orElse(null));
vs.setAutoMemory(Optional.ofNullable(obj.get("autoMemory")).map(JsonElement::getAsBoolean).orElse(true));
vs.setPermSize(Optional.ofNullable(obj.get("permSize")).map(JsonElement::getAsString).orElse(""));
vs.setWidth(Optional.ofNullable(obj.get("width")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
vs.setHeight(Optional.ofNullable(obj.get("height")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
vs.setJavaDir(Optional.ofNullable(obj.get("javaDir")).map(JsonElement::getAsString).orElse(""));
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(""));
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
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 {
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
} catch (IllegalArgumentException ignored) {
return Optional.empty();
}
}).orElseGet(() -> {
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
}));
return vs;
}
@@ -637,5 +896,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;
}
}
}
}
}
@@ -3,11 +3,14 @@ package org.jackhuang.hmcl.ui;
import java.awt.*;
import java.lang.reflect.Method;
public class AwtUtils {
public final class AwtUtils {
private AwtUtils() {
}
public static void setAppleIcon(Image image) {
try {
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskBar");
Class<?> taskbarClass = Class.forName("java.awt.Taskbar");
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskbar");
Object taskBar = getTaskBarMethod.invoke(null);
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
setIconImageMethod.invoke(taskBar, image);
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,54 +17,98 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.java.JavaManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AuthlibInjectorServersPage;
import org.jackhuang.hmcl.ui.account.MicrosoftAccountLoginStage;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.account.AccountListPage;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.download.DownloadPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
import org.jackhuang.hmcl.ui.main.RootPage;
import org.jackhuang.hmcl.ui.versions.GameListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
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 {
public static final String JAVA_VERSION_TIP = "javaVersion";
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
public static final Screen SCREEN = Screen.getPrimary();
private static InvalidationListener stageSizeChangeListener;
private static DoubleProperty stageX = new SimpleDoubleProperty();
private static DoubleProperty stageY = new SimpleDoubleProperty();
private static DoubleProperty stageWidth = new SimpleDoubleProperty();
private static DoubleProperty stageHeight = new SimpleDoubleProperty();
private static Scene scene;
private static Stage stage;
private static VersionPage versionPage = null;
private static AuthlibInjectorServersPage serversPage = null;
private static RootPage rootPage;
private static Lazy<VersionPage> versionPage = new Lazy<>(VersionPage::new);
private static Lazy<GameListPage> gameListPage = new Lazy<>(() -> {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
return gameListPage;
});
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
private static DecoratorController decorator;
private static Lazy<DownloadPage> downloadPage = new Lazy<>(DownloadPage::new);
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
AccountListPage accountListPage = new AccountListPage();
accountListPage.selectedAccountProperty().bindBidirectional(Accounts.selectedAccountProperty());
accountListPage.accountsProperty().bindContent(Accounts.getAccounts());
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
return accountListPage;
});
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
private Controllers() {
}
public static Scene getScene() {
return scene;
@@ -76,23 +120,32 @@ public final class Controllers {
// FXThread
public static VersionPage getVersionPage() {
if (versionPage == null)
versionPage = new VersionPage();
return versionPage;
return versionPage.get();
}
// FXThread
public static GameListPage getGameListPage() {
return gameListPage.get();
}
// FXThread
public static RootPage getRootPage() {
if (rootPage == null)
rootPage = new RootPage();
return rootPage;
return rootPage.get();
}
// FXThread
public static AuthlibInjectorServersPage getServersPage() {
if (serversPage == null)
serversPage = new AuthlibInjectorServersPage();
return serversPage;
public static LauncherSettingsPage getSettingsPage() {
return settingsPage.get();
}
// FXThread
public static AccountListPage getAccountListPage() {
return accountListPage.get();
}
// FXThread
public static DownloadPage getDownloadPage() {
return downloadPage.get();
}
// FXThread
@@ -101,20 +154,101 @@ public final class Controllers {
}
public static void onApplicationStop() {
config().setHeight(stageHeight.get());
config().setWidth(stageWidth.get());
stageSizeChangeListener = null;
if (stageX != null) {
config().setX(stageX.get() / SCREEN.getBounds().getWidth());
stageX = null;
}
if (stageY != null) {
config().setY(stageY.get() / SCREEN.getBounds().getHeight());
stageY = null;
}
if (stageHeight != null) {
config().setHeight(stageHeight.get());
stageHeight = null;
}
if (stageWidth != null) {
config().setWidth(stageWidth.get());
stageWidth = null;
}
}
public static void initialize(Stage stage) {
Logging.LOG.info("Start initializing application");
LOG.info("Start initializing application");
Controllers.stage = stage;
MicrosoftAccountLoginStage.INSTANCE.initOwner(stage);
stage.setHeight(config().getHeight());
stageHeight.bind(stage.heightProperty());
stage.setWidth(config().getWidth());
stageWidth.bind(stage.widthProperty());
stageSizeChangeListener = o -> {
ReadOnlyDoubleProperty sourceProperty = (ReadOnlyDoubleProperty) o;
DoubleProperty targetProperty;
switch (sourceProperty.getName()) {
case "x": {
targetProperty = stageX;
break;
}
case "y": {
targetProperty = stageY;
break;
}
case "width": {
targetProperty = stageWidth;
break;
}
case "height": {
targetProperty = stageHeight;
break;
}
default: {
targetProperty = null;
}
}
if (targetProperty != null
&& Controllers.stage != null
&& !Controllers.stage.isIconified()) {
targetProperty.set(sourceProperty.get());
}
};
WeakInvalidationListener weakListener = new WeakInvalidationListener(stageSizeChangeListener);
double initWidth = Math.max(MIN_WIDTH, config().getWidth());
double initHeight = Math.max(MIN_HEIGHT, config().getHeight());
{
double initX = config().getX() * SCREEN.getBounds().getWidth();
double initY = config().getY() * SCREEN.getBounds().getHeight();
boolean invalid = true;
double border = 20D;
for (Screen screen : Screen.getScreens()) {
Rectangle2D bound = screen.getBounds();
if (bound.getMinX() + border <= initX + initWidth && initX <= bound.getMaxX() - border && bound.getMinY() + border <= initY && initY <= bound.getMaxY() - border) {
invalid = false;
break;
}
}
if (invalid) {
initX = (0.5D - initWidth / SCREEN.getBounds().getWidth() / 2) * SCREEN.getBounds().getWidth();
initY = (0.5D - initHeight / SCREEN.getBounds().getHeight() / 2) * SCREEN.getBounds().getHeight();
}
stage.setX(initX);
stage.setY(initY);
stageX.set(initX);
stageY.set(initY);
}
stage.setHeight(initHeight);
stage.setWidth(initWidth);
stageHeight.set(initHeight);
stageWidth.set(initWidth);
stage.xProperty().addListener(weakListener);
stage.yProperty().addListener(weakListener);
stage.heightProperty().addListener(weakListener);
stage.widthProperty().addListener(weakListener);
stage.setOnCloseRequest(e -> Launcher.stopApplication());
@@ -126,20 +260,94 @@ public final class Controllers {
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
Lang.thread(JavaManager::initialize, "Search Java", true);
scene = new Scene(decorator.getDecorator());
scene.setFill(Color.TRANSPARENT);
stage.setMinHeight(482 + 32);
stage.setMinWidth(802 + 32);
stage.setMinWidth(MIN_WIDTH);
stage.setMinHeight(MIN_HEIGHT);
decorator.getDecorator().prefWidthProperty().bind(scene.widthProperty());
decorator.getDecorator().prefHeightProperty().bind(scene.heightProperty());
scene.getStylesheets().setAll(config().getTheme().getStylesheets());
StyleSheets.init(scene);
stage.getIcons().add(newImage("/assets/img/icon.png"));
stage.setTitle(Metadata.TITLE);
FXUtils.setIcon(stage);
stage.setTitle(Metadata.FULL_TITLE);
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
if (AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(decorator.getDecorator().opacityProperty(), 0, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, FXUtils.EASE)
),
new KeyFrame(Duration.millis(600),
new KeyValue(decorator.getDecorator().opacityProperty(), 1, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleXProperty(), 1, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleYProperty(), 1, FXUtils.EASE),
new KeyValue(decorator.getDecorator().scaleZProperty(), 1, FXUtils.EASE)
)
);
timeline.play();
}
if (!Architecture.SYSTEM_ARCH.isX86() && globalConfig().getPlatformPromptVersion() < 1) {
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.macos_arm64"), null, MessageType.INFO, continueAction);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.windows_arm64"), null, MessageType.INFO, continueAction);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX &&
(Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64
|| Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW
|| Architecture.SYSTEM_ARCH == Architecture.MIPS64EL)) {
Controllers.dialog(i18n("fatal.unsupported_platform.loongarch"), null, MessageType.INFO, continueAction);
} else {
Controllers.dialog(i18n("fatal.unsupported_platform"), null, MessageType.WARNING, continueAction);
}
}
if (JavaRuntime.CURRENT_VERSION < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
Number shownTipVersion = null;
try {
shownTipVersion = (Number) config().getShownTips().get(JAVA_VERSION_TIP);
} catch (ClassCastException e) {
LOG.warning("Invalid type for shown tips key: " + JAVA_VERSION_TIP, e);
}
if (shownTipVersion == null || shownTipVersion.intValue() < Metadata.MINIMUM_SUPPORTED_JAVA_VERSION) {
MessageDialogPane.Builder builder = new MessageDialogPane.Builder(i18n("fatal.deprecated_java_version"), null, MessageType.WARNING);
String downloadLink = Metadata.getSuggestedJavaDownloadLink();
if (downloadLink != null)
builder.addHyperLink(i18n("fatal.deprecated_java_version.download_link", 21), downloadLink);
Controllers.dialog(builder
.ok(() -> config().getShownTips().put(JAVA_VERSION_TIP, Metadata.MINIMUM_SUPPORTED_JAVA_VERSION))
.build());
}
}
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
agreementPane.setBody(new Label(i18n("launcher.agreement.hint")));
JFXHyperlink agreementLink = new JFXHyperlink(i18n("launcher.agreement"));
agreementLink.setExternalLink(Metadata.EULA_URL);
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
yesButton.getStyleClass().add("dialog-accept");
yesButton.setOnAction(e -> {
globalConfig().setAgreementVersion(1);
agreementPane.fireEvent(new DialogCloseEvent());
});
JFXButton noButton = new JFXButton(i18n("launcher.agreement.decline"));
noButton.getStyleClass().add("dialog-cancel");
noButton.setOnAction(e -> javafx.application.Platform.exit());
agreementPane.setActions(agreementLink, yesButton, noButton);
Controllers.dialog(agreementPane);
}
}
public static void dialog(Region content) {
@@ -152,19 +360,31 @@ public final class Controllers {
}
public static void dialog(String text, String title) {
dialog(text, title, MessageType.INFORMATION);
dialog(text, title, MessageType.INFO);
}
public static void dialog(String text, String title, MessageType type) {
dialog(text, title, type, null);
}
public static void dialog(String text, String title, MessageType type, Runnable onAccept) {
dialog(new MessageDialogPane(text, title, type, onAccept));
public static void dialog(String text, String title, MessageType type, Runnable ok) {
dialog(new MessageDialogPane.Builder(text, title, type).ok(ok).build());
}
public static void confirm(String text, String title, Runnable onAccept, Runnable onCancel) {
dialog(new MessageDialogPane(text, title, onAccept, onCancel));
public static void confirm(String text, String title, Runnable yes, Runnable no) {
confirm(text, title, MessageType.QUESTION, yes, no);
}
public static void confirm(String text, String title, MessageType type, Runnable yes, Runnable no) {
dialog(new MessageDialogPane.Builder(text, title, type).yesOrNo(yes, no).build());
}
public static void confirmAction(String text, String title, MessageType type, ButtonBase actionButton) {
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, null).build());
}
public static void confirmAction(String text, String title, MessageType type, ButtonBase actionButton, Runnable cancel) {
dialog(new MessageDialogPane.Builder(text, title, type).actionOrCancel(actionButton, cancel).build());
}
public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
@@ -183,11 +403,7 @@ public final class Controllers {
return pane.getCompletableFuture();
}
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title) {
return taskDialog(executor, title, null);
}
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, Consumer<Region> onCancel) {
public static TaskExecutorDialogPane taskDialog(TaskExecutor executor, String title, TaskCancellationAction onCancel) {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
pane.setExecutor(executor);
@@ -195,8 +411,32 @@ public final class Controllers {
return pane;
}
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, TaskCancellationAction onCancel) {
TaskExecutor executor = task.executor();
TaskExecutorDialogPane pane = taskDialog(executor, title, onCancel);
executor.start();
return pane;
}
public static void navigate(Node node) {
decorator.getNavigator().navigate(node, ContainerAnimations.FADE.getAnimationProducer());
decorator.navigate(node);
}
public static void showToast(String content) {
decorator.showToast(content);
}
public static void onHyperlinkAction(String href) {
if (href.startsWith("hmcl://")) {
switch (href) {
case "hmcl://settings/feedback":
Controllers.getSettingsPage().showFeedback();
Controllers.navigate(Controllers.getSettingsPage());
break;
}
} else {
FXUtils.openLink(href);
}
}
public static boolean isStopped() {
@@ -206,9 +446,15 @@ public final class Controllers {
public static void shutdown() {
rootPage = null;
versionPage = null;
serversPage = null;
gameListPage = null;
downloadPage = null;
accountListPage = null;
settingsPage = null;
decorator = null;
stage = null;
scene = null;
onApplicationStop();
FXUtils.shutdown();
}
}
@@ -27,9 +27,9 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.countly.CrashReport;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
@@ -37,21 +37,23 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
*/
public class CrashWindow extends Stage {
public CrashWindow(String text) {
public CrashWindow(CrashReport report) {
Label lblCrash = new Label();
if (UpdateChecker.isOutdated())
lblCrash.setText(i18n("launcher.crash_out_dated"));
if (report.getThrowable() instanceof InternalError)
lblCrash.setText(i18n("launcher.crash.java_internal_error"));
else if (UpdateChecker.isOutdated())
lblCrash.setText(i18n("launcher.crash.hmcl_out_dated"));
else
lblCrash.setText(i18n("launcher.crash"));
lblCrash.setWrapText(true);
TextArea textArea = new TextArea();
textArea.setText(text);
textArea.setText(report.getDisplayText());
textArea.setEditable(false);
Button btnContact = new Button();
btnContact.setText(i18n("launcher.contact"));
btnContact.setOnMouseClicked(event -> FXUtils.openLink(Metadata.CONTACT_URL));
btnContact.setOnAction(event -> FXUtils.openLink(Metadata.CONTACT_URL));
HBox box = new HBox();
box.setStyle("-fx-padding: 8px;");
box.getChildren().add(btnContact);
@@ -67,10 +69,10 @@ public class CrashWindow extends Stage {
Scene scene = new Scene(pane, 800, 480);
setScene(scene);
getIcons().add(newImage("/assets/img/icon.png"));
FXUtils.setIcon(this);
setTitle(i18n("message.error"));
setOnCloseRequest(e -> System.exit(1));
setOnCloseRequest(e -> javafx.application.Platform.exit());
}
}
@@ -17,11 +17,9 @@
*/
package org.jackhuang.hmcl.ui;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.ui.account.AccountLoginPane;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.ui.account.ClassicAccountLoginDialog;
import org.jackhuang.hmcl.ui.account.OAuthAccountLoginDialog;
import java.util.Optional;
import java.util.concurrent.CancellationException;
@@ -31,15 +29,29 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public final class DialogController {
private DialogController() {
}
public static AuthInfo logIn(Account account) throws CancellationException, AuthenticationException, InterruptedException {
if (account instanceof YggdrasilAccount) {
if (account instanceof ClassicAccount) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
AccountLoginPane pane = new AccountLoginPane(account, it -> {
res.set(it);
latch.countDown();
ClassicAccountLoginDialog pane = new ClassicAccountLoginDialog((ClassicAccount) account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
Controllers.dialog(pane);
});
latch.await();
return Optional.ofNullable(res.get()).orElseThrow(CancellationException::new);
} else if (account instanceof OAuthAccount) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null);
runInFX(() -> {
OAuthAccountLoginDialog pane = new OAuthAccountLoginDialog((OAuthAccount) account, it -> {
res.set(it);
latch.countDown();
}, latch::countDown);
Controllers.dialog(pane);
});
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,433 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2023 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.JFXButton;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.*;
import java.io.IOException;
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.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
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;
public class GameCrashWindow extends Stage {
private final Version version;
private final String memory;
private final String total_memory;
private final String java;
private final LibraryAnalyzer analyzer;
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();
private final ManagedProcess managedProcess;
private final DefaultGameRepository repository;
private final ProcessListener.ExitType exitType;
private final LaunchOptions launchOptions;
private final View view;
private final List<Log> 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;
this.version = version;
this.launchOptions = launchOptions;
this.logs = logs;
this.analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null));
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
total_memory = MEGABYTES.formatBytes(SystemInfo.getTotalMemorySize());
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
? launchOptions.getJava().getVersion()
: launchOptions.getJava().getVersion() + " (" + launchOptions.getJava().getArchitecture().getDisplayName() + ")";
this.view = new View();
this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
setScene(new Scene(view, 800, 480));
StyleSheets.init(getScene());
setTitle(i18n("game.crash.title"));
FXUtils.setIcon(this);
analyzeCrashReport();
}
@SuppressWarnings("unchecked")
private void analyzeCrashReport() {
loading.set(true);
Task.allOf(Task.supplyAsync(() -> {
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.warning("Failed to read crash report", e);
}
if (crashReport == null) {
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
}
return pair(CrashReportAnalyzer.analyze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>());
}), Task.supplyAsync(() -> {
Path latestLog = repository.getRunDirectory(version.getId()).toPath().resolve("logs/latest.log");
if (!Files.isReadable(latestLog)) {
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
}
String log;
try {
log = FileUtils.readTextMaybeNativeEncoding(latestLog);
} catch (IOException e) {
LOG.warning("Failed to read logs/latest.log", e);
return pair(new HashSet<CrashReportAnalyzer.Result>(), new HashSet<String>());
}
return pair(CrashReportAnalyzer.analyze(log), CrashReportAnalyzer.findKeywordsFromCrashReport(log));
})).whenComplete(Schedulers.javafx(), (taskResult, exception) -> {
loading.set(false);
if (exception != null) {
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);
Set<String> keywords = new HashSet<>();
for (Pair<Set<CrashReportAnalyzer.Result>, Set<String>> pair : (List<Pair<Set<CrashReportAnalyzer.Result>, Set<String>>>) (List<?>) taskResult) {
for (CrashReportAnalyzer.Result result : pair.getKey()) {
results.put(result.getRule(), result);
}
keywords.addAll(pair.getValue());
}
List<Node> segments = new ArrayList<>(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
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));
} else {
segments.add(new Text("\n\n"));
}
for (CrashReportAnalyzer.Result result : results.values()) {
String message;
switch (result.getRule()) {
case TOO_OLD_JAVA:
message = i18n("game.crash.reason.too_old_java", CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected"))));
break;
case MOD_RESOLUTION_CONFLICT:
case MOD_RESOLUTION_MISSING:
case MOD_RESOLUTION_COLLECTION:
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.getMatcher().group("sourcemod")),
parseFabricModId(result.getMatcher().group("destmod")),
parseFabricModId(result.getMatcher().group("destmod")));
break;
case MOD_RESOLUTION_MISSING_MINECRAFT:
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
translateFabricModId(result.getMatcher().group("mod")),
result.getMatcher().group("version"));
break;
case MOD_FOREST_OPTIFINE:
case TWILIGHT_FOREST_OPTIFINE:
case PERFORMANT_FOREST_OPTIFINE:
case JADE_FOREST_OPTIFINE:
case NEOFORGE_FOREST_OPTIFINE:
message = 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),
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
.toArray());
break;
}
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.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.info("Crash reason unknown");
}
} else {
feedbackTextFlow.setVisible(false);
reasonTextFlow.getChildren().setAll(segments);
}
}
}).start();
}
private static final Pattern FABRIC_MOD_ID = Pattern.compile("\\{(?<modid>.*?) @ (?<version>.*?)}");
private String translateFabricModId(String modName) {
switch (modName) {
case "fabricloader":
return "Fabric";
case "fabric":
return "Fabric API";
case "minecraft":
return "Minecraft";
default:
return modName;
}
}
private String parseFabricModId(String modName) {
Matcher matcher = FABRIC_MOD_ID.matcher(modName);
if (matcher.find()) {
String modid = matcher.group("modid");
String version = matcher.group("version");
if ("[*]".equals(version)) {
return i18n("game.crash.reason.mod_resolution_mod_version.any", translateFabricModId(modid));
} else {
return i18n("game.crash.reason.mod_resolution_mod_version", translateFabricModId(modid), version);
}
}
return translateFabricModId(modName);
}
private void showLogWindow() {
LogWindow logWindow = new LogWindow(managedProcess);
logWindow.logLine(new Log(Logger.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO));
if (managedProcess.getClasspath() != null)
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(Log::getLog).collect(Collectors.joining("\n")))
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.handleAsync((result, exception) -> {
Alert alert;
if (exception == null) {
FXUtils.showFileInExplorer(logFile);
alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
} else {
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));
}
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
alert.showAndWait();
return null;
});
}
private final class View extends VBox {
View() {
setStyle("-fx-background-color: white");
HBox titlePane = new HBox();
{
Label title = new Label();
HBox.setHgrow(title, Priority.ALWAYS);
switch (exitType) {
case JVM_ERROR:
title.setText(i18n("launch.failed.cannot_create_jvm"));
break;
case APPLICATION_ERROR:
title.setText(i18n("launch.failed.exited_abnormally"));
break;
case SIGKILL:
title.setText(i18n("launch.failed.sigkill"));
break;
}
titlePane.setAlignment(Pos.CENTER);
titlePane.getStyleClass().addAll("jfx-tool-bar-second", "depth-1", "padding-8");
titlePane.getChildren().setAll(title);
}
HBox infoPane = new HBox(8);
{
infoPane.setPadding(new Insets(8));
infoPane.setAlignment(Pos.CENTER_LEFT);
TwoLineListItem launcher = new TwoLineListItem();
launcher.getStyleClass().setAll("two-line-item-second-large");
launcher.setTitle(i18n("launcher"));
launcher.setSubtitle(Metadata.VERSION);
TwoLineListItem version = new TwoLineListItem();
version.getStyleClass().setAll("two-line-item-second-large");
version.setTitle(i18n("game.version"));
version.setSubtitle(GameCrashWindow.this.version.getId());
TwoLineListItem total_memory = new TwoLineListItem();
total_memory.getStyleClass().setAll("two-line-item-second-large");
total_memory.setTitle(i18n("settings.physical_memory"));
total_memory.setSubtitle(GameCrashWindow.this.total_memory);
TwoLineListItem memory = new TwoLineListItem();
memory.getStyleClass().setAll("two-line-item-second-large");
memory.setTitle(i18n("settings.memory"));
memory.setSubtitle(GameCrashWindow.this.memory);
TwoLineListItem java = new TwoLineListItem();
java.getStyleClass().setAll("two-line-item-second-large");
java.setTitle("Java");
java.setSubtitle(GameCrashWindow.this.java);
TwoLineListItem os = new TwoLineListItem();
os.getStyleClass().setAll("two-line-item-second-large");
os.setTitle(i18n("system.operating_system"));
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.setSubtitle(Architecture.SYSTEM_ARCH.getDisplayName());
infoPane.getChildren().setAll(launcher, version, total_memory, memory, java, os, arch);
}
HBox moddedPane = new HBox(8);
{
moddedPane.setPadding(new Insets(8));
moddedPane.setAlignment(Pos.CENTER_LEFT);
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
if (!type.getPatchId().isEmpty()) {
analyzer.getVersion(type).ifPresent(ver -> {
TwoLineListItem item = new TwoLineListItem();
item.getStyleClass().setAll("two-line-item-second-large");
item.setTitle(i18n("install.installer." + type.getPatchId()));
item.setSubtitle(ver);
moddedPane.getChildren().add(item);
});
}
}
}
VBox gameDirPane = new VBox(8);
{
TwoLineListItem gameDir = new TwoLineListItem();
gameDir.getStyleClass().setAll("two-line-item-second-large");
gameDir.setTitle(i18n("game.directory"));
gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath());
FXUtils.installFastTooltip(gameDir, i18n("game.directory"));
TwoLineListItem javaDir = new TwoLineListItem();
javaDir.getStyleClass().setAll("two-line-item-second-large");
javaDir.setTitle(i18n("settings.game.java_directory"));
javaDir.setSubtitle(launchOptions.getJava().getBinary().toAbsolutePath().toString());
FXUtils.installFastTooltip(javaDir, i18n("settings.game.java_directory"));
Label reasonTitle = new Label(i18n("game.crash.reason"));
reasonTitle.getStyleClass().add("two-line-item-second-large-title");
ScrollPane reasonPane = new ScrollPane(reasonTextFlow);
reasonPane.setFitToWidth(true);
reasonPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
gameDirPane.setPadding(new Insets(8));
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
FXUtils.onChangeAndOperate(feedbackTextFlow.visibleProperty(), visible -> {
if (visible) {
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonPane, feedbackTextFlow));
} else {
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonPane));
}
});
}
HBox toolBar = new HBox();
{
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
logButton.setOnAction(e -> showLogWindow());
JFXButton helpButton = FXUtils.newRaisedButton(i18n("help"));
helpButton.setOnAction(e -> FXUtils.openLink(Metadata.CONTACT_URL));
FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"));
toolBar.setPadding(new Insets(8));
toolBar.setSpacing(8);
toolBar.getStyleClass().add("jfx-tool-bar");
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton);
}
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
}
}
}
@@ -0,0 +1,315 @@
/*
* 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.jackhuang.hmcl.util.StringUtils;
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) {
FXUtils.onClicked(text, () -> 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 appendAutoLineBreak(String text) {
AutoLineBreak textNode = new AutoLineBreak(text);
applyStyle(textNode);
children.add(textNode);
}
private void appendImage(Node node) {
String src = node.absUrl("src");
String alt = node.attr("alt");
if (StringUtils.isNotBlank(src)) {
String widthAttr = node.attr("width");
String heightAttr = node.attr("height");
int width = 0;
int height = 0;
if (!widthAttr.isEmpty() && !heightAttr.isEmpty()) {
try {
width = (int) Double.parseDouble(widthAttr);
height = (int) Double.parseDouble(heightAttr);
} catch (NumberFormatException ignored) {
}
if (width <= 0 || height <= 0) {
width = 0;
height = 0;
}
}
try {
Image image = FXUtils.getRemoteImageTask(src, width, height, true, true)
.run();
if (image == null)
throw new AssertionError("Image loading task returned null");
ImageView imageView = new ImageView(image);
if (hyperlink != null) {
URI target = resolveLink(hyperlink);
if (target != null) {
FXUtils.onClicked(imageView, () -> onClickHyperlink.accept(target));
imageView.setCursor(Cursor.HAND);
}
}
children.add(imageView);
return;
} catch (Throwable e) {
LOG.warning("Failed to load image: " + src, e);
}
}
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())
appendAutoLineBreak("\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":
appendAutoLineBreak("\n");
break;
}
}
private static boolean isSpacing(String text) {
if (text == null)
return true;
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch != ' ' && ch != '\t')
return false;
}
return true;
}
public void mergeLineBreaks() {
for (int i = 0; i < this.children.size(); i++) {
javafx.scene.Node child = this.children.get(i);
if (child instanceof AutoLineBreak) {
int lastAutoLineBreak = -1;
for (int j = i + 1; j < this.children.size(); j++) {
javafx.scene.Node otherChild = this.children.get(j);
if (otherChild instanceof AutoLineBreak) {
lastAutoLineBreak = j;
} else if (otherChild instanceof Text && isSpacing(((Text) otherChild).getText())) {
// do nothing
} else {
break;
}
}
if (lastAutoLineBreak > 0) {
this.children.subList(i + 1, lastAutoLineBreak + 1).clear();
if (((Text) child).getText().length() == 1) {
((Text) child).setText("\n\n");
}
}
}
}
}
public TextFlow render() {
TextFlow textFlow = new TextFlow();
textFlow.getStyleClass().add("html");
textFlow.getChildren().setAll(children);
return textFlow;
}
private static final class AutoLineBreak extends Text {
public AutoLineBreak(String text) {
super(text);
}
}
}
@@ -18,26 +18,34 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.event.EventHandler;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -47,168 +55,407 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
*/
public class InstallerItem extends Control {
private final String id;
private final String imageUrl;
public final StringProperty libraryVersion = new SimpleStringProperty();
public final StringProperty incompatibleLibraryName = 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 VersionIconType iconType;
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);
public InstallerItem(LibraryAnalyzer.LibraryType id) {
this(id.getPatchId());
private final ObjectProperty<Runnable> onInstall = new SimpleObjectProperty<>(this, "onInstall");
private final ObjectProperty<Runnable> onRemove = new SimpleObjectProperty<>(this, "onRemove");
public interface State {
}
public InstallerItem(String id) {
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, Style style) {
this(id.getPatchId(), style);
}
public InstallerItem(String id, Style style) {
this.id = id;
this.style = style;
switch (id) {
case "game":
imageUrl = "/assets/img/grass.png";
iconType = VersionIconType.GRASS;
break;
case "fabric":
imageUrl = "/assets/img/fabric.png";
case "fabric-api":
iconType = VersionIconType.FABRIC;
break;
case "forge":
imageUrl = "/assets/img/forge.png";
iconType = VersionIconType.FORGE;
break;
case "liteloader":
imageUrl = "/assets/img/chicken.png";
iconType = VersionIconType.CHICKEN;
break;
case "optifine":
imageUrl = "/assets/img/command.png";
iconType = VersionIconType.OPTIFINE;
break;
case "quilt":
case "quilt-api":
iconType = VersionIconType.QUILT;
break;
case "neoforge":
iconType = VersionIconType.NEO_FORGE;
break;
default:
imageUrl = null;
iconType = null;
break;
}
}
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<Runnable> onInstallProperty() {
return onInstall;
}
public Runnable getOnInstall() {
return onInstall.get();
}
public void setOnInstall(Runnable onInstall) {
this.onInstall.set(onInstall);
}
public ObjectProperty<Runnable> onRemoveProperty() {
return onRemove;
}
public Runnable getOnRemove() {
return onRemove.get();
}
public void setOnRemove(Runnable onRemove) {
this.onRemove.set(onRemove);
}
@Override
protected Skin<?> createDefaultSkin() {
return new InstallerItemSkin(this);
}
public static class InstallerItemGroup {
public InstallerItem game = new InstallerItem(MINECRAFT);
public InstallerItem fabric = new InstallerItem(FABRIC);
public InstallerItem forge = new InstallerItem(FORGE);
public InstallerItem liteLoader = new InstallerItem(LITELOADER);
public InstallerItem optiFine = new InstallerItem(OPTIFINE);
public final static class InstallerItemGroup {
private final InstallerItem game;
public InstallerItemGroup() {
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
return null;
}, fabric.libraryVersion));
private final InstallerItem[] libraries;
liteLoader.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
return null;
}, fabric.libraryVersion));
private Set<InstallerItem> getIncompatibles(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem item) {
return incompatibleMap.computeIfAbsent(item, it -> new HashSet<>());
}
optiFine.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
return null;
}, fabric.libraryVersion));
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(incompatibleMap, other).add(item);
}
}
fabric.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId();
if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId();
if (forge.libraryVersion.get() != null) return FORGE.getPatchId();
return null;
}, optiFine.libraryVersion, forge.libraryVersion));
private void mutualIncompatible(Map<InstallerItem, Set<InstallerItem>> incompatibleMap, InstallerItem... items) {
for (InstallerItem item : items) {
Set<InstallerItem> set = getIncompatibles(incompatibleMap, item);
for (InstallerItem item2 : items) {
if (item2 != item) {
set.add(item2);
}
}
}
}
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);
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);
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);
}
}
return InstallableState.INSTANCE;
}, bindings));
}
if (gameVersion != null) {
game.versionProperty.set(new InstalledState(gameVersion, false, false));
}
InstallerItem[] all = {game, forge, neoForge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
for (InstallerItem item : all) {
if (!item.resolvedStateProperty.isBound()) {
item.resolvedStateProperty.bind(Bindings.createObjectBinding(() -> {
InstalledState itemVersion = item.versionProperty.get();
if (itemVersion != null) {
return itemVersion;
}
return InstallableState.INSTANCE;
}, item.versionProperty));
}
}
if (gameVersion == null) {
this.libraries = all;
} else if (GameVersionNumber.compare(gameVersion, "1.13") < 0) {
this.libraries = new InstallerItem[]{game, forge, liteLoader, optiFine};
} else {
this.libraries = new InstallerItem[]{game, forge, neoForge, optiFine, fabric, fabricApi, quilt, quiltApi};
}
}
public InstallerItem getGame() {
return game;
}
public InstallerItem[] getLibraries() {
return new InstallerItem[]{game, fabric, forge, liteLoader, optiFine};
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");
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final ChangeListener<Number> holder;
InstallerItemSkin(InstallerItem control) {
super(control);
HBox hbox = new HBox();
getChildren().setAll(hbox);
JFXDepthManager.setDepth(hbox, 1);
Pane pane;
if (control.style == Style.CARD) {
pane = new VBox();
hbox.getStyleClass().add("card");
if (FXUtils.JAVAFX_MAJOR_VERSION == 8)
holder = FXUtils.onWeakChange(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
else
holder = FXUtils.onWeakChangeAndOperate(pane.widthProperty(), v -> FXUtils.setLimitHeight(pane, v.doubleValue() * 0.7));
} else {
pane = new HBox();
holder = null;
}
pane.getStyleClass().add("installer-item");
RipplerContainer container = new RipplerContainer(pane);
getChildren().setAll(container);
hbox.setAlignment(Pos.CENTER_LEFT);
pane.pseudoClassStateChanged(LIST_ITEM, control.style == Style.LIST_ITEM);
pane.pseudoClassStateChanged(CARD, control.style == Style.CARD);
if (control.imageUrl != null) {
hbox.getChildren().add(FXUtils.limitingSize(new ImageView(new Image(control.imageUrl, 32, 32, true, true)), 32, 32));
if (control.iconType != null) {
ImageView view = new ImageView(control.iconType.getIcon());
Node node = FXUtils.limitingSize(view, 32, 32);
node.setMouseTransparent(true);
node.getStyleClass().add("installer-item-image");
pane.getChildren().add(node);
if (control.style == Style.CARD) {
VBox.setMargin(node, new Insets(8, 0, 16, 0));
}
}
Label nameLabel = new Label();
hbox.getChildren().add(nameLabel);
nameLabel.setPrefWidth(80);
nameLabel.getStyleClass().add("installer-item-name");
nameLabel.setMouseTransparent(true);
pane.getChildren().add(nameLabel);
nameLabel.textProperty().set(I18n.hasKey("install.installer." + control.id) ? i18n("install.installer." + control.id) : control.id);
HBox.setMargin(nameLabel, new Insets(0, 4, 0, 4));
Label label = new Label();
hbox.getChildren().add(label);
label.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(label, Priority.ALWAYS);
label.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) {
return i18n("install.installer.not_installed");
Label statusLabel = new Label();
statusLabel.getStyleClass().add("installer-item-status");
statusLabel.setMouseTransparent(true);
pane.getChildren().add(statusLabel);
HBox.setHgrow(statusLabel, Priority.ALWAYS);
statusLabel.textProperty().bind(Bindings.createStringBinding(() -> {
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 control.style == Style.CARD
? i18n("install.installer.do_not_install")
: 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));
BorderPane.setMargin(label, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
}, control.resolvedStateProperty));
BorderPane.setMargin(statusLabel, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
JFXButton closeButton = new JFXButton();
closeButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
closeButton.getStyleClass().add("toggle-icon4");
closeButton.visibleProperty().bind(control.removable);
closeButton.managedProperty().bind(closeButton.visibleProperty());
closeButton.onMouseClickedProperty().bind(control.removeAction);
hbox.getChildren().add(closeButton);
HBox buttonsContainer = new HBox();
buttonsContainer.setSpacing(8);
buttonsContainer.setAlignment(Pos.CENTER);
pane.getChildren().add(buttonsContainer);
JFXButton arrowButton = new JFXButton();
arrowButton.graphicProperty().bind(Bindings.createObjectBinding(() -> control.upgradable.get()
? SVG.update(Theme.blackFillBinding(), -1, -1)
: SVG.arrowRight(Theme.blackFillBinding(), -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);
hbox.getChildren().add(arrowButton);
JFXButton removeButton = new JFXButton();
removeButton.setGraphic(SVG.CLOSE.createIcon(Theme.blackFill(), -1));
removeButton.getStyleClass().add("toggle-icon4");
if (control.id.equals(MINECRAFT.getPatchId())) {
removeButton.setVisible(false);
} else {
removeButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
State state = control.resolvedStateProperty.get();
return state instanceof InstalledState && !((InstalledState) state).external;
}, control.resolvedStateProperty));
}
removeButton.managedProperty().bind(removeButton.visibleProperty());
removeButton.setOnAction(e -> {
Runnable onRemove = control.getOnRemove();
if (onRemove != null)
onRemove.run();
});
buttonsContainer.getChildren().add(removeButton);
FXUtils.onChangeAndOperate(arrowButton.visibleProperty(), clickable -> {
JFXButton installButton = new JFXButton();
installButton.graphicProperty().bind(Bindings.createObjectBinding(() ->
control.resolvedStateProperty.get() instanceof InstallableState ?
SVG.ARROW_FORWARD.createIcon(Theme.blackFill(), -1) :
SVG.UPDATE.createIcon(Theme.blackFill(), -1),
control.resolvedStateProperty
));
installButton.getStyleClass().add("toggle-icon4");
installButton.visibleProperty().bind(Bindings.createBooleanBinding(() -> {
if (control.getOnInstall() == null) {
return false;
}
State state = control.resolvedStateProperty.get();
if (state instanceof InstallableState) {
return true;
}
if (state instanceof InstalledState) {
return !((InstalledState) state).external;
}
return false;
}, control.resolvedStateProperty, control.onInstall));
installButton.managedProperty().bind(installButton.visibleProperty());
installButton.setOnAction(e -> {
Runnable onInstall = control.getOnInstall();
if (onInstall != null)
onInstall.run();
});
buttonsContainer.getChildren().add(installButton);
FXUtils.onChangeAndOperate(installButton.visibleProperty(), clickable -> {
if (clickable) {
hbox.onMouseClickedProperty().bind(control.action);
hbox.setCursor(Cursor.HAND);
container.setOnMouseClicked(event -> {
Runnable onInstall = control.getOnInstall();
if (onInstall != null && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
onInstall.run();
event.consume();
}
});
pane.setCursor(Cursor.HAND);
} else {
hbox.onMouseClickedProperty().unbind();
hbox.onMouseClickedProperty().set(null);
hbox.setCursor(Cursor.DEFAULT);
container.setOnMouseClicked(null);
pane.setCursor(Cursor.DEFAULT);
}
});
}
@@ -17,17 +17,19 @@
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.control.Control;
import static org.jackhuang.hmcl.ui.construct.SpinnerPane.FAILED_ACTION;
public class ListPageBase<T> extends Control {
private final ListProperty<T> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading", false);
private final StringProperty failedReason = new SimpleStringProperty(this, "failed");
public ObservableList<T> getItems() {
return items.get();
@@ -52,4 +54,35 @@ public class ListPageBase<T> extends Control {
public BooleanProperty loadingProperty() {
return loading;
}
public String getFailedReason() {
return failedReason.get();
}
public StringProperty failedReasonProperty() {
return failedReason;
}
public void setFailedReason(String failedReason) {
this.failedReason.set(failedReason);
}
public final ObjectProperty<EventHandler<Event>> onFailedActionProperty() {
return onFailedAction;
}
public final void setOnFailedAction(EventHandler<Event> value) {
onFailedActionProperty().set(value);
}
public final EventHandler<Event> getOnFailedAction() {
return onFailedActionProperty().get();
}
private ObjectProperty<EventHandler<Event>> onFailedAction = new SimpleObjectProperty<EventHandler<Event>>(this, "onFailedAction") {
@Override
protected void invalidated() {
setEventHandler(FAILED_ACTION, get());
}
};
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -56,31 +55,30 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
}
VBox vBox = new VBox();
{
vBox.getStyleClass().add("card-list");
vBox.setAlignment(Pos.BOTTOM_RIGHT);
vBox.setPickOnBounds(false);
vBox.setPadding(new Insets(15));
vBox.setSpacing(15);
JFXButton btnAdd = new JFXButton();
FXUtils.setLimitWidth(btnAdd, 40);
FXUtils.setLimitHeight(btnAdd, 40);
btnAdd.getStyleClass().add("jfx-button-raised-round");
btnAdd.setButtonType(JFXButton.ButtonType.RAISED);
btnAdd.setGraphic(SVG.plus(Theme.whiteFillBinding(), -1, -1));
btnAdd.setOnMouseClicked(e -> skinnable.add());
btnAdd.setGraphic(SVG.ADD.createIcon(Theme.whiteFill(), -1));
btnAdd.setOnAction(e -> skinnable.add());
JFXButton btnRefresh = new JFXButton();
FXUtils.setLimitWidth(btnRefresh, 40);
FXUtils.setLimitHeight(btnRefresh, 40);
btnRefresh.getStyleClass().add("jfx-button-raised-round");
btnRefresh.setButtonType(JFXButton.ButtonType.RAISED);
btnRefresh.setGraphic(SVG.refresh(Theme.whiteFillBinding(), -1, -1));
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
btnRefresh.setGraphic(SVG.REFRESH.createIcon(Theme.whiteFill(), -1));
btnRefresh.setOnAction(e -> skinnable.refresh());
vBox.getChildren().setAll(btnAdd);
@@ -25,153 +25,163 @@ import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.Label;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets;
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.SystemUtils;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
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.ArrayDeque;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
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;
/**
*
* @author huangyuhui
*/
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 WeakChangeListener<Number> logLinesListener = FXUtils.onWeakChange(config().logLinesProperty(), logLines -> checkLogCount());
private boolean stopCheckLogCount = false;
public LogWindow() {
setScene(new Scene(impl, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets());
setTitle(i18n("logwindow.title"));
getIcons().add(newImage("/assets/img/icon.png"));
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
}
public void logLine(String line, Log4jLevel level) {
Log log = new Log(parseEscapeSequence(line), level);
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));
StyleSheets.init(getScene());
setTitle(i18n("logwindow.title"));
FXUtils.setIcon(this);
for (SimpleBooleanProperty property : levelShownMap.values()) {
property.addListener(o -> shakeLogs());
}
this.gameProcess = gameProcess;
}
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() {
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();
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) {
// TODO: fix O(n)
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;
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 ListView<Log> listView = new JFXListView<>();
private BooleanProperty autoScroll = new SimpleBooleanProperty();
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
private List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private JFXComboBox<String> cboLines = new JFXComboBox<>();
private final ListView<Log> listView = new JFXListView<>();
private final BooleanProperty autoScroll = 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");
boolean flag = false;
cboLines.getItems().setAll("500", "2000", "5000");
for (String i : cboLines.getItems())
if (Integer.toString(config().getLogLines()).equals(i)) {
cboLines.getSelectionModel().select(i);
flag = true;
}
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
config().setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
});
for (int i = 0; i < LEVELS.length; i++) {
buttonText[i] = new SimpleStringProperty();
showLevel[i] = new SimpleBooleanProperty(true);
}
if (!flag)
cboLines.getSelectionModel().select(0);
cboLines.getItems().setAll(500, 2000, 5000, 10000);
cboLines.setValue(Log.getLogLines());
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> config().setLogLines(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]);
}
}
@@ -188,19 +198,46 @@ 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;
}
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException | IllegalArgumentException ignored) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
alert.showAndWait();
});
FXUtils.showFileInExplorer(logFile);
});
}
private void onExportDump(SpinnerPane pane) {
assert SystemUtils.supportJVMAttachment();
pane.setLoading(true);
thread(() -> {
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.warning("Failed to create minecraft jstack dump", e);
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));
});
}
@@ -210,26 +247,19 @@ public final class LogWindow extends Stage {
}
}
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
private static PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
private static PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
private static PseudoClass ERROR = PseudoClass.getPseudoClass("error");
private static PseudoClass WARN = PseudoClass.getPseudoClass("warn");
private static PseudoClass INFO = PseudoClass.getPseudoClass("info");
private static PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
private static PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
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");
private static final PseudoClass WARN = PseudoClass.getPseudoClass("warn");
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private 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;
}
private final Set<ListCell<Log>> selected = new HashSet<>();
protected LogWindowSkin(LogWindowImpl control) {
LogWindowSkin(LogWindowImpl control) {
super(control);
VBox vbox = new VBox(3);
@@ -254,13 +284,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);
}
@@ -273,10 +306,14 @@ public final class LogWindow extends Stage {
if (!listView.getItems().isEmpty() && control.autoScroll.get())
listView.scrollTo(listView.getItems().size() - 1);
});
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
listView.setStyle("-fx-font-family: \"" + Lang.requireNonNullElse(config().getFontFamily(), FXUtils.DEFAULT_MONOSPACE_FONT)
+ "\"; -fx-font-size: " + config().getFontSize() + "px;");
Holder<Object> lastCell = new Holder<>();
listView.setCellFactory(x -> new ListCell<Log>() {
{
Region clippedContainer = (Region)listView.lookup(".clipped-container");
getStyleClass().add("log-window-list-cell");
Region clippedContainer = (Region) listView.lookup(".clipped-container");
if (clippedContainer != null) {
maxWidthProperty().bind(clippedContainer.widthProperty());
prefWidthProperty().bind(clippedContainer.widthProperty());
@@ -284,32 +321,85 @@ public final class LogWindow extends Stage {
setPadding(new Insets(2));
setWrapText(true);
setGraphic(null);
setOnMouseClicked(event -> {
if (event.getButton() != MouseButton.PRIMARY)
return;
if (!event.isControlDown()) {
for (ListCell<Log> logListCell : selected) {
if (logListCell != this) {
logListCell.pseudoClassStateChanged(SELECTED, false);
if (logListCell.getItem() != null) {
logListCell.getItem().setSelected(false);
}
}
}
selected.clear();
}
selected.add(this);
pseudoClassStateChanged(SELECTED, true);
if (getItem() != null) {
getItem().setSelected(true);
}
event.consume();
});
}
@Override
protected void updateItem(Log item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.value && !isVisible())
return;
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(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());
}
}
});
listView.setOnKeyPressed(event -> {
if (event.isControlDown() && event.getCode() == KeyCode.C) {
StringBuilder stringBuilder = new StringBuilder();
for (Log item : listView.getItems()) {
if (item != null && item.isSelected()) {
if (item.getLog() != null)
stringBuilder.append(item.getLog());
stringBuilder.append('\n');
}
}
FXUtils.copyText(stringBuilder.toString());
}
});
VBox.setVgrow(listView, Priority.ALWAYS);
vbox.getChildren().add(listView);
}
{
BorderPane bottom = new BorderPane();
HBox hBox = new HBox(3);
bottom.setRight(hBox);
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.setPadding(new Insets(0, 3, 0, 3));
@@ -317,17 +407,27 @@ public final class LogWindow extends Stage {
autoScrollCheckBox.setSelected(true);
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
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.setOnAction(e -> getSkinnable().onTerminateGame());
SpinnerPane exportDumpPane = new SpinnerPane();
JFXButton exportDumpButton = new JFXButton(i18n("logwindow.export_dump"));
if (SystemUtils.supportJVMAttachment()) {
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpPane));
} else {
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, clearButton);
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
vbox.getChildren().add(hBox);
vbox.getChildren().add(bottom);
}
}
}
+120 -155
View File
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.ui;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
@@ -25,173 +25,138 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
public final class SVG {
private SVG() {
/**
* All vector icons used in the launcher.
* <p>
* Unless otherwise stated,
* these icons are from <a href="https://fonts.google.com/icons?icon.size=24&icon.color=%235f6368&icon.query=list&icon.set=Material+Symbols">Material Symbols</a>,
* with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
* The view boxes of all icons are normalized to {@code 0 0 24 24}.
*/
public enum SVG {
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
ARCHIVE("M12 18 16 14 14.6 12.6 13 14.2V10H11V14.2L9.4 12.6 8 14 12 18ZM5 8V19H19V8H5ZM5 21Q4.175 21 3.5875 20.4125T3 19V6.525Q3 6.175 3.1125 5.85T3.45 5.25L4.7 3.725Q4.975 3.375 5.3875 3.1875T6.25 3H17.75Q18.2 3 18.6125 3.1875T19.3 3.725L20.55 5.25Q20.775 5.525 20.8875 5.85T21 6.525V19Q21 19.825 20.4125 20.4125T19 21H5ZM5.4 6H18.6L17.75 5H6.25L5.4 6ZM12 13.5Z"),
ARROW_BACK("M7.825 13 13.425 18.6 12 20 4 12 12 4 13.425 5.4 7.825 11H20V13H7.825Z"),
ARROW_DROP_DOWN("M12 15 7 10H17L12 15Z"),
ARROW_DROP_UP("M7 14 12 9 17 14H7Z"),
ARROW_FORWARD("M16.175 13H4V11H16.175L10.575 5.4 12 4 20 12 12 20 10.575 18.6 16.175 13Z"),
BETA_CIRCLE("M15,10.5C15,11.3 14.3,12 13.5,12C14.3,12 15,12.7 15,13.5V15A2,2 0 0,1 13,17H9V7H13A2,2 0 0,1 15,9V10.5M13,15V13H11V15H13M13,11V9H11V11H13M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material
CANCEL("M8.4 17 12 13.4 15.6 17 17 15.6 13.4 12 17 8.4 15.6 7 12 10.6 8.4 7 7 8.4 10.6 12 7 15.6 8.4 17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
CHAT("M6 14H14V12H6V14ZM6 11H18V9H6V11ZM6 8H18V6H6V8ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
CHECK("M9.55 18 3.85 12.3 5.275 10.875 9.55 15.15 18.725 5.975 20.15 7.4 9.55 18Z"),
CHECKROOM("M3 20Q2.575 20 2.2875 19.7125T2 19Q2 18.75 2.1 18.5375T2.4 18.2L11 11.75V10Q11 9.575 11.3 9.2875T12.025 9Q12.65 9 13.075 8.55T13.5 7.475Q13.5 6.85 13.0625 6.425T12 6Q11.375 6 10.9375 6.4375T10.5 7.5H8.5Q8.5 6.05 9.525 5.025T12 4Q13.45 4 14.475 5.0125T15.5 7.475Q15.5 8.65 14.8125 9.575T13 10.85V11.75L21.6 18.2Q21.8 18.325 21.9 18.5375T22 19Q22 19.425 21.7125 19.7125T21 20H3ZM6 18H18L12 13.5 6 18Z"),
CHECK_CIRCLE("M10.6 16.6 17.65 9.55 16.25 8.15 10.6 13.8 7.75 10.95 6.35 12.35 10.6 16.6ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
CLOSE("M6.4 19 5 17.6 10.6 12 5 6.4 6.4 5 12 10.6 17.6 5 19 6.4 13.4 12 19 17.6 17.6 19 12 13.4 6.4 19Z"),
CONTENT_COPY("M9 18Q8.175 18 7.5875 17.4125T7 16V4Q7 3.175 7.5875 2.5875T9 2H18Q18.825 2 19.4125 2.5875T20 4V16Q20 16.825 19.4125 17.4125T18 18H9ZM9 16H18V4H9V16ZM5 22Q4.175 22 3.5875 21.4125T3 20V6H5V20H16V22H5ZM9 16V4 16Z"),
CREATE_NEW_FOLDER("M14 16h2V14h2V12H16V10H14v2H12v2h2v2ZM4 20q-.825 0-1.4125-.5875T2 18V6q0-.825.5875-1.4125T4 4h6l2 2h8q.825 0 1.4125.5875T22 8V18q0 .825-.5875 1.4125T20 20H4Zm0-2H20V8H11.175l-2-2H4V18ZV6 18Z"),
DELETE("M7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM9 17H11V8H9V17ZM13 17H15V8H13V17ZM7 6V19 6Z"),
DELETE_FOREVER("M9.4 16.5 12 13.9 14.6 16.5 16 15.1 13.4 12.5 16 9.9 14.6 8.5 12 11.1 9.4 8.5 8 9.9 10.6 12.5 8 15.1 9.4 16.5ZM7 21Q6.175 21 5.5875 20.4125T5 19V6H4V4H9V3H15V4H20V6H19V19Q19 19.825 18.4125 20.4125T17 21H7ZM17 6H7V19H17V6ZM7 6V19 6Z"),
DEPLOYED_CODE("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM12 10.85 17.925 7.425 12 4 6.075 7.425 12 10.85ZM4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725L4 17.7ZM12 12Z"),
DOWNLOAD("M12 16 7 11 8.4 9.55 11 12.15V4H13V12.15L15.6 9.55 17 11 12 16ZM6 20Q5.175 20 4.5875 19.4125T4 18V15H6V18H18V15H20V18Q20 18.825 19.4125 19.4125T18 20H6Z"),
DRESSER("M4 21V5Q4 4.175 4.5875 3.5875T6 3H18Q18.825 3 19.4125 3.5875T20 5V21H18V19H6V21H4ZM6 11H11V5H6V11ZM13 7H18V5H13V7ZM13 11H18V9H13V11ZM10 16H14V14H10V16ZM6 13V17H18V13H6ZM6 13V17 13Z"),
EDIT("M5 19H6.425L16.2 9.225 14.775 7.8 5 17.575V19ZM3 21V16.75L16.2 3.575Q16.5 3.3 16.8625 3.15T17.625 3Q18.025 3 18.4 3.15T19.05 3.6L20.425 5Q20.725 5.275 20.8625 5.65T21 6.4Q21 6.8 20.8625 7.1625T20.425 7.825L7.25 21H3ZM19 6.4 17.6 5 19 6.4ZM15.475 8.525 14.775 7.8 16.2 9.225 15.475 8.525Z"),
ERROR("M12 17Q12.425 17 12.7125 16.7125T13 16Q13 15.575 12.7125 15.2875T12 15Q11.575 15 11.2875 15.2875T11 16Q11 16.425 11.2875 16.7125T12 17ZM11 13H13V7H11V13ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
EXPLORE("M12 12Zm0 8q-3.325 0-5.6625-2.3375T4 12Q4 8.675 6.3375 6.3375T12 4q3.325-0 5.6625 2.3375T20 12q0 3.325-2.3375 5.6625T12 20Zm0 2q2.075-0 3.9-.7875t3.175-2.1375q1.35-1.35 2.1375-3.175T22 12q-0-2.075-.7875-3.9T19.075 4.925q-1.35-1.35-3.175-2.1375T12 2q-2.075 0-3.9.7875T4.925 4.925Q3.575 6.275 2.7875 8.1T2 12q0 2.075.7875 3.9T4.925 19.075q1.35 1.35 3.175 2.1375T12 22Zm0-8.5q.625 0 1.0625-.4375T13.5 12t-.4375-1.0625T12 10.5t-1.0625.4375T10.5 12t.4375 1.0625T12 13.5Zm-4.5 3 2-7 7-2-2 7-7 2Z"),
EXTENSION("M8.8 21H5Q4.175 21 3.5875 20.4125T3 19V15.2Q4.2 15.2 5.1 14.4375T6 12.5Q6 11.325 5.1 10.5625T3 9.8V6Q3 5.175 3.5875 4.5875T5 4H9Q9 2.95 9.725 2.225T11.5 1.5Q12.55 1.5 13.275 2.225T14 4H18Q18.825 4 19.4125 4.5875T20 6V10Q21.05 10 21.775 10.725T22.5 12.5Q22.5 13.55 21.775 14.275T20 15V19Q20 19.825 19.4125 20.4125T18 21H14.2Q14.2 19.75 13.4125 18.875T11.5 18Q10.375 18 9.5875 18.875T8.8 21ZM5 19H7.125Q7.725 17.35 9.05 16.675T11.5 16Q12.625 16 13.95 16.675T15.875 19H18V13H20Q20.2 13 20.35 12.85T20.5 12.5Q20.5 12.3 20.35 12.15T20 12H18V6H12V4Q12 3.8 11.85 3.65T11.5 3.5Q11.3 3.5 11.15 3.65T11 4V6H5V8.2Q6.35 8.7 7.175 9.875T8 12.5Q8 13.925 7.175 15.1T5 16.8V19ZM11.5 12.5Z"),
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
FORMAT_LIST_BULLETED("M9 19V17H21V19H9ZM9 13V11H21V13H9ZM9 7V5H21V7H9ZM5 20Q4.175 20 3.5875 19.4125T3 18Q3 17.175 3.5875 16.5875T5 16Q5.825 16 6.4125 16.5875T7 18Q7 18.825 6.4125 19.4125T5 20ZM5 14Q4.175 14 3.5875 13.4125T3 12Q3 11.175 3.5875 10.5875T5 10Q5.825 10 6.4125 10.5875T7 12Q7 12.825 6.4125 13.4125T5 14ZM5 8Q4.175 8 3.5875 7.4125T3 6Q3 5.175 3.5875 4.5875T5 4Q5.825 4 6.4125 4.5875T7 6Q7 6.825 6.4125 7.4125T5 8Z"),
FORT("M1 21V17l2-2V9L1 7V3H3V5H5V3H7V5H9V3h2V7L9 9v1h6V9L13 7V3h2V5h2V3h2V5h2V3h2V7L21 9v6l2 2v4H14V18q0-.825-.5875-1.4125T12 16q-.825 0-1.4125.5875T10 18v3H1Zm2-2H8V18q0-1.65 1.175-2.825T12 14q1.65 0 2.825 1.175T16 18v1h5V17.825l-2-2V8.175L20.175 7h-4.35L17 8.175V12H7V8.175L8.175 7H3.825L5 8.175v7.65l-2 2V19Zm9-6Z"),
FOR_YOU("M12 12Q14.025 12 16.225 11.5875T20 10.5V20.5Q18.5 21.175 16.35 21.5875T12 22Q9.8 22 7.65 21.5875T4 20.5V10.5Q5.575 11.175 7.775 11.5875T12 12ZM18 19V13.25Q16.75 13.6 15.1125 13.8T12 14Q10.525 14 8.8875 13.8T6 13.25V19Q7.25 19.45 8.875 19.725T12 20Q13.5 20 15.125 19.725T18 19ZM12 2Q13.65 2 14.825 3.175T16 6Q16 7.65 14.825 8.825T12 10Q10.35 10 9.175 8.825T8 6Q8 4.35 9.175 3.175T12 2ZM12 8Q12.825 8 13.4125 7.4125T14 6Q14 5.175 13.4125 4.5875T12 4Q11.175 4 10.5875 4.5875T10 6Q10 6.825 10.5875 7.4125T12 8ZM12 6ZM12 16.625Z"),
GAMEPAD("M12 7.65ZM16.35 12ZM7.65 12ZM12 16.35ZM12 10.5 9 7.5V2H15V7.5L12 10.5ZM16.5 15 13.5 12 16.5 9H22V15H16.5ZM2 15V9H7.5L10.5 12 7.5 15H2ZM9 22V16.5L12 13.5 15 16.5V22H9ZM12 7.65 13 6.65V4H11V6.65L12 7.65ZM4 13H6.65L7.65 12 6.65 11H4V13ZM11 20H13V17.35L12 16.35 11 17.35V20ZM17.35 13H20V11H17.35L16.35 12 17.35 13Z"),
GLOBE_BOOK("M3.075 13Q3.05 12.75 3.0375 12.5T3.025 12Q3.025 10.125 3.725 8.4875T5.65 5.6375Q6.875 4.425 8.5 3.7125T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 12.25 20.9875 12.5T20.95 13H18.925Q18.975 12.75 18.9875 12.5T19 12Q19 11.75 18.9875 11.5T18.925 11H15.975Q16 11.25 16 11.5V12.5Q16 12.75 15.975 13H14V12.175Q14 11.875 13.9875 11.575T13.95 11H10.075Q10.05 11.275 10.0375 11.575T10.025 12.175V13H8.05Q8.025 12.75 8.025 12.5V11.5Q8.025 11.25 8.05 11H5.1Q5.05 11.25 5.0375 11.5T5.025 12Q5.025 12.25 5.0375 12.5T5.1 13H3.075ZM5.7 9H8.275Q8.475 7.925 8.775 7.0625T9.425 5.5Q8.225 5.95 7.25 6.8625T5.7 9ZM10.35 9H13.65Q13.4 7.925 13.025 6.9T12 5Q11.35 5.875 10.9625 6.9T10.35 9ZM15.75 9H18.325Q17.75 7.775 16.7625 6.8625T14.575 5.5Q14.925 6.25 15.2375 7.0875T15.75 9ZM11 21V20Q11 18.75 10.125 17.875T8 17H2V15H8Q9.2 15 10.2375 15.525T12 17Q12.725 16.05 13.7625 15.525T16 15H22V17H16Q14.75 17 13.875 17.875T13 20V21H11Z"),
HELP("M11.95 18Q12.475 18 12.8375 17.6375T13.2 16.75Q13.2 16.225 12.8375 15.8625T11.95 15.5Q11.425 15.5 11.0625 15.8625T10.7 16.75Q10.7 17.275 11.0625 17.6375T11.95 18ZM11.05 14.15H12.9Q12.9 13.325 13.0875 12.85T14.15 11.55Q14.8 10.9 15.175 10.3125T15.55 8.9Q15.55 7.5 14.525 6.75T12.1 6Q10.675 6 9.7875 6.75T8.55 8.55L10.2 9.2Q10.325 8.75 10.7625 8.225T12.1 7.7Q12.9 7.7 13.3 8.1375T13.7 9.1Q13.7 9.6 13.4 10.0375T12.65 10.85Q11.55 11.825 11.3 12.325T11.05 14.15ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
HOME("M6 19H9V13H15V19H18V10L12 5.5 6 10V19ZM4 21V9L12 3 20 9V21H13V15H11V21H4ZM12 12.25Z"),
HOST("M4 21Q3.175 21 2.5875 20.4125T2 19V5Q2 4.175 2.5875 3.5875T4 3H9Q9.825 3 10.4125 3.5875T11 5V19Q11 19.825 10.4125 20.4125T9 21H4ZM15 21Q14.175 21 13.5875 20.4125T13 19V5Q13 4.175 13.5875 3.5875T15 3H20Q20.825 3 21.4125 3.5875T22 5V19Q22 19.825 21.4125 20.4125T20 21H15ZM4 19H9V5H4V19ZM15 19H20V5H15V19ZM5 15H8V13H5V15ZM16 15H19V13H16V15ZM5 12H8V10H5V12ZM16 12H19V10H16V12ZM5 9H8V7H5V9ZM16 9H19V7H16V9ZM4 19H9 4ZM15 19H20 15Z"),
INFO("M11 17H13V11H11V17ZM12 9Q12.425 9 12.7125 8.7125T13 8Q13 7.575 12.7125 7.2875T12 7Q11.575 7 11.2875 7.2875T11 8Q11 8.425 11.2875 8.7125T12 9ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
KEYBOARD_ARROW_DOWN("M12 15.4 6 9.4 7.4 8 12 12.6 16.6 8 18 9.4 12 15.4Z"),
KEYBOARD_ARROW_UP("M12 10.8 7.4 15.4 6 14 12 8 18 14 16.6 15.4 12 10.8Z"),
LANDSCAPE("M1 18l6-8 4.5 6H19L14 9.35l-2.5 3.3L10.25 11 14 6l9 12H1Zm13.025-2ZM5 16H9L7 13.325 5 16ZH9 5Z"),
LIST("M7 9V7H21V9H7ZM7 13V11H21V13H7ZM7 17V15H21V17H7ZM4 9Q3.575 9 3.2875 8.7125T3 8Q3 7.575 3.2875 7.2875T4 7Q4.425 7 4.7125 7.2875T5 8Q5 8.425 4.7125 8.7125T4 9ZM4 13Q3.575 13 3.2875 12.7125T3 12Q3 11.575 3.2875 11.2875T4 11Q4.425 11 4.7125 11.2875T5 12Q5 12.425 4.7125 12.7125T4 13ZM4 17Q3.575 17 3.2875 16.7125T3 16Q3 15.575 3.2875 15.2875T4 15Q4.425 15 4.7125 15.2875T5 16Q5 16.425 4.7125 16.7125T4 17Z"),
LISTS("M2 20V16H6V20H2ZM8 20V16H22V20H8ZM2 14V10H6V14H2ZM8 14V10H22V14H8ZM2 8V4H6V8H2ZM8 8V4H22V8H8Z"),
LOCAL_CAFE("M4 21V19H20V21H4ZM8 17Q6.35 17 5.175 15.825T4 13V3H20Q20.825 3 21.4125 3.5875T22 5V8Q22 8.825 21.4125 9.4125T20 10H18V13Q18 14.65 16.825 15.825T14 17H8ZM8 15H14Q14.825 15 15.4125 14.4125T16 13V5H6V13Q6 13.825 6.5875 14.4125T8 15ZM18 8H20V5H18V8ZM8 15H6 16 8Z"),
LOCATION_CITY("M3 21V7H9V5l3-3 3 3v6h6V21H3Zm2-2H7V17H5v2Zm0-4H7V13H5v2Zm0-4H7V9H5v2Zm6 8h2V17H11v2Zm0-4h2V13H11v2Zm0-4h2V9H11v2Zm0-4h2V5H11V7Zm6 12h2V17H17v2Zm0-4h2V13H17v2Z"),
MENU("M3 18V16H21V18H3ZM3 13V11H21V13H3ZM3 8V6H21V8H3Z"),
MICROSOFT("M4 20H22v2H4V13H20v7h2V4H20v7H4V4h7V20h2V4h9V2H2V22H4"), // Not Material
MINIMIZE("M6 21V19H18V21H6Z"),
MOJANG("M13.9658 0C12.9552.828 12.7686 2.195 12.7007 3.418 12.7219 4.4201 14.0423 4.7174 14.6538 4.0082 15.0912 2.6579 14.3692 1.2739 13.9658 0ZM10.913 2.9297C10.7559 3.6983 10.6284 4.4669 10.4925 5.2355 8.9894 3.9913 7.1505 3.0825 5.142 3.2948 3.4944 3.4646.9429 2.6961.1404 4.6452-.1229 8.314.0722 12.0124.034 15.6896-.0891 16.9126.8957 18.1101 2.1781 17.9699 6.3989 18.0039 10.6283 18.0172 14.8491 17.9661 16.1145 18.102 16.8067 16.9554 16.9851 15.8768 13.2696 16.4374 9.1761 16.9296 5.7111 15.1292 2.5986 13.5836 2.246 8.3139 5.5581 6.798 9.3203 5.1759 13.8596 8.0383 14.9424 11.7877 15.6133 12.1105 16.2844 12.4247 16.9596 12.7432 16.6241 10.3483 16.6537 7.8005 15.5793 5.5882 15.0571 4.7474 14.0975 6.2714 13.3799 5.6217 12.416 4.8659 11.7495 3.8129 10.913 2.9297Z"), // Not Material
MORE_HORIZ("M6 14Q5.175 14 4.5875 13.4125T4 12Q4 11.175 4.5875 10.5875T6 10Q6.825 10 7.4125 10.5875T8 12Q8 12.825 7.4125 13.4125T6 14ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM18 14Q17.175 14 16.5875 13.4125T16 12Q16 11.175 16.5875 10.5875T18 10Q18.825 10 19.4125 10.5875T20 12Q20 12.825 19.4125 13.4125T18 14Z"),
MORE_VERT("M12 20Q11.175 20 10.5875 19.4125T10 18Q10 17.175 10.5875 16.5875T12 16Q12.825 16 13.4125 16.5875T14 18Q14 18.825 13.4125 19.4125T12 20ZM12 14Q11.175 14 10.5875 13.4125T10 12Q10 11.175 10.5875 10.5875T12 10Q12.825 10 13.4125 10.5875T14 12Q14 12.825 13.4125 13.4125T12 14ZM12 8Q11.175 8 10.5875 7.4125T10 6Q10 5.175 10.5875 4.5875T12 4Q12.825 4 13.4125 4.5875T14 6Q14 6.825 13.4125 7.4125T12 8Z"),
OPEN_IN_NEW("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H12V5H5V19H19V12H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM9.7 15.7 8.3 14.3 17.6 5H14V3H21V10H19V6.4L9.7 15.7Z"),
OUTPUT("M5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H19Q19.825 3 20.4125 3.5875T21 5V7H19V5H5V19H19V17H21V19Q21 19.825 20.4125 20.4125T19 21H5ZM17 17 15.6 15.6 18.175 13H9V11H18.175L15.6 8.4 17 7 22 12 17 17Z"),
PACKAGE("M10 9.75 12 8.75 14 9.75V5H10V9.75ZM7 17V15H12V17H7ZM5 21Q4.175 21 3.5875 20.4125T3 19V5Q3 4.175 3.5875 3.5875T5 3H19Q19.825 3 20.4125 3.5875T21 5V19Q21 19.825 20.4125 20.4125T19 21H5ZM5 5V19 5ZM5 19H19V5H16V13L12 11 8 13V5H5V19Z"),
PACKAGE2("M11 19.425V12.575L5 9.1V15.95L11 19.425ZM13 19.425 19 15.95V9.1L13 12.575V19.425ZM11 21.725 4 17.7Q3.525 17.425 3.2625 16.975T3 15.975V8.025Q3 7.475 3.2625 7.025T4 6.3L11 2.275Q11.475 2 12 2T13 2.275L20 6.3Q20.475 6.575 20.7375 7.025T21 8.025V15.975Q21 16.525 20.7375 16.975T20 17.7L13 21.725Q12.525 22 12 22T11 21.725ZM16 8.525 17.925 7.425 12 4 10.05 5.125 16 8.525ZM12 10.85 13.95 9.725 8.025 6.3 6.075 7.425 12 10.85Z"),
PERSON("M12 12Q10.35 12 9.175 10.825T8 8Q8 6.35 9.175 5.175T12 4Q13.65 4 14.825 5.175T16 8Q16 9.65 14.825 10.825T12 12ZM4 20V17.2Q4 16.35 4.4375 15.6375T5.6 14.55Q7.15 13.775 8.75 13.3875T12 13Q13.65 13 15.25 13.3875T18.4 14.55Q19.125 14.925 19.5625 15.6375T20 17.2V20H4ZM6 18H18V17.2Q18 16.925 17.8625 16.7T17.5 16.35Q16.15 15.675 14.775 15.3375T12 15Q10.6 15 9.225 15.3375T6.5 16.35Q6.275 16.475 6.1375 16.7T6 17.2V18ZM12 10Q12.825 10 13.4125 9.4125T14 8Q14 7.175 13.4125 6.5875T12 6Q11.175 6 10.5875 6.5875T10 8Q10 8.825 10.5875 9.4125T12 10ZM12 8ZM12 18Z"),
PUBLIC("M12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM11 19.95V18Q10.175 18 9.5875 17.4125T9 16V15L4.2 10.2Q4.125 10.65 4.0625 11.1T4 12Q4 15.025 5.9875 17.3T11 19.95ZM17.9 17.4Q18.925 16.275 19.4625 14.8875T20 12Q20 9.55 18.6375 7.525T15 4.6V5Q15 5.825 14.4125 6.4125T13 7H11V9Q11 9.425 10.7125 9.7125T10 10H8V12H14Q14.425 12 14.7125 12.2875T15 13V16H16Q16.65 16 17.175 16.3875T17.9 17.4Z"),
REFRESH("M12 20Q8.65 20 6.325 17.675T4 12Q4 8.65 6.325 6.325T12 4Q13.725 4 15.3 4.7125T18 6.75V4H20V11H13V9H17.2Q16.4 7.6 15.0125 6.8T12 6Q9.5 6 7.75 7.75T6 12Q6 14.5 7.75 16.25T12 18Q13.925 18 15.475 16.9T17.65 14H19.75Q19.05 16.65 16.9 18.325T12 20Z"),
RELEASE_CIRCLE("M9,7H13A2,2 0 0,1 15,9V11C15,11.84 14.5,12.55 13.76,12.85L15,17H13L11.8,13H11V17H9V7M11,9V11H13V9H11M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,16.41 7.58,20 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z"), // Not Material
RESTORE("M12 21Q8.55 21 5.9875 18.7125T3.05 13H5.1Q5.45 15.6 7.4125 17.3T12 19Q14.925 19 16.9625 16.9625T19 12Q19 9.075 16.9625 7.0375T12 5Q10.275 5 8.775 5.8T6.25 8H9V10H3V4H5V6.35Q6.275 4.75 8.1125 3.875T12 3Q13.875 3 15.5125 3.7125T18.3625 5.6375Q19.575 6.85 20.2875 8.4875T21 12Q21 13.875 20.2875 15.5125T18.3625 18.3625Q17.15 19.575 15.5125 20.2875T12 21Z"), // Not Material
ROCKET_LAUNCH("M5.65 10.025 7.6 10.85Q7.95 10.15 8.325 9.5T9.15 8.2L7.75 7.925 5.65 10.025ZM9.2 12.1 12.05 14.925Q13.1 14.525 14.3 13.7T16.55 11.825Q18.3 10.075 19.2875 7.9375T20.15 4Q18.35 3.875 16.2 4.8625T12.3 7.6Q11.25 8.65 10.425 9.85T9.2 12.1ZM13.65 10.475Q13.075 9.9 13.075 9.0625T13.65 7.65Q14.225 7.075 15.075 7.075T16.5 7.65Q17.075 8.225 17.075 9.0625T16.5 10.475Q15.925 11.05 15.075 11.05T13.65 10.475ZM14.125 18.5 16.225 16.4 15.95 15Q15.3 15.45 14.65 15.8125T13.3 16.525L14.125 18.5ZM21.95 2.175Q22.425 5.2 21.3625 8.0625T17.7 13.525L18.2 16Q18.3 16.5 18.15 16.975T17.65 17.8L13.45 22 11.35 17.075 7.075 12.8 2.15 10.7 6.325 6.5Q6.675 6.15 7.1625 6T8.15 5.95L10.625 6.45Q13.225 3.85 16.075 2.775T21.95 2.175ZM3.925 15.975Q4.8 15.1 6.0625 15.0875T8.2 15.95Q9.075 16.825 9.0625 18.0875T8.175 20.225Q7.55 20.85 6.0875 21.3T2.05 22.1Q2.4 19.525 2.85 18.0625T3.925 15.975ZM5.35 17.375Q5.1 17.625 4.85 18.2875T4.5 19.625Q5.175 19.525 5.8375 19.2875T6.75 18.8Q7.05 18.5 7.075 18.075T6.8 17.35Q6.5 17.05 6.075 17.0625T5.35 17.375Z"),
SCHEMA("M4 23V17H6.5V15H4V9H6.5V7H4V1h7V7H8.5V9H11v2h3V9h7v6H14V13H11v2H8.5v2H11v6H4Zm2-2H9V19H6v2Zm0-8H9V11H6v2Zm10 0h3V11H16v2ZM6 5H9V3H6V5ZM7.5 4Zm0 8Zm10 0Zm-10 8Z"),
SCREENSHOT_MONITOR("M15 16H19V12H17.5V14.5H15V16ZM5 10H6.5V7.5H9V6H5V10ZM8 21V19H4Q3.175 19 2.5875 18.4125T2 17V5Q2 4.175 2.5875 3.5875T4 3H20Q20.825 3 21.4125 3.5875T22 5V17Q22 17.825 21.4125 18.4125T20 19H16V21H8ZM4 17H20V5H4V17ZM4 17V5 17Z"),
SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material
SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"),
SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"),
SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons
STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"),
STYLE("M3.975 19.8 3.125 19.45Q2.35 19.125 2.0875 18.325T2.175 16.75L3.975 12.85V19.8ZM7.975 22Q7.15 22 6.5625 21.4125T5.975 20V14L8.625 21.35Q8.7 21.525 8.775 21.6875T8.975 22H7.975ZM13.125 21.9Q12.325 22.2 11.575 21.825T10.525 20.65L6.075 8.45Q5.775 7.65 6.125 6.8875T7.275 5.85L14.825 3.1Q15.625 2.8 16.375 3.175T17.425 4.35L21.875 16.55Q22.175 17.35 21.825 18.1125T20.675 19.15L13.125 21.9ZM10.975 10Q11.4 10 11.6875 9.7125T11.975 9Q11.975 8.575 11.6875 8.2875T10.975 8Q10.55 8 10.2625 8.2875T9.975 9Q9.975 9.425 10.2625 9.7125T10.975 10ZM12.425 20 19.975 17.25 15.525 5 7.975 7.75 12.425 20ZM7.975 7.75 15.525 5 7.975 7.75Z"),
TEXTURE("M4.4-3Q3.925-3.1 3.5125-3.5125T3-4.4L19.6-21Q20.125-20.875 20.5-20.4875T21.025-19.6L4.4-3ZM3-9.3V-12.1L11.9-21H14.7L3-9.3ZM3-17V-19Q3-19.825 3.5875-20.4125T5-21H7L3-17ZM17-3 21-7V-5Q21-4.175 20.4125-3.5875T19-3H17ZM9.3-3 21-14.7V-11.9L12.1-3H9.3Z"),
TRIP("M4 21Q3.175 21 2.5875 20.4125T2 19V8Q2 7.175 2.5875 6.5875T4 6H8V4Q8 3.175 8.5875 2.5875T10 2H14Q14.825 2 15.4125 2.5875T16 4V6H20Q20.825 6 21.4125 6.5875T22 8V19Q22 19.825 21.4125 20.4125T20 21H4ZM10 6H14V4H10V6ZM6 8H4V19H6V8ZM16 19V8H8V19H16ZM18 8V19H20V8H18ZM12 13.5Z"),
TUNE("M11 21V15H13V17H21V19H13V21H11ZM3 19V17H9V19H3ZM7 15V13H3V11H7V9H9V15H7ZM11 13V11H21V13H11ZM15 9V3H17V5H21V7H17V9H15ZM3 7V5H13V7H3Z"),
UPDATE("M12 21Q10.125 21 8.4875 20.2875T5.6375 18.3625Q4.425 17.15 3.7125 15.5125T3 12Q3 10.125 3.7125 8.4875T5.6375 5.6375Q6.85 4.425 8.4875 3.7125T12 3Q14.05 3 15.8875 3.875T19 6.35V4H21V10H15V8H17.75Q16.725 6.6 15.225 5.8T12 5Q9.075 5 7.0375 7.0375T5 12Q5 14.925 7.0375 16.9625T12 19Q14.625 19 16.5875 17.3T18.9 13H20.95Q20.575 16.425 18.0125 18.7125T12 21ZM14.8 16.2 11 12.4V7H13V11.6L16.2 14.8 14.8 16.2Z"),
VISIBILITY("M12 16q1.875 0 3.1875-1.3125T16.5 11.5 15.1875 8.3125 12 7 8.8125 8.3125 7.5 11.5t1.3125 3.1875T12 16Zm0-1.8q-1.125 0-1.9125-.7875T9.3 11.5t.7875-1.9125T12 8.8q1.125 0 1.9125.7875T14.7 11.5q0 1.125-.7875 1.9125T12 14.2ZM12 19q-3.65 0-6.65-2.0375T1 11.5Q2.35 8.075 5.35 6.0375T12 4q3.65 0 6.65 2.0375T23 11.5q-1.35 3.425-4.35 5.4625T12 19Zm0-7.5ZM12 17q2.825 0 5.1875-1.4875T20.8 11.5q-1.25-2.525-3.6125-4.0125T12 6 6.8125 7.4875 3.2 11.5q1.25 2.525 3.6125 4.0125T12 17Z"),
VISIBILITY_OFF("M16.1 13.3l-1.45-1.45q.225-1.175-.675-2.2t-2.325-.8L10.2 7.4q.425-.2.8625-.3T12 7q1.875 0 3.1875 1.3125T16.5 11.5q0 .5-.1.9375t-.3.8625Zm3.2 3.15-1.45-1.4q.95-.725 1.6875-1.5875T20.8 11.5q-1.25-2.525-3.5875-4.0125T12 6q-.725 0-1.425.1T9.2 6.4L7.65 4.85q1.025-.425 2.1-.6375T12 4q3.775 0 6.725 2.0875T23 11.5q-.575 1.475-1.5125 2.7375T19.3 16.45Zm.5 6.15-4.2-4.15q-.875.275-1.7625.4125T12 19q-3.775 0-6.725-2.0875T1 11.5q.525-1.325 1.325-2.4625T4.15 7L1.4 4.2 2.8 2.8 21.2 21.2l-1.4 1.4ZM5.55 8.4q-.725.65-1.325 1.425T3.2 11.5q1.25 2.525 3.5875 4.0125T12 17q.5 0 .975-.0625T13.95 16.8l-.9-.95q-.275.075-.525.1125T12 16q-1.875 0-3.1875-1.3125T7.5 11.5q0-.275.0375-.525T7.65 10.45L5.55 8.4Zm7.975 2.325ZM9.75 12.6Z"),
WARNING("M1 21 12 2 23 21H1ZM4.45 19H19.55L12 6 4.45 19ZM12 18Q12.425 18 12.7125 17.7125T13 17Q13 16.575 12.7125 16.2875T12 16Q11.575 16 11.2875 16.2875T11 17Q11 17.425 11.2875 17.7125T12 18ZM11 15H13V10H11V15ZM12 12.5Z"),
WB_SUNNY("M11 4V1H13V4H11ZM11 23V20H13V23H11ZM20 13V11H23V13H20ZM1 13V11H4V13H1ZM18.7 6.7 17.3 5.3 19.05 3.5 20.5 4.95 18.7 6.7ZM4.95 20.5 3.5 19.05 5.3 17.3 6.7 18.7 4.95 20.5ZM19.05 20.5 17.3 18.7 18.7 17.3 20.5 19.05 19.05 20.5ZM5.3 6.7 3.5 4.95 4.95 3.5 6.7 5.3 5.3 6.7ZM12 18Q9.5 18 7.75 16.25T6 12Q6 9.5 7.75 7.75T12 6Q14.5 6 16.25 7.75T18 12Q18 14.5 16.25 16.25T12 18ZM12 16Q13.675 16 14.8375 14.8375T16 12Q16 10.325 14.8375 9.1625T12 8Q10.325 8 9.1625 9.1625T8 12Q8 13.675 9.1625 14.8375T12 16ZM12 12Z"),
;
public static final double DEFAULT_SIZE = 24;
private final String path;
SVG(String path) {
this.path = path;
}
public interface SVGIcon {
Node createIcon(ObjectBinding<? extends Paint> fill, double width, double height);
public String getPath() {
return path;
}
private static Node createSVGPath(String d, ObjectBinding<? extends Paint> fill, double width, double height) {
SVGPath path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(d);
if (fill != null) path.fillProperty().bind(fill);
if (width < 0 || height < 0) {
private static Node createIcon(SVGPath path, double size) {
if (size < 0) {
StackPane pane = new StackPane(path);
pane.setAlignment(Pos.CENTER);
return pane;
}
Group svg = new Group(path);
double scale = Math.min(width / svg.getBoundsInParent().getWidth(), height / svg.getBoundsInParent().getHeight());
svg.setScaleX(scale);
svg.setScaleY(scale);
return svg;
double scale = size / 24;
path.setScaleX(scale);
path.setScaleY(scale);
return new Group(path);
}
// default fill: white, width: 20, height 20
public Node createIcon(ObservableValue<? extends Paint> fill, double size) {
SVGPath p = new SVGPath();
p.getStyleClass().add("svg");
p.setContent(path);
if (fill != null)
p.fillProperty().bind(fill);
public static Node gear(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z", fill, width, height);
return createIcon(p, size);
}
public static Node back(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z", fill, width, height);
public Node createIcon(Paint fill, double size) {
SVGPath p = new SVGPath();
p.getStyleClass().add("svg");
p.setContent(path);
if (fill != null)
p.fillProperty().set(fill);
return createIcon(p, size);
}
public static Node close(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height);
}
public static Node copy(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z", fill, width, height);
}
public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height);
}
public static Node dotsHorizontal(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z", fill, width, height);
}
public static Node delete(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z", fill, width, height);
}
public static Node accountEdit(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z", fill, width, height);
}
public static Node expand(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z", fill, width, height);
}
public static Node collapse(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z", fill, width, height);
}
public static Node navigate(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
public static Node launch(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M1008 6.286q18.857 13.714 15.429 36.571l-146.286 877.714q-2.857 16.571-18.286 25.714-8 4.571-17.714 4.571-6.286 0-13.714-2.857l-258.857-105.714-138.286 168.571q-10.286 13.143-28 13.143-7.429 0-12.571-2.286-10.857-4-17.429-13.429t-6.571-20.857v-199.429l493.714-605.143-610.857 528.571-225.714-92.571q-21.143-8-22.857-31.429-1.143-22.857 18.286-33.714l950.857-548.571q8.571-5.143 18.286-5.14311.429 0 20.571 6.286z", fill, width, height);
}
public static Node launch2(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
public static Node script(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z", fill, width, height);
}
public static Node pencil(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z", fill, width, height);
}
public static Node refresh(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z", fill, width, height);
}
public static Node folderOpen(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10L12,6H19A2,2 0 0,1 21,8H21L4,8V18L6.14,10H23.21L20.93,18.5C20.7,19.37 19.92,20 19,20Z", fill, width, height);
}
public static Node update(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,10.12H14.22L16.96,7.3C14.23,4.6 9.81,4.5 7.08,7.2C4.35,9.91 4.35,14.28 7.08,17C9.81,19.7 14.23,19.7 16.96,17C18.32,15.65 19,14.08 19,12.1H21C21,14.08 20.12,16.65 18.36,18.39C14.85,21.87 9.15,21.87 5.64,18.39C2.14,14.92 2.11,9.28 5.62,5.81C9.13,2.34 14.76,2.34 18.27,5.81L21,3V10.12M12.5,8V12.25L16,14.33L15.28,15.54L11,13V8H12.5Z", fill, width, height);
}
public static Node closeCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z", fill, width, height);
}
public static Node checkCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z", fill, width, height);
}
public static Node infoCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z", fill, width, height);
}
public static Node helpCircle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z", fill, width, height);
}
public static Node alert(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z", fill, width, height);
}
public static Node plus(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z", fill, width, height);
}
public static Node importIcon(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,12L10,8V11H2V13H10V16M20,18V6C20,4.89 19.1,4 18,4H6A2,2 0 0,0 4,6V9H6V6H18V18H6V15H4V18A2,2 0 0,0 6,20H18A2,2 0 0,0 20,18Z", fill, width, height);
}
public static Node export(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M23,12L19,8V11H10V13H19V16M1,18V6C1,4.89 1.9,4 3,4H15A2,2 0 0,1 17,6V9H15V6H3V18H15V15H17V18A2,2 0 0,1 15,20H3A2,2 0 0,1 1,18Z", fill, width, height);
}
public static Node openInNew(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
public static Node triangle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M1,21H23L12,2", fill, width, height);
}
public static Node home(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z", fill, width, height);
}
public static Node viewList(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z", fill, width, height);
}
public static Node check(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z", fill, width, height);
}
public static Node arrowRight(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M4,11V13H16L10.5,18.5L11.92,19.92L19.84,12L11.92,4.08L10.5,5.5L16,11H4Z", fill, width, height);
}
public static Node wrench(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z", fill, width, height);
}
public static Node upload(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z", fill, width, height);
}
public static Node hanger(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12 4A3.5 3.5 0 0 0 8.5 7.5H10.5A1.5 1.5 0 0 1 12 6A1.5 1.5 0 0 1 13.5 7.5A1.5 1.5 0 0 1 12 9C11.45 9 11 9.45 11 10V11.75L2.4 18.2A1 1 0 0 0 3 20H21A1 1 0 0 0 21.6 18.2L13 11.75V10.85A3.5 3.5 0 0 0 15.5 7.5A3.5 3.5 0 0 0 12 4M12 13.5L18 18H6Z", fill, width, height);
}
}
@@ -0,0 +1,210 @@
// Copy from https://github.com/palexdev/MaterialFX/blob/c8038ce2090f5cddf923a19d79cc601db86a4d17/materialfx/src/main/java/io/github/palexdev/materialfx/utils/ScrollUtils.java
/*
* Copyright (C) 2022 Parisi Alessandro
* This file is part of MaterialFX (https://github.com/palexdev/MaterialFX).
*
* MaterialFX is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MaterialFX 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.animation.Animation;
import javafx.animation.Animation.Status;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.EventHandler;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.util.Duration;
import org.jackhuang.hmcl.util.Holder;
/**
* Utility class for ScrollPanes.
*/
final class ScrollUtils {
public enum ScrollDirection {
UP(-1), RIGHT(-1), DOWN(1), LEFT(1);
final int intDirection;
ScrollDirection(int intDirection) {
this.intDirection = intDirection;
}
public int intDirection() {
return intDirection;
}
}
private ScrollUtils() {
}
/**
* Determines if the given ScrollEvent comes from a trackpad.
* <p></p>
* Although this method works in most cases, it is not very accurate.
* Since in JavaFX there's no way to tell if a ScrollEvent comes from a trackpad or a mouse
* we use this trick: I noticed that a mouse scroll has a delta of 32 (don't know if it changes depending on the device or OS)
* and trackpad scrolls have a way smaller delta. So depending on the scroll direction we check if the delta is lesser than 10
* (trackpad event) or greater(mouse event).
*
* @see ScrollEvent#getDeltaX()
* @see ScrollEvent#getDeltaY()
*/
public static boolean isTrackPad(ScrollEvent event, ScrollDirection scrollDirection) {
switch (scrollDirection) {
case UP:
case DOWN:
return Math.abs(event.getDeltaY()) < 10;
case LEFT:
case RIGHT:
return Math.abs(event.getDeltaX()) < 10;
default:
return false;
}
}
/**
* Determines the scroll direction of the given ScrollEvent.
* <p></p>
* Although this method works fine, it is not very accurate.
* In JavaFX there's no concept of scroll direction, if you try to scroll with a trackpad
* you'll notice that you can scroll in both directions at the same time, both deltaX and deltaY won't be 0.
* <p></p>
* For this method to work we assume that this behavior is not possible.
* <p></p>
* If deltaY is 0 we return LEFT or RIGHT depending on deltaX (respectively if lesser or greater than 0).
* <p>
* Else we return DOWN or UP depending on deltaY (respectively if lesser or greater than 0).
*
* @see ScrollEvent#getDeltaX()
* @see ScrollEvent#getDeltaY()
*/
public static ScrollDirection determineScrollDirection(ScrollEvent event) {
double deltaX = event.getDeltaX();
double deltaY = event.getDeltaY();
if (deltaY == 0.0) {
return deltaX < 0 ? ScrollDirection.LEFT : ScrollDirection.RIGHT;
} else {
return deltaY < 0 ? ScrollDirection.DOWN : ScrollDirection.UP;
}
}
//================================================================================
// ScrollPanes
//================================================================================
/**
* Adds a smooth scrolling effect to the given scroll pane,
* calls {@link #addSmoothScrolling(ScrollPane, double)} with a
* default speed value of 1.
*/
public static void addSmoothScrolling(ScrollPane scrollPane) {
addSmoothScrolling(scrollPane, 1);
}
/**
* Adds a smooth scrolling effect to the given scroll pane with the given scroll speed.
* Calls {@link #addSmoothScrolling(ScrollPane, double, double)}
* with a default trackPadAdjustment of 7.
*/
public static void addSmoothScrolling(ScrollPane scrollPane, double speed) {
addSmoothScrolling(scrollPane, speed, 7);
}
/**
* Adds a smooth scrolling effect to the given scroll pane with the given
* scroll speed and the given trackPadAdjustment.
* <p></p>
* The trackPadAdjustment is a value used to slow down the scrolling if a trackpad is used.
* This is kind of a workaround and it's not perfect, but at least it's way better than before.
* The default value is 7, tested up to 10, further values can cause scrolling misbehavior.
*/
public static void addSmoothScrolling(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
smoothScroll(scrollPane, speed, trackPadAdjustment);
}
private static final double[] FRICTIONS = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001};
private static final Duration DURATION = Duration.millis(3);
private static void smoothScroll(ScrollPane scrollPane, double speed, double trackPadAdjustment) {
final double[] derivatives = new double[FRICTIONS.length];
Timeline timeline = new Timeline();
Holder<ScrollDirection> scrollDirectionHolder = new Holder<>();
final EventHandler<MouseEvent> mouseHandler = event -> timeline.stop();
final EventHandler<ScrollEvent> scrollHandler = event -> {
if (event.getEventType() == ScrollEvent.SCROLL) {
ScrollDirection scrollDirection = determineScrollDirection(event);
scrollDirectionHolder.value = scrollDirection;
double currentSpeed = isTrackPad(event, scrollDirection) ? speed / trackPadAdjustment : speed;
derivatives[0] += scrollDirection.intDirection * currentSpeed;
if (timeline.getStatus() == Status.STOPPED) {
timeline.play();
}
event.consume();
}
};
if (scrollPane.getContent().getParent() != null) {
scrollPane.getContent().getParent().addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
scrollPane.getContent().getParent().addEventHandler(ScrollEvent.ANY, scrollHandler);
}
scrollPane.getContent().parentProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
oldValue.removeEventHandler(ScrollEvent.ANY, scrollHandler);
}
if (newValue != null) {
newValue.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler);
newValue.addEventHandler(ScrollEvent.ANY, scrollHandler);
}
});
timeline.getKeyFrames().add(new KeyFrame(DURATION, event -> {
for (int i = 0; i < derivatives.length; i++) {
derivatives[i] *= FRICTIONS[i];
}
for (int i = 1; i < derivatives.length; i++) {
derivatives[i] += derivatives[i - 1];
}
double dy = derivatives[derivatives.length - 1];
double size;
switch (scrollDirectionHolder.value) {
case LEFT:
case RIGHT:
size = scrollPane.getContent().getLayoutBounds().getWidth();
scrollPane.setHvalue(Math.min(Math.max(scrollPane.getHvalue() + dy / size, 0), 1));
break;
case UP:
case DOWN:
size = scrollPane.getContent().getLayoutBounds().getHeight();
scrollPane.setVvalue(Math.min(Math.max(scrollPane.getVvalue() + dy / size, 0), 1));
break;
}
if (Math.abs(dy) < 0.001) {
timeline.stop();
}
}));
timeline.setCycleCount(Animation.INDEFINITE);
}
}
@@ -0,0 +1,33 @@
package org.jackhuang.hmcl.ui;
import javax.swing.*;
public final class SwingUtils {
static {
if (System.getProperty("swing.defaultlaf") == null) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Throwable ignored) {
}
}
}
private SwingUtils() {
}
public static void initLookAndFeel() {
// Make sure the static constructor is called
}
public static void showInfoDialog(Object message) {
JOptionPane.showMessageDialog(null, message, "Info", JOptionPane.INFORMATION_MESSAGE);
}
public static void showWarningDialog(Object message) {
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
}
public static void showErrorDialog(Object message) {
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
}
}
@@ -18,18 +18,18 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane;
import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import java.util.List;
@@ -40,37 +40,39 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
super(skinnable);
SpinnerPane spinnerPane = new SpinnerPane();
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.failedReasonProperty().bind(skinnable.failedReasonProperty());
spinnerPane.onFailedActionProperty().bind(skinnable.onFailedActionProperty());
spinnerPane.getStyleClass().add("large-spinner-pane");
BorderPane root = new BorderPane();
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
StackPane.setMargin(root, new Insets(10));
List<Node> toolbarButtons = initializeToolbar(skinnable);
if (!toolbarButtons.isEmpty()) {
HBox toolbar = new HBox();
toolbar.getStyleClass().add("jfx-tool-bar-second");
JFXDepthManager.setDepth(toolbar, 1);
toolbar.setAlignment(Pos.CENTER_LEFT);
toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(toolbarButtons);
root.setTop(toolbar);
root.getContent().add(toolbar);
}
{
ScrollPane scrollPane = new ScrollPane();
ComponentList.setVgrow(scrollPane, Priority.ALWAYS);
scrollPane.setFitToWidth(true);
VBox content = new VBox();
content.setSpacing(10);
content.setPadding(new Insets(10));
Bindings.bindContent(content.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane);
FXUtils.smoothScrolling(scrollPane);
root.setCenter(scrollPane);
root.getContent().add(scrollPane);
}
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
spinnerPane.setContent(root);
getChildren().setAll(spinnerPane);
@@ -83,23 +85,32 @@ public abstract class ToolbarListPageSkin<T extends ListPageBase<? extends Node>
return stackPane;
}
public static JFXButton createToolbarButton(String text, SVG.SVGIcon creator, Runnable onClick) {
public static JFXButton createToolbarButton(String text, SVG svg, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1)));
ret.setText(text);
ret.setOnMouseClicked(e -> onClick.run());
ret.setOnAction(e -> onClick.run());
return ret;
}
public static JFXButton createDecoratorButton(String tooltip, SVG.SVGIcon creator, Runnable onClick) {
public static JFXButton createToolbarButton2(String text, SVG svg, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-tool-bar-button");
ret.setGraphic(wrap(svg.createIcon(Theme.blackFill(), -1)));
ret.setText(text);
ret.setOnAction(e -> onClick.run());
return ret;
}
public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable onClick) {
JFXButton ret = new JFXButton();
ret.getStyleClass().add("jfx-decorator-button");
ret.textFillProperty().bind(Theme.foregroundFillBinding());
ret.setGraphic(wrap(creator.createIcon(Theme.foregroundFillBinding(), -1, -1)));
ret.setGraphic(wrap(svg.createIcon(Theme.foregroundFillBinding(), -1)));
FXUtils.installFastTooltip(ret, tooltip);
ret.setOnMouseClicked(e -> onClick.run());
ret.setOnAction(e -> onClick.run());
return ret;
}
@@ -0,0 +1,123 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.scene.Node;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public class TooltipInstaller {
public static final TooltipInstaller INSTALLER;
static {
TooltipInstaller installer = null;
try {
installer = new NewInstaller();
} catch (Throwable e) {
try {
installer = new OldInstaller();
} catch (Throwable e2) {
e2.addSuppressed(e);
LOG.warning("Failed to initialize TooltipInstaller", e2);
}
}
INSTALLER = installer != null ? installer : new TooltipInstaller();
}
TooltipInstaller() {
}
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
Tooltip.install(node, tooltip);
}
// For Java 8
private static final class OldInstaller extends TooltipInstaller {
private static final Constructor<?> createTooltipBehavior;
private static final Method installTooltipBehavior;
static {
try {
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
createTooltipBehavior.setAccessible(true);
installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
installTooltipBehavior.setAccessible(true);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false);
installTooltipBehavior.invoke(behavior, node, tooltip);
} catch (ReflectiveOperationException e) {
LOG.warning("Failed to set tooltip show delay", e);
Tooltip.install(node, tooltip);
}
}
}
// For Java 9+
private static final class NewInstaller extends TooltipInstaller {
private static final MethodHandle setTooltipShowDelay;
private static final MethodHandle setTooltipShowDuration;
private static final MethodHandle setTooltipHideDelay;
static {
try {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodType methodType = MethodType.methodType(void.class, Duration.class);
setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType);
setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType);
setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
@Override
public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
try {
setTooltipShowDelay.invokeExact(tooltip, showDelay);
setTooltipShowDuration.invokeExact(tooltip, showDuration);
setTooltipHideDelay.invokeExact(tooltip, hideDelay);
} catch (Throwable e) {
LOG.warning("Failed to set tooltip show delay", e);
}
Tooltip.install(node, tooltip);
}
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -20,46 +20,91 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
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.io.IOException;
import java.net.URL;
import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class UpgradeDialog extends JFXDialogLayout {
public UpgradeDialog(Runnable updateRunnable) {
{
setHeading(new Label(i18n("update.changelog")));
}
public final class UpgradeDialog extends JFXDialogLayout {
public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) {
maxWidthProperty().bind(Controllers.getScene().widthProperty().multiply(0.7));
maxHeightProperty().bind(Controllers.getScene().heightProperty().multiply(0.7));
{
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
WebEngine engine = webView.getEngine();
engine.load(CHANGELOG_URL + config().getUpdateChannel().channelName);
engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> {
String url = engine.getLoadWorker().getMessage().trim();
if (!url.startsWith(CHANGELOG_URL)) {
engine.getLoadWorker().cancel();
FXUtils.openLink(url);
}
setHeading(new Label(i18n("update.changelog")));
setBody(new ProgressIndicator());
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
boolean isPreview = remoteVersion.isPreview();
Task.supplyAsync(Schedulers.io(), () -> {
Document document = Jsoup.parse(new URL(url), 30 * 1000);
String id = null;
Node node = null;
if (isPreview) {
id = "nowpreview";
node = document.selectFirst("#" + id);
}
if (node == null) {
id = "nowchange";
node = document.selectFirst("#" + id);
}
if (node == null || !"h1".equals(node.nodeName()))
throw new IOException("Cannot find current changelog in document");
HTMLRenderer renderer = new HTMLRenderer(uri -> {
LOG.info("Open link: " + uri);
FXUtils.openLink(uri.toString());
});
setBody(webView);
}
{
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnMouseClicked(e -> updateRunnable.run());
do {
if ("h1".equals(node.nodeName()) && !id.equals(node.attr("id"))) {
break;
}
renderer.appendNode(node);
node = node.nextSibling();
} while (node != null);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
renderer.mergeLineBreaks();
return renderer.render();
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
ScrollPane scrollPane = new ScrollPane(result);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
setBody(scrollPane);
} else {
LOG.warning("Failed to load update log, trying to open it in browser");
FXUtils.openLink(url);
setBody();
}
}).start();
setActions(updateButton, cancelButton);
}
JFXHyperlink openInBrowser = new JFXHyperlink(i18n("web.view_in_browser"));
openInBrowser.setExternalLink(url);
JFXButton updateButton = new JFXButton(i18n("update.accept"));
updateButton.getStyleClass().add("dialog-accept");
updateButton.setOnAction(e -> updateRunnable.run());
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);
}
}
Loaded 100 of 935 files, more files were not shown because too many files have changed in this diff. Show more