Compare commits

..
Author SHA1 Message Date
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
342 changed files with 18064 additions and 9278 deletions

No files matched your search

+1117
View File
File diff suppressed because it is too large. Load diff
+28 -27
View File
@@ -1,68 +1,69 @@
name: Bug 反馈
description: 反馈一个 Bug。为了提高反馈效率,请优先在开黑啦或者或 Discord 频道中反馈。
name: Bug Feedback
description:
反馈一个 Bug。为了提高反馈效率,请优先在 KOOK 或者或 Discord 频道中反馈。若确认是 HMCL 问题,请发布 issue。
body:
- type: markdown
attributes:
value: |
提交前请确保你的启动器版本是**最新的开发版**,可以在启动器设置中更换更新通道至开发版获取更新。
我们发现,对于不经常检查邮箱的人来说,GitHub Issues 的反馈效率太低,时隔几个小时甚至一天的情况很多。
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 [开黑啦频道](https://kaihei.co/Kx7n3t)
为了改善反馈效率,并将积极为 HMCL 提供问题反馈和建议的人聚集起来,我们希望在这里反馈的人可以在 [KOOK 频道](https://kook.top/Kx7n3t)
或 [Discord 频道](https://discord.gg/jVvC7HfM6U)中反馈信息,**而不在 GitHub Issues 中反馈问题**。
如果你希望在 QQ 群中反馈问题,请[赞助 HMCL](https://afdian.net/@huanghongxun) 后申请加入群聊,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
如果你希望在 QQ 群中反馈问题,请[赞助 HMCL](https://hmcl.huangyuhui.net/api/redirect/sponsor) 后申请加入群聊,也请将下面的信息表填好直接发在群中,加快我们的沟通速度。
- type: input
id: hmcl-version
attributes:
label: 启动器版本
description: 请输入您所使用的的 HMCL 启动器版本。
placeholder: 例如:3.3.188
label: 启动器版本 Launcher Version
description: 请输入您所使用的的 HMCL 启动器版本。 Please enter the HMCL launcher version you are using.
placeholder: e.g. 3.5.3.211
validations:
required: true
- type: input
id: platform
attributes:
label: 平台
description: 请输入您遇到 BUG 的平台。
placeholder: 例如:Windows 11
label: 平台 Platform
description: 请输入您遇到 BUG 的平台。 Please enter the platform on which you encountered the bug.
placeholder: e.g. Windows 11
validations:
required: true
- type: input
id: java-version
attributes:
label: Java
description: 请输入您所使用的 Java 的名称与版本。
placeholder: 例如:LibericaJDK Full JDK 17.0.1
description: 请输入您所使用的 Java 的名称与版本。 Please enter the name and version of Java you are using.
placeholder: e.g. LibericaJDK Full JDK 17.0.1
- type: input
id: game-version
attributes:
label: 游戏版本
description: 如果您是在启动通过 HMCL 启动器自动下载的游戏时遇到了问题,请输入您所启动的游戏版本。
placeholder: 例如:1.17.1
label: 游戏版本 Game Version
description: 如果您是在启动通过 HMCL 启动器自动下载的游戏时遇到了问题,请输入您所启动的游戏版本。If you are having trouble launching a game that was automatically downloaded through the HMCL launcher, please enter the version of the game you are launching.
placeholder: e.g. 1.19
- type: input
id: game-modify
attributes:
label: 对游戏所做的修改
description: 如果您为游戏安装了 OptiFine、Forge,或者是其他的 mod,请将您所做的修改填入下方。
placeholder: 例如:OptiFine HD_U_G9, Rift
label: 对游戏所做的修改 Modifications Made to the Game
description: 如果您为游戏安装了 OptiFine、Forge,或者是其他的 mod,请将您所做的修改填入下方。 If you have OptiFine, Forge, or other mods installed for the game, please enter your modifications below.
placeholder: e.g. OptiFine HD_U_G9, Rift
- type: textarea
id: game-crash-report
attributes:
label: 游戏崩溃报告
description: 如果有游戏崩溃报告的话,请填入(或将文件拖入)下方。
label: 游戏崩溃报告 Game Crash Report
description: 如果有游戏崩溃报告的话,请填入(或将文件拖入)下方。 If there is a game crash report, please fill in (or drag the file into) below.
- type: textarea
id: hmcl-crash-report
attributes:
label: 启动器崩溃报告
description: 如果有启动器崩溃报告的话,请填入(或将文件拖入)下方。
label: 启动器崩溃报告 Launcher Crash Report
description: 如果有启动器崩溃报告的话,请填入(或将文件拖入)下方。 If there is a launcher crash report, please fill it in (or drag the file in) below.
- type: textarea
id: hmcl-logs
attributes:
label: 启动器日志文件
description: 遇到问题后,请在启动器的【设置】>【通用】>【调试】一栏中点击“导出启动器日志”,并将导出的日志拖到下方的输入栏中。
label: 启动器日志文件 Launcher Log File
description: 遇到问题后,请在启动器的【设置】>【通用】>【调试】一栏中点击“导出启动器日志”,并将导出的日志拖到下方的输入栏中。 After encountering a problem, please click "Export Launcher Log" in the [Settings] > [General] > [Debug] column of the launcher, and drag the exported log to the input field below.
- type: textarea
id: bug-report
attributes:
label: 问题描述
description: 请尽可能地详细描述问题,特别是您是怎么遇到这个 BUG 的。
label: 问题描述 Problem Description
description: 请尽可能地详细描述问题,特别是您是怎么遇到这个 BUG 的。 Please describe the problem in as much detail as possible, especially how you encountered this bug.
+4 -4
View File
@@ -1,8 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: 开黑啦频道
url: https://kaihei.co/Kx7n3t
about: Hello Minecraft! Launcher 的官方开黑啦频道。
- name: KOOK 频道
url: https://kook.top/Kx7n3t
about: Hello Minecraft! Launcher 的官方 KOOK 频道。
- name: Discord 频道
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 频道。
@@ -11,4 +11,4 @@ contact_links:
about: 前往爱发电赞助 HMCL。赞助后可以申请加入官方 QQ 群反馈问题。
- name: 其他反馈
url: https://github.com/huanghongxun/HMCL/issues/new
about: 通过 Issue 反馈其他问题。
about: 通过 Issue 反馈其他问题。
+3 -5
View File
@@ -8,15 +8,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout submodules
run: git submodule update --init --recursive
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
java-package: jdk+fx
java-package: 'jdk+fx'
- name: Check style main
run: ./gradlew checkstyleMain
- name: Check style test
+3 -5
View File
@@ -8,14 +8,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout submodules
run: git submodule update --init --recursive
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
java-package: jdk+fx
java-package: 'jdk+fx'
- name: Check style test
run: ./gradlew checkTranslations
+9 -10
View File
@@ -10,7 +10,7 @@ jobs:
if: ${{ github.repository_owner == 'huanghongxun' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Fetch tags
run: git fetch --all --tags
- name: Install tools
@@ -20,18 +20,13 @@ jobs:
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 "^HMCL-[0-9]\.[0-9]\.[0-9]\{3\}\.exe\$"` ]; then exit 1; fi
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_BASE_VERSION=${HMCL_EXE_FILE_NAME:5:3}
export HMCL_BUILD_NUMBER=`cat ci.json | jq -M -r '.id'`
export HMCL_VERSION="$HMCL_BASE_VERSION.$HMCL_BUILD_NUMBER"
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_BUILD_NUMBER}" != 3 ]; then exit 1; fi
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
echo "HMCL_BASE_VERSION=$HMCL_BASE_VERSION" >> $GITHUB_ENV
echo "HMCL_BUILD_NUMBER=$HMCL_BUILD_NUMBER" >> $GITHUB_ENV
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
@@ -44,14 +39,16 @@ jobs:
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha1"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh"
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.sh.sha1"
env:
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/${{ env.HMCL_BUILD_NUMBER }}/artifact/HMCL/build/libs
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/artifact/HMCL/build/libs
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/dev.html" >> RELEASE_NOTE
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
- name: Create release
if: ${{ env.continue == 'true' }}
@@ -63,6 +60,8 @@ jobs:
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
HMCL-${{ env.HMCL_VERSION }}.sh
HMCL-${{ env.HMCL_VERSION }}.sh.sha1
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 == 'huanghongxun' }}
runs-on: ubuntu-latest
steps:
- name: Mirror Github to Gitee
uses: Yikun/hub-mirror-action@v1.1
with:
src: github/huanghongxun
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
+8 -6
View File
@@ -8,21 +8,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout submodules
run: git submodule update --init --recursive
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v2
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '11'
java-package: jdk+fx
java-package: 'jdk+fx'
- name: Build with Gradle
run: ./gradlew build
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@v2
uses: actions/upload-artifact@v3
with:
name: HMCL-${{ env.SHORT_SHA }}
path: HMCL/build/libs
+2 -3
View File
@@ -12,13 +12,14 @@ hs_err_pid*
.mine*
/externalgames
NVIDIA
minecraft-exported-crash-info*
# gradle build
/build/
/HMCL/build/
/HMCLCore/build/
/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/log4j-patch/build/
# idea
.idea
@@ -26,14 +27,12 @@ NVIDIA
/HMCL/out/
/HMCLCore/out/
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
/minecraft/libraries/log4j-patch/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
/minecraft/libraries/log4j-patch/bin/
.classpath
.project
.settings
-3
View File
@@ -1,3 +0,0 @@
[submodule "JSTUN"]
path = JSTUN
url = https://github.com/huanghongxun/JSTUN
-235
View File
@@ -1,235 +0,0 @@
buildscript {
repositories {
gradlePluginPortal()
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'org.tukaani:xz:1.8'
classpath 'org.glavo:pack200:0.3.0'
}
}
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.zip.GZIPOutputStream
import java.util.zip.ZipFile
import java.nio.file.Files
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZOutputStream
import org.glavo.pack200.Pack200
def dev = null
def shortcommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7) ?: null
if (shortcommit != null && !shortcommit.isEmpty()) dev = "dev-" + shortcommit
def buildnumber = System.getenv("BUILD_NUMBER") ?: dev ?: "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.4"
def microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
def microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
def versionType = System.getenv("VERSION_TYPE") ?: "nightly"
version = versionroot + '.' + buildnumber
mainClassName = 'org.jackhuang.hmcl.Main'
dependencies {
implementation project(":HMCLCore")
implementation 'libs:JFoenix'
implementation group: 'de.javawi.jstun', name: 'jstun', version: '0.7.4'
}
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 {
if(JavaVersion.current() < JavaVersion.VERSION_11) {
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
minimize {
exclude(dependency('com.google.code.gson:.*:.*'))
exclude(dependency('com.github.steveice10:.*:.*'))
exclude(dependency('libs:JFoenix:.*'))
}
manifest {
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
'Main-Class': mainClassName,
'Multi-Release': 'true',
'Implementation-Version': project.version,
'Microsoft-Auth-Id': microsoftAuthId,
'Microsoft-Auth-Secret': microsoftAuthSecret,
'Build-Channel': versionType,
'Class-Path': 'pack200.jar',
'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()
javaexec {
classpath = sourceSets.main.compileClasspath
mainClass = "com.sun.javafx.css.parser.Css2Bin"
args = [cssFile, bssFile]
}
}
}
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
+283
View File
@@ -0,0 +1,283 @@
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.jar.JarFile
import java.util.jar.JarOutputStream
import java.util.zip.ZipFile
import java.util.zip.GZIPOutputStream
import java.io.ByteArrayOutputStream
import java.io.ByteArrayInputStream
import java.net.URI
import org.glavo.pack200.Pack200
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZOutputStream
buildscript {
repositories {
gradlePluginPortal()
maven(url = "https://jitpack.io")
}
dependencies {
classpath("org.tukaani:xz:1.8")
classpath("org.glavo:pack200:0.3.0")
}
}
plugins {
id("com.github.johnrengelman.shadow") version "7.1.2"
}
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
if (number != null) {
(number - offset).toString()
} else {
val shortCommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7)
val prefix = if (System.getenv("GITHUB_REPOSITORY_OWNER") == "huanghongxun") "dev" else "unofficial"
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
}
}
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.5"
val versionType = System.getenv("VERSION_TYPE") ?: "nightly"
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
version = "$versionRoot.$buildNumber"
dependencies {
implementation(project(":HMCLCore"))
implementation("libs:JFoenix")
}
fun digest(algorithm: String, bytes: ByteArray) = MessageDigest.getInstance(algorithm).digest(bytes)
fun createChecksum(file: File) {
val algorithms = linkedMapOf(
"MD5" to "md5",
"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") ?: 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 packer = Pack200.newPacker().apply {
properties()["pack.effort"] = "9"
}
val 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.
fun repack(file: File) {
val packed = ByteArrayOutputStream()
JarFile(file).use { packer.pack(it, packed) }
JarOutputStream(file.outputStream()).use { unpacker.unpack(ByteArrayInputStream(packed.toByteArray()), it) }
}
val java11 = sourceSets.create("java11") {
java {
srcDir("src/main/java11")
}
}
tasks.getByName<JavaCompile>(java11.compileJavaTaskName) {
if (JavaVersion.current() < JavaVersion.VERSION_11) {
javaCompiler.set(javaToolchains.compilerFor {
languageVersion.set(JavaLanguageVersion.of(11))
})
}
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.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("shadowJar") {
archiveClassifier.set(null as String?)
minimize {
exclude(dependency("com.google.code.gson:.*:.*"))
exclude(dependency("com.github.steveice10:.*:.*"))
exclude(dependency("libs:JFoenix:.*"))
}
manifest {
attributes(
"Created-By" to "Copyright(c) 2013-2021 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,
"Class-Path" to "pack200.jar",
"Add-Opens" to listOf(
"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"
).joinToString(" "),
"Add-Exports" to listOf(
"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"
).joinToString(" ")
)
}
doLast {
repack(jarPath) // see repack()
attachSignature(jarPath)
createChecksum(jarPath)
}
}
fun createExecutable(suffix: String, header: String) {
val output = File(jarPath.parentFile, jarPath.nameWithoutExtension + '.' + suffix)
output.outputStream().use {
it.write(File(project.projectDir, header).readBytes())
it.write(jarPath.readBytes())
}
createChecksum(output)
}
tasks.processResources {
fun convertToBSS(resource: String) {
doFirst {
val cssFile = File(projectDir, "src/main/resources/$resource")
val bssFile = File(projectDir, "build/compiled-resources/${resource.substring(0, resource.length - 4)}.bss")
bssFile.parentFile.mkdirs()
javaexec {
classpath = sourceSets["main"].compileClasspath
mainClass.set("com.sun.javafx.css.parser.Css2Bin")
args(cssFile, bssFile)
}
}
}
from("build/compiled-resources")
convertToBSS("assets/css/root.css")
convertToBSS("assets/css/blue.css")
into("META-INF/versions/11") {
from(sourceSets["java11"].output)
}
dependsOn(tasks["java11Classes"])
into("assets") {
from(project.buildDir.resolve("openjfx-dependencies.json"))
}
dependsOn(rootProject.tasks["generateOpenJFXDependencies"])
}
val packFile = File(jarPath.parentFile, jarPath.nameWithoutExtension + ".pack")
val makePack = tasks.create("makePack") {
dependsOn(tasks.jar)
doLast {
packFile.outputStream().use { out ->
JarFile(jarPath).use { jarFile -> packer.pack(jarFile, out) }
}
createChecksum(packFile)
}
}
val makePackXz = tasks.create("makePackXz") {
dependsOn(makePack)
val packXz = File(packFile.parentFile, packFile.name + ".xz")
doLast {
// Our CI server does not have enough memory space to compress file at highest level.
XZOutputStream(packXz.outputStream(), LZMA2Options(5))
.use { it.write(packFile.readBytes()) }
createChecksum(packXz)
}
}
val makePackGz = tasks.create("makePackGz") {
dependsOn(makePack)
val packGz = File(packFile.parentFile, packFile.name + ".gz")
doLast {
GZIPOutputStream(packGz.outputStream()).use { it.write(packFile.readBytes()) }
createChecksum(packGz)
}
}
val makeExecutables = tasks.create("makeExecutables") {
dependsOn(makePack)
doLast {
createExecutable("exe", "src/main/resources/assets/HMCLauncher.exe")
createExecutable("sh", "src/main/resources/assets/HMCLauncher.sh")
}
}
tasks.build {
dependsOn(makePackXz, makePackGz, makeExecutables)
}
tasks.create<JavaExec>("run") {
dependsOn(tasks.jar)
group = "application"
classpath = files(jarPath)
workingDir = rootProject.rootDir
}
+21 -6
View File
@@ -19,6 +19,7 @@
import json
import codecs
import re
import sys
from urllib.parse import urlparse, parse_qs
S = ';'
@@ -26,18 +27,21 @@ S = ';'
MOD_SEPARATOR = ','
CURSEFORGE_PATTERN1 = re.compile(
r'^/minecraft/(mc-mods|customization|mc-addons|customization/configuration)/(?P<modid>[\w-]+)(/(.*?))?$')
r'^/minecraft/(mc-mods|modpacks|customization|mc-addons|customization/configuration)/+(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN2 = re.compile(
r'^/projects/(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN3 = re.compile(
r'^/mc-mods/minecraft/(?P<modid>[\w-]+)(/(.*?))?$')
CURSEFORGE_PATTERN4 = re.compile(
r'^/legacy/mc-mods/minecraft/(\d+)-(?P<modid>[\w-]+)'
)
def parseCurseforge(url):
res = urlparse(url)
if res.scheme not in ['http', 'https']:
return ''
for pattern in [CURSEFORGE_PATTERN1, CURSEFORGE_PATTERN2, CURSEFORGE_PATTERN3]:
for pattern in [CURSEFORGE_PATTERN1, CURSEFORGE_PATTERN2, CURSEFORGE_PATTERN3, CURSEFORGE_PATTERN4]:
match = pattern.match(res.path)
if match:
return match.group('modid')
@@ -45,7 +49,7 @@ def parseCurseforge(url):
MCMOD_PATTERN = re.compile(
r'^http(s)?://www\.mcmod\.cn/class/(?P<modid>\d+)\.html$')
r'^http(s)?://www\.mcmod\.cn/(class|modpack)/(?P<modid>\d+)\.html$')
def parseMcmod(url):
@@ -55,7 +59,7 @@ def parseMcmod(url):
return ''
MCBBS_HTML_PATTERN = re.compile(r'/thread-(?P<modid>\d+)-(\d+)-(\d+).html')
MCBBS_HTML_PATTERN = re.compile(r'/+thread-(?P<modid>\d+)-(\d+)-(\d+).html')
MCBBS_PHP_PATTERN = re.compile(r'')
@@ -78,8 +82,16 @@ def parseMcbbs(url):
return ''
skip = [
'Minecraft',
'The Building Game'
]
if __name__ == '__main__':
with codecs.open('data.json', mode='r', encoding='utf-8-sig') as jsonfile, codecs.open('data.csv', mode='w', encoding='utf-8') as outfile:
json_name = sys.argv[1] or 'data.json'
with codecs.open(json_name, mode='r', encoding='utf-8-sig') as jsonfile, codecs.open('data.csv', mode='w', encoding='utf-8') as outfile:
data = json.load(jsonfile)
for mod in data:
@@ -87,6 +99,9 @@ if __name__ == '__main__':
sub_name = mod['name']['sub']
abbr = mod['name']['abbr']
if sub_name in skip:
continue
if S in chinese_name:
print('Error! ' + chinese_name)
exit(1)
@@ -123,7 +138,7 @@ if __name__ == '__main__':
exit(1)
mod_id = []
if 'list' in mod['modid']:
if 'modid' in mod and 'list' in mod['modid']:
for id in mod['modid']['list']:
if MOD_SEPARATOR in id:
print('Error mod id!' + id)
@@ -3,8 +3,8 @@ package moe.mickey.minecraft.skin.fx;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
public class SkinAnimation {
@@ -12,9 +12,8 @@ public class SkinAnimation {
protected int weight, left;
protected List<SkinTransition> transitions;
@Deprecated
public SkinAnimation() {
this.transitions = new LinkedList<>();
this.transitions = new ArrayList<>();
}
public SkinAnimation(int weight, SkinTransition... transitions) {
@@ -2,14 +2,15 @@ package moe.mickey.minecraft.skin.fx;
import javafx.animation.AnimationTimer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
public class SkinAnimationPlayer {
protected final Random random = new Random();
protected LinkedList<SkinAnimation> animations = new LinkedList<>();
protected List<SkinAnimation> animations = new ArrayList<>();
protected SkinAnimation playing;
protected boolean running;
protected int weightedSum = 0;
@@ -28,7 +29,7 @@ public class SkinAnimationPlayer {
}
playing = tmp;
if (playing == null && animations.size() > 0)
playing = animations.getLast();
playing = animations.get(animations.size() - 1);
if (playing != null) {
playing.playFromStart();
lastPlayTime = now;
@@ -4,6 +4,7 @@ import javafx.scene.*;
import javafx.scene.image.Image;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Shape3D;
@@ -11,10 +12,12 @@ import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import org.jetbrains.annotations.Nullable;
public class SkinCanvas extends Group {
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/alex.png"));
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/steve.png"));
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/skin/alex.png"));
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/skin/steve.png"));
public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true);
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
@@ -22,7 +25,7 @@ public class SkinCanvas extends Group {
public static final SkinCube STEVEN_LARM = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, false);
public static final SkinCube STEVEN_RARM = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, false);
protected Image srcSkin, skin;
protected Image srcSkin, skin, srcCape, cape;
protected boolean isSlim;
protected double preW, preH;
@@ -45,6 +48,8 @@ public class SkinCanvas extends Group {
public final SkinCube llegInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 16F / 64F, 48F / 64F, 0F, false);
public final SkinCube rlegInside = new SkinCube(4, 12, 4, 16F / 64F, 16F / 64F, 0F, 16F / 64F, 0F, false);
public final SkinCube capeCube = new SkinCube(10, 16, 1, 22F / 64F, 17F / 32F, 0F, 0F, 0F, false);
public final SkinGroup head = new SkinGroup(
new Rotate(0, 0, headInside.getHeight() / 2, 0, Rotate.X_AXIS),
new Rotate(0, Rotate.Y_AXIS),
@@ -82,6 +87,13 @@ public class SkinCanvas extends Group {
rlegOuter, rlegInside
);
public final SkinGroup capeGroup = new SkinGroup(
new Rotate(0, 0, -capeCube.getHeight() / 2, 0, Rotate.X_AXIS),
new Rotate(0, Rotate.Y_AXIS),
new Rotate(0, Rotate.Z_AXIS),
capeCube
);
protected PerspectiveCamera camera = new PerspectiveCamera(true);
protected Rotate xRotate = new Rotate(0, Rotate.X_AXIS);
@@ -90,10 +102,10 @@ public class SkinCanvas extends Group {
protected Translate translate = new Translate(0, 0, -80);
protected Scale scale = new Scale(1, 1);
protected SkinAnimationPlayer animationplayer = new SkinAnimationPlayer();
protected SkinAnimationPlayer animationPlayer = new SkinAnimationPlayer();
public SkinAnimationPlayer getAnimationplayer() {
return animationplayer;
public SkinAnimationPlayer getAnimationPlayer() {
return animationPlayer;
}
public Image getSrcSkin() {
@@ -104,20 +116,21 @@ public class SkinCanvas extends Group {
return skin;
}
public void updateSkin(Image skin, boolean isSlim) {
public void updateSkin(Image skin, boolean isSlim, final @Nullable Image cape) {
if (SkinHelper.isNoRequest(skin) && SkinHelper.isSkin(skin)) {
this.srcSkin = skin;
this.skin = SkinHelper.x32Tox64(skin);
this.srcCape = cape;
this.cape = cape == null ? null : cape.getWidth() < 256 ? SkinHelper.enlarge(cape, 4, 8) : cape;
int multiple = Math.max((int) (1024 / skin.getWidth()), 1);
if (multiple > 1)
this.skin = SkinHelper.enlarge(this.skin, multiple);
if (this.isSlim != isSlim)
updateSkinModel(isSlim);
this.skin = SkinHelper.enlarge(this.skin, multiple, multiple);
updateSkinModel(isSlim, cape != null);
bindMaterial(root);
}
}
protected void updateSkinModel(boolean isSlim) {
protected void updateSkinModel(boolean isSlim, boolean hasCape) {
this.isSlim = isSlim;
FunctionHelper.alwaysB(SkinMultipleCubes::setWidth, isSlim ? 3 : 4, larmOuter, rarmOuter);
FunctionHelper.alwaysB(SkinCube::setWidth, isSlim ? 3D : 4D, larmInside, rarmInside);
@@ -134,6 +147,8 @@ public class SkinCanvas extends Group {
larm.getZRotate().setPivotX(-larmInside.getWidth() / 2);
rarm.getZRotate().setPivotX(+rarmInside.getWidth() / 2);
capeGroup.setVisible(hasCape);
}
public SkinCanvas(double preW, double preH) {
@@ -149,17 +164,17 @@ public class SkinCanvas extends Group {
init();
}
protected Material createMaterial() {
protected Material createMaterial(final Image image) {
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap(skin);
material.setDiffuseMap(image);
return material;
}
protected void bindMaterial(Group group) {
Material material = createMaterial();
Material material = createMaterial(skin);
for (Node node : group.getChildren())
if (node instanceof Shape3D)
((Shape3D) node).setMaterial(material);
((Shape3D) node).setMaterial(node == capeCube ? createMaterial(cape) : material);
else if (node instanceof SkinMultipleCubes)
((SkinMultipleCubes) node).updateSkin(skin);
else if (node instanceof Group)
@@ -178,6 +193,11 @@ public class SkinCanvas extends Group {
lleg.setTranslateY(+(bodyInside.getHeight() + llegInside.getHeight()) / 2);
rleg.setTranslateY(+(bodyInside.getHeight() + rlegInside.getHeight()) / 2);
capeGroup.setTranslateY(+(capeCube.getHeight() - bodyOuter.getHeight()) / 2);
capeGroup.setTranslateZ(-(bodyInside.getDepth() + bodyOuter.getDepth()) / 2);
capeGroup.getTransforms().addAll(new Rotate(180, Rotate.Y_AXIS), new Rotate(10, Rotate.X_AXIS));
root.getTransforms().addAll(xRotate);
root.getChildren().addAll(
@@ -186,15 +206,20 @@ public class SkinCanvas extends Group {
larm,
rarm,
lleg,
rleg
rleg,
capeGroup
);
updateSkin(skin, false);
updateSkin(skin, false, null);
return root;
}
protected SubScene createSubScene() {
Group group = new Group();
AmbientLight light = new AmbientLight(Color.WHITE);
group.getChildren().add(light);
group.getChildren().add(createPlayerModel());
group.getTransforms().add(zRotate);
@@ -1,9 +1,9 @@
package moe.mickey.minecraft.skin.fx;
import javafx.scene.image.Image;
import javafx.scene.shape.Mesh;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import org.apache.commons.lang3.ArrayUtils;
public class SkinCube extends MeshView {
@@ -20,61 +20,60 @@ public class SkinCube extends MeshView {
width /= 2F;
height /= 2F;
depth /= 2F;
return new float[]{
-width, -height, depth, // P0
width, -height, depth, // P1
-width, height, depth, // P2
width, height, depth, // P3
-width, -height, -depth, // P4
width, -height, -depth, // P5
-width, height, -depth, // P6
width, height, -depth // P7
-width, -height, +depth, // P0
+width, -height, +depth, // P1
-width, +height, +depth, // P2
+width, +height, +depth, // P3
-width, -height, -depth, // P4
+width, -height, -depth, // P5
-width, +height, -depth, // P6
+width, +height, -depth // P7
};
}
public static float[] createTexCoords(float width, float height, float depth, float scaleX, float scaleY,
float startX, float startY, boolean isSlim) {
float startX, float startY, boolean isSlim) {
float x = (width + depth) * 2, y = height + depth, half_width = width / x * scaleX, half_depth = depth / x * scaleX,
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = isSlim ? half_depth : half_width,
bottom_x = startX, middle_y = depth / y * scaleY + top_y, bottom_y = scaleY + top_y;
return new float[]{
top_x, top_y, // T0 ---
top_x + half_width, top_y, // T1 |
top_x + half_width * 2, top_y, // T2 ---
bottom_x, middle_y, // T3 ---
bottom_x + half_depth, middle_y, // T4 |
top_x + half_width, top_y, // T1 |
top_x + half_width * 2, top_y, // T2 ---
bottom_x, middle_y, // T3 ---
bottom_x + half_depth, middle_y, // T4 |
bottom_x + half_depth + half_width, middle_y, // T5 |
bottom_x + scaleX - arm4, middle_y, // T6 |
bottom_x + scaleX, middle_y, // T7 ---
bottom_x, bottom_y, // T8 ---
bottom_x + half_depth, bottom_y, // T9 |
bottom_x + scaleX - arm4, middle_y, // T6 |
bottom_x + scaleX, middle_y, // T7 ---
bottom_x, bottom_y, // T8 ---
bottom_x + half_depth, bottom_y, // T9 |
bottom_x + half_depth + half_width, bottom_y, // T10 |
bottom_x + scaleX - arm4, bottom_y, // T11 |
bottom_x + scaleX, bottom_y // T12 ---
bottom_x + scaleX - arm4, bottom_y, // T11 |
bottom_x + scaleX, bottom_y // T12 ---
};
}
public static int[] createFaces() {
int[] faces = new int[]{
int[] faces = {
// TOP
5, 0, 4, 1, 0, 5, //P5,T0, P4,T1, P0,T5
5, 0, 0, 5, 1, 4, //P5,T0, P0,T5, P1,T4
// RIGHT
0, 5, 4, 6, 6, 11, //P0,T4 ,P4,T3, P6,T8
0, 5, 6, 11, 2, 10, //P0,T4 ,P6,T8, P2,T9
0, 5, 4, 6, 6, 11, //P0,T4 ,P4,T3, P6,T8
0, 5, 6, 11, 2, 10, //P0,T4 ,P6,T8, P2,T9
// FRONT
1, 4, 0, 5, 2, 10, //P1,T5, P0,T4, P2,T9
1, 4, 2, 10, 3, 9, //P1,T5, P2,T9, P3,T10
1, 4, 0, 5, 2, 10, //P1,T5, P0,T4, P2,T9
1, 4, 2, 10, 3, 9, //P1,T5, P2,T9, P3,T10
// LEFT
5, 3, 1, 4, 3, 9, //P5,T6, P1,T5, P3,T10
5, 3, 3, 9, 7, 8, //P5,T6, P3,T10,P7,T11
// BACK
4, 6, 5, 7, 7, 12, //P4,T6, P5,T7, P7,T12
4, 6, 7, 12, 6, 11, //P4,T6, P7,T12,P6,T11
4, 6, 5, 7, 7, 12, //P4,T6, P5,T7, P7,T12
4, 6, 7, 12, 6, 11, //P4,T6, P7,T12,P6,T11
// BOTTOM
3, 5, 2, 6, 6, 2, //P3,T2, P2,T1, P6,T5
3, 5, 6, 2, 7, 1 //P3,T2, P6,T5, P7,T6
3, 5, 6, 2, 7, 1 //P3,T2, P6,T5, P7,T6
};
int[] copy = faces.clone();
@@ -91,7 +90,6 @@ public class SkinCube extends MeshView {
private double width, height, depth;
private boolean isSlim;
private Image skin;
private Mesh model;
public SkinCube(float width, float height, float depth, float scaleX, float scaleY, float startX, float startY, float enlarge, boolean isSlim) {
@@ -10,12 +10,12 @@ public final class SkinHelper {
private SkinHelper() {
}
public static class PixelCopyer {
public static class PixelCopier {
protected Image srcImage;
protected WritableImage newImage;
public PixelCopyer(Image srcImage, WritableImage newImage) {
public PixelCopier(Image srcImage, WritableImage newImage) {
this.srcImage = srcImage;
this.newImage = newImage;
}
@@ -70,53 +70,53 @@ public final class SkinHelper {
}
public static Image x32Tox64(Image srcSkin) {
if (srcSkin.getHeight() == 64)
if (srcSkin.getHeight() == srcSkin.getWidth())
return srcSkin;
WritableImage newSkin = new WritableImage((int) srcSkin.getWidth(), (int) srcSkin.getHeight() * 2);
PixelCopyer copyer = new PixelCopyer(srcSkin, newSkin);
PixelCopier copier = new PixelCopier(srcSkin, newSkin);
// HEAD & HAT
copyer.copy(0 / 64F, 0 / 32F, 0 / 64F, 0 / 64F, 64 / 64F, 16 / 32F);
copier.copy(0 / 64F, 0 / 32F, 0 / 64F, 0 / 64F, 64 / 64F, 16 / 32F);
// LEFT-LEG
x32Tox64(copyer, 0 / 64F, 16 / 32F, 16 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
x32Tox64(copier, 0 / 64F, 16 / 32F, 16 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
// RIGHT-LEG
copyer.copy(0 / 64F, 16 / 32F, 0 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
copier.copy(0 / 64F, 16 / 32F, 0 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
// BODY
copyer.copy(16 / 64F, 16 / 32F, 16 / 64F, 16 / 64F, 24 / 64F, 16 / 32F);
copier.copy(16 / 64F, 16 / 32F, 16 / 64F, 16 / 64F, 24 / 64F, 16 / 32F);
// LEFT-ARM
x32Tox64(copyer, 40 / 64F, 16 / 32F, 32 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
x32Tox64(copier, 40 / 64F, 16 / 32F, 32 / 64F, 48 / 64F, 4 / 64F, 12 / 32F, 4 / 64F);
// RIGHT-ARM
copyer.copy(40 / 64F, 16 / 32F, 40 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
copier.copy(40 / 64F, 16 / 32F, 40 / 64F, 16 / 64F, 16 / 64F, 16 / 32F);
return newSkin;
}
static void x32Tox64(PixelCopyer copyer, float srcX, float srcY, float toX, float toY, float width, float height, float depth) {
static void x32Tox64(PixelCopier copier, float srcX, float srcY, float toX, float toY, float width, float height, float depth) {
// TOP
copyer.copy(srcX + depth, srcY, toX + depth, toY, width, depth * 2, true, false);
copier.copy(srcX + depth, srcY, toX + depth, toY, width, depth * 2, true, false);
// BOTTOM
copyer.copy(srcX + depth + width, srcY, toX + depth + width, toY, width, depth * 2, true, false);
copier.copy(srcX + depth + width, srcY, toX + depth + width, toY, width, depth * 2, true, false);
// INS
copyer.copy(srcX, srcY + depth * 2, toX + width + depth, toY + depth, depth, height, true, false);
copier.copy(srcX, srcY + depth * 2, toX + width + depth, toY + depth, depth, height, true, false);
// OUTS
copyer.copy(srcX + width + depth, srcY + depth * 2, toX, toY + depth, depth, height, true, false);
copier.copy(srcX + width + depth, srcY + depth * 2, toX, toY + depth, depth, height, true, false);
// FRONT
copyer.copy(srcX + depth, srcY + depth * 2, toX + depth, toY + depth, width, height, true, false);
copier.copy(srcX + depth, srcY + depth * 2, toX + depth, toY + depth, width, height, true, false);
// BACK
copyer.copy(srcX + width + depth * 2, srcY + depth * 2, toX + width + depth * 2, toY + depth, width, height, true, false);
copier.copy(srcX + width + depth * 2, srcY + depth * 2, toX + width + depth * 2, toY + depth, width, height, true, false);
}
public static Image enlarge(Image srcSkin, int multiple) {
WritableImage newSkin = new WritableImage((int) srcSkin.getWidth() * multiple, (int) srcSkin.getHeight() * multiple);
public static Image enlarge(Image srcSkin, int multipleX, int multipleY) {
WritableImage newSkin = new WritableImage((int) srcSkin.getWidth() * multipleX, (int) srcSkin.getHeight() * multipleY);
PixelReader reader = srcSkin.getPixelReader();
PixelWriter writer = newSkin.getPixelWriter();
for (int x = 0, lenX = (int) srcSkin.getWidth(); x < lenX; x++)
for (int y = 0, lenY = (int) srcSkin.getHeight(); y < lenY; y++)
for (int mx = 0; mx < multiple; mx++)
for (int my = 0; my < multiple; my++) {
for (int mx = 0; mx < multipleX; mx++)
for (int my = 0; my < multipleY; my++) {
int argb = reader.getArgb(x, y);
writer.setArgb(x * multiple + mx, y * multiple + my, argb);
writer.setArgb(x * multipleX + mx, y * multipleY + my, argb);
}
return newSkin;
@@ -19,31 +19,35 @@ package org.jackhuang.hmcl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.control.Alert;
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.setting.SambaException;
import org.jackhuang.hmcl.task.Schedulers;
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.OperatingSystem;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.management.ManagementFactory;
import java.net.*;
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;
@@ -67,7 +71,55 @@ public final class Launcher extends Application {
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()));
try {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
String owner = Files.getOwner(ConfigHolder.configLocation()).getName();
String userName = System.getProperty("user.name");
if (!Files.isWritable(ConfigHolder.configLocation())
&& !userName.equals("root")
&& !userName.equals(owner)) {
ArrayList<String> files = new ArrayList<>();
{
files.add(ConfigHolder.configLocation().toString());
if (Files.exists(Metadata.HMCL_DIRECTORY))
files.add(Metadata.HMCL_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"));
ButtonType res = new Alert(Alert.AlertType.ERROR, i18n("fatal.config_loading_failure.unix", owner, command), copyAndExit, ButtonType.CLOSE)
.showAndWait()
.orElse(null);
if (res == copyAndExit) {
Clipboard.getSystemClipboard()
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
}
System.exit(1);
}
}
} catch (IOException ioe) {
LOG.log(Level.WARNING, "Failed to get file owner", ioe);
}
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize().toString()));
}
if (ConfigHolder.isOwnerChanged()) {
ButtonType res = new Alert(Alert.AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO)
.showAndWait()
.orElse(null);
if (res == ButtonType.NO)
return;
}
if (Metadata.HMCL_DIRECTORY.toAbsolutePath().toString().indexOf('=') >= 0) {
Main.showWarningAndContinue(i18n("fatal.illegal_char"));
}
// runLater to ensure ConfigHolder.init() finished initialization
@@ -77,8 +129,6 @@ public final class Launcher extends Application {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
initIcon();
UpdateChecker.init();
primaryStage.show();
@@ -94,12 +144,6 @@ public final class Launcher extends Application {
Controllers.onApplicationStop();
}
private void initIcon() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
AwtUtils.setAppleIcon(image);
}
public static void main(String[] args) {
if (UpdateHandler.processArguments(args)) {
return;
@@ -118,6 +162,7 @@ public final class Launcher extends Application {
LOG.info("Java Home: " + System.getProperty("java.home"));
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
LOG.info("HMCL Jar Path: " + JarUtils.thisJar().map(it -> it.toAbsolutePath().toString()).orElse("Not Found"));
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"));
@@ -154,34 +199,5 @@ public final class Launcher extends Application {
});
}
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 {
try {
File jarFile = new File(URLDecoder.decode(Launcher.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8"));
String ext = FileUtils.getExtension(jarFile);
if ("jar".equals(ext) || "exe".equals(ext))
result.add(jarFile);
} catch (UnsupportedEncodingException e) {
LOG.log(Level.WARNING, "Failed to decode jar path", e);
return null;
}
}
if (result.isEmpty())
return null;
else
return result;
}
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
}
+44 -18
View File
@@ -17,13 +17,19 @@
*/
package org.jackhuang.hmcl;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.ui.SwingUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -55,29 +61,25 @@ public final class Main {
// Fix title bar not displaying in GTK systems
System.setProperty("jdk.gtk.version", "2");
// Use System look and feel
initLookAndFeel();
checkDirectoryPath();
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9)
// This environment check will take ~300ms
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
initIcon();
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
checkJavaFX();
Launcher.main(args);
}
private static void initLookAndFeel() {
if (System.getProperty("swing.defaultlaf") == null) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Throwable ignored) {
}
}
private static void initIcon() {
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon@8x.png"));
AwtUtils.setAppleIcon(image);
}
private static void checkDirectoryPath() {
@@ -97,7 +99,12 @@ public final class Main {
showErrorAndExit(i18n("fatal.javafx.missing"));
} catch (SelfDependencyPatcher.IncompatibleVersionException e) {
LOG.log(Level.SEVERE, "unable to patch JVM", e);
showErrorAndExit(i18n("fatal.javafx.incompatible"));
if (Architecture.CURRENT_ARCH == Architecture.MIPS64EL
|| Architecture.CURRENT_ARCH == Architecture.LOONGARCH64
|| Architecture.CURRENT_ARCH == Architecture.LOONGARCH64_OW)
showErrorAndExit(i18n("fatal.javafx.incompatible.loongson"));
else
showErrorAndExit(i18n("fatal.javafx.incompatible"));
} catch (CancellationException e) {
LOG.log(Level.SEVERE, "User cancels downloading JavaFX", e);
System.exit(0);
@@ -110,7 +117,16 @@ 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);
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.ERROR, message).showAndWait();
System.exit(1);
}
} catch (Throwable ignored) {
}
SwingUtils.showErrorDialog(message);
System.exit(1);
}
@@ -120,7 +136,16 @@ 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() {
@@ -150,7 +175,8 @@ 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) {
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
KeyManagementException e) {
LOG.log(Level.SEVERE, "Failed to load lets encrypt certificate. Expect problems", e);
}
}
@@ -18,11 +18,9 @@
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Stores metadata about this application.
@@ -32,35 +30,22 @@ public final class Metadata {
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.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + 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 CONTACT_URL = "https://github.com/huanghongxun/HMCL/issues";
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 CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
public static final String PUBLISH_URL = "https://www.mcbbs.net/thread-142335-1-1.html";
public static final String EULA_URL = "https://hmcl.huangyuhui.net/eula";
public static final String BUILD_CHANNEL = JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Build-Channel")).orElse("nightly");
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
private static Path getHMCLDirectory() {
String home = System.getProperty("user.home", ".");
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
// to fulfill XDG standard.
String xdgCache = System.getenv("XDG_CACHE_HOME");
if (StringUtils.isNotBlank(xdgCache)) {
return Paths.get(xdgCache, "hmcl");
}
return Paths.get(home, ".cache", "hmcl");
}
return OperatingSystem.getWorkingDirectory("hmcl");
}
public static final Path HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
public static boolean isStable() {
return "stable".equals(BUILD_CHANNEL);
@@ -59,8 +59,13 @@ public final class HMCLGameLauncher extends DefaultLauncher {
private void generateOptionsTxt() {
File optionsFile = new File(repository.getRunDirectory(version.getId()), "options.txt");
File configFolder = new File(repository.getRunDirectory(version.getId()), "config");
if (optionsFile.exists())
return;
if (configFolder.isDirectory())
if (findFiles(configFolder, "options.txt"))
return;
try {
// TODO: Dirty implementation here
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA)
@@ -70,6 +75,20 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
}
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
public ManagedProcess launch() throws IOException, InterruptedException {
generateOptionsTxt();
@@ -20,7 +20,6 @@ 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;
@@ -28,8 +27,7 @@ import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
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.setting.VersionIconType;
@@ -328,7 +326,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir) {
public LaunchOptions getLaunchOptions(String version, JavaVersion javaVersion, File gameDir, List<String> javaAgents, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -339,7 +337,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setProfileName(Metadata.TITLE)
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory((int)(getAllocatedMemory(
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
vs.isAutoMemory()
@@ -357,8 +355,11 @@ public class HMCLGameRepository extends DefaultGameRepository {
.setNativesDirType(vs.getNativesDirType())
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setUseSoftwareRenderer(vs.isUseSoftwareRenderer())
.setUseNativeGLFW(vs.isUseNativeGLFW())
.setUseNativeOpenAL(vs.isUseNativeOpenAL());
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
.setJavaAgents(javaAgents);
if (config().hasProxy()) {
builder.setProxy(ProxyManager.getProxy());
if (config().hasProxyAuth()) {
@@ -372,16 +373,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();
}
@@ -452,6 +453,20 @@ public class HMCLGameRepository extends DefaultGameRepository {
}
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
return auto ? Math.max(minimum, (long) (available * 0.8)) : minimum;
if (auto) {
available -= 256 * 1024 * 1024; // Reserve 256MB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
final long threshold = 8L * 1024 * 1024 * 1024;
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
32736L * 1024 * 1024); // Limit the maximum suggested memory to ensure that compressed oops are available
return Math.max(minimum, suggested);
} else {
return minimum;
}
}
}
@@ -33,7 +33,8 @@ 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;
public final class HMCLModpackInstallTask extends Task<Void> {
@@ -42,8 +43,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();
@@ -69,13 +70,13 @@ public final class HMCLModpackInstallTask extends Task<Void> {
config = JsonUtils.GSON.fromJson(FileUtils.readText(json), new TypeToken<ModpackConfiguration<Modpack>>() {
}.getType());
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
@@ -104,6 +105,4 @@ public final class HMCLModpackInstallTask extends Task<Void> {
dependencies.add(libraryTask.thenComposeAsync(repository::saveAsync));
}
public static final String MODPACK_TYPE = "HMCL";
}
@@ -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;
}
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -18,8 +18,13 @@
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import org.apache.commons.compress.archivers.zip.ZipFile;
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;
@@ -30,26 +35,39 @@ import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
/**
* @author huangyuhui
*/
public final class HMCLModpackManager {
private HMCLModpackManager() {
public final class HMCLModpackProvider implements ModpackProvider {
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
@Override
public String getName() {
return "HMCL";
}
/**
* Read the manifest in a HMCL modpack.
*
* @param file a HMCL modpack file.
* @param encoding encoding of modpack zip file.
* @return the manifest of HMCL modpack.
* @throws IOException if the file is not a valid zip file.
* @throws JsonParseException if the manifest.json is missing or malformed.
*/
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
@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(ZipFile 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", 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()))
@@ -67,4 +85,5 @@ public final class HMCLModpackManager {
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
}
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.game;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
@@ -25,23 +26,13 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.MaintainTask;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.download.game.*;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.*;
@@ -59,12 +50,15 @@ import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.file.AccessDeniedException;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -73,7 +67,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class LauncherHelper {
private final Profile profile;
private final Account account;
private Account account;
private final String selectedVersion;
private File scriptFile;
private final VersionSetting setting;
@@ -90,8 +84,15 @@ public final class LauncherHelper {
this.launchingStepsPane.setTitle(i18n("version.launch"));
}
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {
});
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL);
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public void setTestMode() {
launcherVisibility = LauncherVisibility.KEEP;
@@ -120,67 +121,70 @@ public final class LauncherHelper {
private void launch0() {
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
Version version = MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion));
Optional<String> gameVersion = repository.getGameVersion(version);
AtomicReference<Version> version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion)));
Optional<String> gameVersion = repository.getGameVersion(version.get());
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
CountDownLatch launchingLatch = new CountDownLatch(1);
List<String> javaAgents = new ArrayList<>(0);
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
TaskExecutor executor = checkGameState(profile, setting, version)
TaskExecutor executor = checkGameState(profile, setting, version.get())
.thenComposeAsync(javaVersion -> {
javaVersionRef.set(Objects.requireNonNull(javaVersion));
return dependencyManager.checkPatchCompletionAsync(repository.getVersion(selectedVersion), integrityCheck);
})
.thenComposeAsync(Task.allOf(
Task.composeAsync(() -> {
if (setting.isNotCheckGame())
return null;
else
return dependencyManager.checkGameCompletionAsync(version, integrityCheck);
}), Task.composeAsync(() -> {
if (setting.isNotCheckGame()) {
return null;
} else {
version.set(NativePatcher.patchNative(version.get(), gameVersion.orElse(null), javaVersion, setting));
if (setting.isNotCheckGame())
return null;
return Task.allOf(
dependencyManager.checkGameCompletionAsync(version.get(), integrityCheck),
Task.composeAsync(() -> {
try {
ModpackConfiguration<?> configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion));
if (CurseInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new CurseCompletionTask(dependencyManager, selectedVersion);
else if (ServerModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new ServerModpackCompletionTask(dependencyManager, selectedVersion);
else if (McbbsModpackLocalInstallTask.MODPACK_TYPE.equals(configuration.getType()))
return new McbbsModpackCompletionTask(dependencyManager, selectedVersion);
else
return null;
ModpackProvider provider = ModpackHelper.getProviderByType(configuration.getType());
if (provider == null) return null;
else return provider.createCompletionTask(dependencyManager, selectedVersion);
} catch (IOException e) {
return null;
}
}
}))).withStage("launch.state.dependencies")
}),
Task.composeAsync(() -> {
if (setting.isUseSoftwareRenderer() && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Library lib = NativePatcher.getSoftwareRendererLoader(javaVersion);
if (lib == null)
return null;
File file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
if (file.getAbsolutePath().indexOf('=') >= 0) {
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
return null;
}
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(file.getAbsolutePath()));
} else {
javaAgents.add(file.getAbsolutePath());
return null;
}
} else {
return null;
}
})
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> {
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version)).orElse(null);
return gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null);
})
.thenComposeAsync(Task.supplyAsync(() -> {
try {
return account.logIn();
} catch (CredentialExpiredException e) {
LOG.info("Credential has expired: " + e);
return DialogController.logIn(account);
} catch (AuthenticationException e) {
LOG.warning("Authentication failed, try playing offline: " + e);
return account.playOffline().orElseThrow(() -> e);
}
}).withStage("launch.state.logging_in"))
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir());
LaunchOptions launchOptions = repository.getLaunchOptions(selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, scriptFile != null);
return new HMCLGameLauncher(
repository,
version,
version.get(),
authInfo,
launchOptions,
launcherVisibility == LauncherVisibility.CLOSE
? null // Unnecessary to start listening to game process output when close launcher immediately after game launched.
: new HMCLProcessListener(repository, version, authInfo, launchOptions, launchingLatch, gameVersion.isPresent())
: new HMCLProcessListener(repository, version.get(), authInfo, launchOptions, launchingLatch, gameVersion.isPresent())
);
}).thenComposeAsync(launcher -> { // launcher is prev task's result
if (scriptFile == null) {
@@ -197,10 +201,10 @@ public final class LauncherHelper {
if (launcherVisibility == LauncherVisibility.CLOSE)
Launcher.stopApplication();
else
launchingStepsPane.setCancel(it -> {
launchingStepsPane.setCancel(new TaskCancellationAction(it -> {
process.stop();
it.fireEvent(new DialogCloseEvent());
});
}));
} else {
Platform.runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
@@ -230,7 +234,7 @@ public final class LauncherHelper {
Exception ex = executor.getException();
if (!(ex instanceof CancellationException)) {
String message;
if (ex instanceof CurseCompletionException) {
if (ex instanceof ModpackCompletionException) {
if (ex.getCause() instanceof FileNotFoundException)
message = i18n("modpack.type.curse.not_found");
else
@@ -336,25 +340,48 @@ public final class LauncherHelper {
Runnable continueAction = () -> future.complete(JavaVersion.fromCurrentEnvironment());
if (setting.isJavaAutoSelected()) {
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
GameJavaVersion targetJavaVersion;
GameJavaVersion targetJavaVersion = null;
if (range.getMandatory().contains(VersionNumber.asVersion("16"))) {
targetJavaVersion = GameJavaVersion.JAVA_16;
} else if (range.getMandatory().contains(VersionNumber.asVersion("1.8.0_51"))) {
targetJavaVersion = GameJavaVersion.JAVA_8;
} else {
targetJavaVersion = null;
if (org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
if (range.getMandatory().contains(VersionNumber.asVersion("17.0.1"))) {
targetJavaVersion = GameJavaVersion.JAVA_17;
} else if (range.getMandatory().contains(VersionNumber.asVersion("16.0.1"))) {
targetJavaVersion = GameJavaVersion.JAVA_16;
} else {
String java8Version;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
java8Version = "1.8.0_51";
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
java8Version = "1.8.0_202";
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
java8Version = "1.8.0_74";
} else {
java8Version = null;
}
if (java8Version != null && range.getMandatory().contains(VersionNumber.asVersion(java8Version)))
targetJavaVersion = GameJavaVersion.JAVA_8;
else
targetJavaVersion = null;
}
}
if (targetJavaVersion != null) {
if (targetJavaVersion == null) {
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> {
future.completeExceptionally(new CancellationException("No accepted java"));
});
} else {
downloadJava(gameVersion.toString(), targetJavaVersion, profile)
.thenAcceptAsync(downloadedJavaVersion -> {
future.complete(downloadedJavaVersion);
})
.exceptionally(throwable -> {
LOG.log(Level.WARNING, "Failed to download java", throwable);
Controllers.dialog(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
Controllers.confirm(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction, () -> {
future.completeExceptionally(new CancellationException("No accepted java"));
});
return null;
});
}
@@ -413,51 +440,35 @@ public final class LauncherHelper {
.thenAcceptAsync(downloadedJavaVersion -> {
setting.setJavaVersion(downloadedJavaVersion);
future.complete(downloadedJavaVersion);
})
.exceptionally(throwable -> {
}, Schedulers.javafx())
.whenCompleteAsync((result, throwable) -> {
LOG.log(Level.WARNING, "Failed to download java", throwable);
breakAction.run();
return null;
});
}, Schedulers.javafx());
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_16:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"), () -> {
FXUtils.openLink("https://adoptium.net/?variant=openjdk17");
FXUtils.openLink(OPENJDK_DOWNLOAD_LINK);
}, breakAction);
return null;
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_17:
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 17), i18n("message.warning"), () -> {
FXUtils.openLink("https://adoptium.net/?variant=openjdk17");
FXUtils.openLink(OPENJDK_DOWNLOAD_LINK);
}, breakAction);
return null;
return Task.fromCompletableFuture(future);
case VANILLA_JAVA_8:
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
return Task.fromCompletableFuture(future);
case VANILLA_LINUX_JAVA_8:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
} else {
break;
}
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
// Windows on ARM introduced translation support for x86-64 after 10.0.21277.
|| (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)) {
Controllers.dialog(i18n("launch.advice.vanilla_x86.translation"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
}
}
Controllers.dialog(i18n("launch.advice.vanilla_x86"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
return Task.fromCompletableFuture(future);
} else {
break;
}
case LAUNCH_WRAPPER:
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction);
return null;
return Task.fromCompletableFuture(future);
}
}
}
@@ -482,13 +493,27 @@ public final class LauncherHelper {
switch (violatedSuggestedConstraint) {
case MODDED_JAVA_7:
Controllers.dialog(i18n("launch.advice.java.modded_java_7"), i18n("message.warning"), MessageType.WARNING, continueAction);
return null;
break;
case MODDED_JAVA_8:
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
case MODDED_JAVA_16:
Controllers.dialog(i18n("launch.advice.forge37_0_60"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
case VANILLA_JAVA_8_51:
Controllers.dialog(i18n("launch.advice.java8_51_1_13"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
case MODLAUNCHER_8:
Controllers.dialog(i18n("launch.advice.modlauncher8"), i18n("message.warning"), MessageType.WARNING, continueAction);
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java()) {
Controllers.dialog(i18n("launch.advice.vanilla_x86.translation"), i18n("message.warning"), MessageType.WARNING, continueAction);
} else {
continueAction.run();
}
break;
}
}
@@ -523,7 +548,7 @@ public final class LauncherHelper {
}
}
if (!future.isDone()) {
if (!suggested && !future.isDone()) {
future.complete(javaVersion);
}
@@ -536,7 +561,11 @@ public final class LauncherHelper {
JFXHyperlink link = new JFXHyperlink(i18n("download.external_link"));
link.setOnAction(e -> {
FXUtils.openLink("https://docs.microsoft.com/zh-cn/java/openjdk/download");
if (javaVersion.getMajorVersion() == JavaVersion.JAVA_8) {
FXUtils.openLink(ORACLEJDK_DOWNLOAD_LINK);
} else {
FXUtils.openLink(OPENJDK_DOWNLOAD_LINK);
}
future.completeExceptionally(new CancellationException());
});
@@ -578,26 +607,55 @@ public final class LauncherHelper {
private static CompletableFuture<JavaVersion> downloadJavaImpl(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
TaskExecutorDialogPane javaDownloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = JavaRepository.downloadJava(javaVersion, downloadProvider)
Controllers.taskDialog(JavaRepository.downloadJava(javaVersion, downloadProvider)
.whenComplete(Schedulers.javafx(), (downloadedJava, exception) -> {
if (exception != null) {
future.completeExceptionally(exception);
} else {
future.complete(downloadedJava);
}
})
.executor(false);
javaDownloadingPane.setExecutor(executor, true);
Controllers.dialog(javaDownloadingPane);
executor.start();
}), i18n("download.java"), TaskCancellationAction.NORMAL);
return future;
}
private static Task<AuthInfo> logIn(Account account) {
return Task.composeAsync(() -> {
try {
return Task.completed(account.logIn());
} catch (CredentialExpiredException e) {
LOG.log(Level.INFO, "Credential has expired", e);
return Task.completed(DialogController.logIn(account));
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Authentication failed, try skipping refresh", e);
CompletableFuture<Task<AuthInfo>> future = new CompletableFuture<>();
runInFX(() -> {
JFXButton loginOfflineButton = new JFXButton(i18n("account.login.skip"));
loginOfflineButton.setOnAction(event -> {
try {
future.complete(Task.completed(account.playOffline()));
} catch (AuthenticationException e2) {
future.completeExceptionally(e2);
}
});
JFXButton retryButton = new JFXButton(i18n("account.login.retry"));
retryButton.setOnAction(event -> {
future.complete(logIn(account));
});
Controllers.dialog(new MessageDialogPane.Builder(i18n("account.failed.server_disconnected"), i18n("account.failed"), MessageType.ERROR)
.addAction(loginOfflineButton)
.addAction(retryButton)
.addCancel(() ->
future.completeExceptionally(new CancellationException()))
.build());
});
return Task.fromCompletableFuture(future).thenComposeAsync(task -> task);
}
});
}
private void checkExit() {
switch (launcherVisibility) {
case HIDE_AND_REOPEN:
@@ -627,19 +685,20 @@ public final class LauncherHelper {
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
* Because every time we launched a game, we generates a new [HMCLProcessListener]
*/
class HMCLProcessListener implements ProcessListener {
private final class HMCLProcessListener implements ProcessListener {
private final HMCLGameRepository repository;
private final Version version;
private final Map<String, String> forbiddenTokens;
private final LaunchOptions launchOptions;
private ManagedProcess process;
private boolean lwjgl;
private LogWindow logWindow;
private final boolean detectWindow;
private final LinkedList<Pair<String, Log4jLevel>> logs;
private final ArrayDeque<String> logs;
private final ArrayDeque</*Log4jLevel*/Object> levels;
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
private final CountDownLatch launchingLatch;
private final String forbiddenAccessToken;
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
this.repository = repository;
@@ -647,15 +706,11 @@ public final class LauncherHelper {
this.launchOptions = launchOptions;
this.launchingLatch = launchingLatch;
this.detectWindow = detectWindow;
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;
if (authInfo == null)
forbiddenTokens = Collections.emptyMap();
else
forbiddenTokens = mapOf(
pair(authInfo.getAccessToken(), "<access token>")
);
logs = new LinkedList<>();
final int numLogs = config().getLogLines() + 1;
this.logs = new ArrayDeque<>(numLogs);
this.levels = new ArrayDeque<>(numLogs);
}
@Override
@@ -663,8 +718,6 @@ public final class LauncherHelper {
this.process = process;
String command = new CommandBuilder().addAll(process.getCommands()).toString();
for (Map.Entry<String, String> entry : forbiddenTokens.entrySet())
command = command.replace(entry.getKey(), entry.getValue());
LOG.info("Launched process: " + command);
@@ -708,6 +761,7 @@ public final class LauncherHelper {
Controllers.getStage().close();
Controllers.shutdown();
Schedulers.shutdown();
System.gc();
}
});
break;
@@ -715,20 +769,28 @@ public final class LauncherHelper {
}
@Override
public synchronized void onLog(String log, Log4jLevel level) {
String newLog = log;
for (Map.Entry<String, String> entry : forbiddenTokens.entrySet())
newLog = newLog.replace(entry.getKey(), entry.getValue());
String filteredLog = newLog;
public void onLog(String log, boolean isErrorStream) {
String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, "<access token>");
if (level.lessOrEqual(Log4jLevel.ERROR))
if (isErrorStream)
System.err.println(filteredLog);
else
System.out.println(filteredLog);
logs.add(pair(filteredLog, level));
if (logs.size() > config().getLogLines())
logs.removeFirst();
Log4jLevel level;
if (isErrorStream)
level = Log4jLevel.ERROR;
else
level = showLogs ? Optional.ofNullable(Log4jLevel.guessLevel(filteredLog)).orElse(Log4jLevel.INFO) : null;
synchronized (this) {
logs.add(filteredLog);
levels.add(level != null ? level : Optional.empty()); // Use 'Optional.empty()' as hole
if (logs.size() > config().getLogLines()) {
logs.removeFirst();
levels.removeFirst();
}
}
if (showLogs) {
try {
@@ -741,7 +803,7 @@ public final class LauncherHelper {
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
}
if (!lwjgl && (filteredLog.toLowerCase().contains("lwjgl version") || filteredLog.toLowerCase().contains("lwjgl openal") || !detectWindow)) {
if (!lwjgl && (!detectWindow || filteredLog.toLowerCase().contains("lwjgl version") || filteredLog.toLowerCase().contains("lwjgl openal"))) {
lwjgl = true;
finishLaunch();
}
@@ -758,8 +820,11 @@ public final class LauncherHelper {
if (!lwjgl) finishLaunch();
if (exitType != ExitType.NORMAL) {
ArrayList<Pair<String, Log4jLevel>> pairs = new ArrayList<>(logs.size());
Lang.forEachZipped(logs, levels,
(log, l) -> pairs.add(pair(log, l instanceof Log4jLevel ? ((Log4jLevel) l) : Optional.ofNullable(Log4jLevel.guessLevel(log)).orElse(Log4jLevel.INFO))));
repository.markVersionLaunchedAbnormally(version.getId());
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, pairs).show());
}
checkExit();
@@ -767,6 +832,9 @@ public final class LauncherHelper {
}
private static final String ORACLEJDK_DOWNLOAD_LINK = "https://www.java.com/download";
private static final String OPENJDK_DOWNLOAD_LINK = "https://docs.microsoft.com/java/openjdk/download";
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
public static void stopManagedProcesses() {
@@ -0,0 +1,88 @@
/*
* 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.StringUtils;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
public abstract class LocalizedRemoteModRepository implements RemoteModRepository {
protected abstract RemoteModRepository getBackedRemoteModRepository();
@Override
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
String newSearchFilter;
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType());
List<ModTranslations.Mod> mods = modTranslations.searchMod(searchFilter);
List<String> searchFilters = new ArrayList<>();
int count = 0;
for (ModTranslations.Mod mod : mods) {
String englishName = mod.getName();
if (StringUtils.isNotBlank(mod.getSubname())) {
englishName = mod.getSubname();
}
searchFilters.add(englishName);
count++;
if (count >= 3) break;
}
newSearchFilter = String.join(" ", searchFilters);
} else {
newSearchFilter = searchFilter;
}
return getBackedRemoteModRepository().search(gameVersion, category, pageOffset, pageSize, newSearchFilter, sort, sortOrder);
}
@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);
}
}
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.Zipper;
@@ -66,7 +65,7 @@ public final class LogExporter {
}
zipper.putTextFile(Logging.getLogs(), "hmcl.log");
zipper.putTextFile(logs, "minecraft.log");
zipper.putTextFile(launchScript, OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh");
zipper.putTextFile(Logging.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");
@@ -77,6 +76,6 @@ public final class LogExporter {
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, Schedulers.io());
});
}
}
@@ -19,16 +19,16 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.mod.curse.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;
@@ -41,6 +41,7 @@ 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;
@@ -51,44 +52,54 @@ 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() {}
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 {
return McbbsModpackManifest.readManifest(file, charset);
} catch (Exception ignored) {
// ignore it, not a valid MCBBS modpack.
}
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 (ZipFile 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 (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
@@ -137,17 +148,6 @@ public final class ModpackHelper {
}
}
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<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
@@ -161,7 +161,7 @@ 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
}
@@ -203,7 +203,7 @@ 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
}
@@ -232,38 +232,13 @@ public final class ModpackHelper {
}
}
public static Task<Void> getUpdateTask(Profile profile, File zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, 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) {
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -104,7 +103,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to load html");
@@ -160,13 +159,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
@Override
public String getClientId() {
return System.getProperty("hmcl.microsoft.auth.id",
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Id")).orElse(""));
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
}
@Override
public String getClientSecret() {
return System.getProperty("hmcl.microsoft.auth.secret",
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Secret")).orElse(""));
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
}
@Override
@@ -17,9 +17,13 @@
*/
package org.jackhuang.hmcl.game;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
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.PixelWriter;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
@@ -31,18 +35,15 @@ import org.jackhuang.hmcl.util.ResourceNotFoundError;
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.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;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -64,15 +65,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;
}
@@ -96,7 +97,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");
}
@@ -117,12 +118,13 @@ public final class TexturesLoader {
}
}
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,16 +138,21 @@ public final class TexturesLoader {
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
static {
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
loadDefaultSkin("/assets/img/skin/steve.png", TextureModel.STEVE);
loadDefaultSkin("/assets/img/skin/alex.png", TextureModel.ALEX);
}
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)));
Image skin;
try {
skin = new Image(path);
if (skin.isError())
throw skin.getException();
} catch (Throwable e) {
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
throw new ResourceNotFoundError("Cannot load default skin from " + path, e);
}
DEFAULT_SKINS.put(model, new LoadedTexture(skin, singletonMap("model", model.modelName)));
}
public static LoadedTexture getDefaultSkin(TextureModel model) {
@@ -172,7 +179,7 @@ public final class TexturesLoader {
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
@@ -195,7 +202,7 @@ public final class TexturesLoader {
return CompletableFuture.supplyAsync(() -> {
try {
return loadTexture(texture);
} catch (IOException e) {
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
return uuidFallback;
}
@@ -209,38 +216,109 @@ public final class TexturesLoader {
// ====
// ==== 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) {
canvas.getGraphicsContext2D().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);
GraphicsContext g = canvas.getGraphicsContext2D();
try {
g.setImageSmoothing(false);
drawAvatarFX(g, skin, size, scale, faceOffset);
} catch (NoSuchMethodError ignored) {
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing, fallback to Java 2D
drawAvatarJ2D(g, skin, size, scale, faceOffset);
}
}
private static void drawAvatarFX(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);
}
private static void drawAvatarJ2D(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
BufferedImage bi = FXUtils.fromFXImage(skin);
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = avatar.createGraphics();
g2d.drawImage(bi,
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
8 * scale, 8 * scale, 16 * scale, 16 * scale,
null);
g.drawImage(skin,
g2d.drawImage(bi,
0, 0, size, size,
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
g.dispose();
return avatar;
g2d.dispose();
PixelWriter pw = g.getPixelWriter();
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
pw.setArgb(x, y, avatar.getRGB(x, y));
}
}
}
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
return BindingMapping.of(skinBinding(service, uuid))
.map(it -> toAvatar(it.image, size))
.map(FXUtils::toFXImage);
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
final WeakReference<Canvas> canvasRef;
final ObjectBinding<LoadedTexture> binding;
SkinBindingChangeListener(Canvas canvas, ObjectBinding<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 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(FXUtils::toFXImage);
} else {
return Bindings.createObjectBinding(
() -> FXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
public static void fxAvatarBinding(Canvas canvas, ObjectBinding<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.get());
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)
fxAvatarBinding(canvas, skinBinding(account));
else {
unbindAvatar(canvas);
drawAvatar(canvas, getDefaultSkin(TextureModel.detectUUID(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);
}
}
// ====
@@ -81,6 +81,7 @@ public final class Accounts {
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
public static final BoundAuthlibInjectorAccountFactory FACTORY_LITTLE_SKIN = getAccountFactoryByAuthlibInjectorServer(new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/"));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
// ==== login type / account factory mapping ====
@@ -96,14 +97,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) {
@@ -119,10 +130,10 @@ public final class Accounts {
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 ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static final ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
private static ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@@ -231,6 +242,14 @@ public final class Accounts {
triggerAuthlibInjectorUpdateCheck();
}
Schedulers.io().execute(() -> {
try {
FACTORY_LITTLE_SKIN.getServer().fetchMetadataResponse();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + FACTORY_LITTLE_SKIN.getServer(), e);
}
});
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
continue;
@@ -313,7 +332,7 @@ 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"),
@@ -40,6 +40,7 @@ import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jackhuang.hmcl.util.javafx.PropertyUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.io.File;
@@ -140,13 +141,13 @@ public final class Config implements Cloneable, Observable {
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("accounts")
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@SerializedName("fontFamily")
private StringProperty fontFamily = new SimpleStringProperty("Consolas");
private StringProperty fontFamily = new SimpleStringProperty(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace");
@SerializedName("fontSize")
private DoubleProperty fontSize = new SimpleDoubleProperty(12);
@@ -475,7 +476,7 @@ public final class Config implements Cloneable, Observable {
return versionListSource;
}
public ObservableMap<String, Profile> getConfigurations() {
public MapProperty<String, Profile> getConfigurations() {
return configurations;
}
@@ -22,6 +22,7 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -45,6 +46,7 @@ public final class ConfigHolder {
private static Config configInstance;
private static GlobalConfig globalConfigInstance;
private static boolean newlyCreated;
private static boolean ownerChanged = false;
public static Config config() {
if (configInstance == null) {
@@ -60,10 +62,18 @@ public final class ConfigHolder {
return globalConfigInstance;
}
public static Path configLocation() {
return configLocation;
}
public static boolean isNewlyCreated() {
return newlyCreated;
}
public static boolean isOwnerChanged() {
return ownerChanged;
}
public synchronized static void init() throws IOException {
if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded");
@@ -109,7 +119,7 @@ public final class ConfigHolder {
}
private static Path locateConfig() {
Path exePath = Paths.get("");
Path exePath = Paths.get("").toAbsolutePath();
try {
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
.toURI()).toAbsolutePath();
@@ -143,6 +153,15 @@ public final class ConfigHolder {
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.log(Level.WARNING, "Failed to get owner");
}
try {
String content = FileUtils.readText(configLocation);
Config deserialized = Config.fromJson(content);
@@ -189,6 +208,28 @@ public final class ConfigHolder {
// Global Config
private static GlobalConfig loadGlobalConfig() throws IOException {
// Migrate from old directory
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Files.notExists(GLOBAL_CONFIG_PATH)) {
Path oldHome;
String xdgCache = System.getenv("XDG_CACHE_HOME");
if (StringUtils.isNotBlank(xdgCache)) {
oldHome = Paths.get(xdgCache, "hmcl");
} else {
oldHome = Paths.get(System.getProperty("user.home", "."), ".cache", "hmcl");
}
if (Files.exists(oldHome)) {
Path oldConfigPath = oldHome.resolve("config.json");
if (Files.isRegularFile(oldConfigPath)) {
try {
Files.copy(oldConfigPath, GLOBAL_CONFIG_PATH);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to migrate global config", e);
}
}
}
}
if (Files.exists(GLOBAL_CONFIG_PATH)) {
try {
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
@@ -68,7 +68,7 @@ public class GlobalConfig implements Cloneable, Observable {
private IntegerProperty agreementVersion = new SimpleIntegerProperty();
private StringProperty multiplayerToken = new SimpleStringProperty();
private IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
private BooleanProperty multiplayerRelay = new SimpleBooleanProperty();
@@ -113,6 +113,18 @@ public class GlobalConfig implements Cloneable, Observable {
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 boolean isMultiplayerRelay() {
return multiplayerRelay.get();
}
@@ -137,22 +149,12 @@ public class GlobalConfig implements Cloneable, Observable {
this.multiplayerAgreementVersion.set(multiplayerAgreementVersion);
}
public String getMultiplayerToken() {
return multiplayerToken.get();
}
public StringProperty multiplayerTokenProperty() {
return multiplayerToken;
}
public void setMultiplayerToken(String multiplayerToken) {
this.multiplayerToken.set(multiplayerToken);
}
public static class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
"agreementVersion",
"platformPromptVersion",
"multiplayerToken",
"multiplayerRelay",
"multiplayerAgreementVersion"
));
@@ -164,7 +166,7 @@ public class GlobalConfig implements Cloneable, Observable {
JsonObject jsonObject = new JsonObject();
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
jsonObject.add("multiplayerToken", context.serialize(src.getMultiplayerToken()));
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
jsonObject.add("multiplayerRelay", context.serialize(src.isMultiplayerRelay()));
jsonObject.add("multiplayerAgreementVersion", context.serialize(src.getMultiplayerAgreementVersion()));
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
@@ -182,7 +184,7 @@ public class GlobalConfig implements Cloneable, Observable {
GlobalConfig config = new GlobalConfig();
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
config.setMultiplayerToken(Optional.ofNullable(obj.get("multiplayerToken")).map(JsonElement::getAsString).orElse(null));
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
config.setMultiplayerRelay(Optional.ofNullable(obj.get("multiplayerRelay")).map(JsonElement::getAsBoolean).orElse(false));
config.setMultiplayerAgreementVersion(Optional.ofNullable(obj.get("multiplayerAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
@@ -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;
}
}
@@ -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();
@@ -83,14 +83,7 @@ public class Theme {
public String[] getStylesheets(String overrideFontFamily) {
Color textFill = getForegroundColor();
String fontFamily;
if (System.getProperty("hmcl.font.override") != null) {
fontFamily = System.getProperty("hmcl.font.override");
} else if (overrideFontFamily != null) {
fontFamily = overrideFontFamily;
} else {
fontFamily = null;
}
String fontFamily = System.getProperty("hmcl.font.override", overrideFontFamily);
String css;
try {
@@ -104,6 +97,7 @@ public class Theme {
.replace("%disabled-font-color%", String.format("rgba(%d, %d, %d, 0.7)", (int) Math.ceil(textFill.getRed() * 256), (int) Math.ceil(textFill.getGreen() * 256), (int) Math.ceil(textFill.getBlue() * 256)))
.replace("%font-color%", getColorDisplayName(getForegroundColor()))
.replace("%font%", Optional.ofNullable(fontFamily).map(f -> "-fx-font-family: \"" + f + "\";").orElse("")));
temp.deleteOnExit();
css = temp.toURI().toString();
} catch (IOException | NullPointerException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
@@ -27,7 +27,8 @@ public enum VersionIconType {
CRAFT_TABLE("/assets/img/craft_table.png"),
FABRIC("/assets/img/fabric.png"),
FORGE("/assets/img/forge.png"),
FURNACE("/assets/img/furnace.png");
FURNACE("/assets/img/furnace.png"),
QUILT("/assets/img/quilt.png");
// Please append new items at last
@@ -24,6 +24,7 @@ import javafx.beans.property.*;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
@@ -41,8 +42,6 @@ import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.stream.Collectors;
import static com.jfoenix.concurrency.JFXUtilities.runInFX;
/**
*
* @author huangyuhui
@@ -392,6 +391,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() {
@@ -540,6 +553,20 @@ public final class VersionSetting implements Cloneable {
processPriorityProperty.set(processPriority);
}
private final BooleanProperty useSoftwareRenderer = new SimpleBooleanProperty(this, "softwareRenderer", false);
public boolean isUseSoftwareRenderer() {
return useSoftwareRenderer.get();
}
public BooleanProperty useSoftwareRendererProperty() {
return useSoftwareRenderer;
}
public void setUseSoftwareRenderer(boolean useSoftwareRenderer) {
this.useSoftwareRenderer.set(useSoftwareRenderer);
}
private final BooleanProperty useNativeGLFW = new SimpleBooleanProperty(this, "nativeGLFW", false);
public boolean isUseNativeGLFW() {
@@ -610,7 +637,7 @@ public final class VersionSetting implements Cloneable {
public Task<JavaVersion> getJavaVersion(VersionNumber gameVersion, Version version, boolean checkJava) {
return Task.runAsync(Schedulers.javafx(), () -> {
if (StringUtils.isBlank(getJava())) {
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
setJava(StringUtils.isBlank(getJavaDir()) ? "Auto" : "Custom");
}
}).thenSupplyAsync(() -> {
try {
@@ -632,9 +659,7 @@ public final class VersionSetting implements Cloneable {
.filter(java -> java.getVersion().equals(getJava()))
.collect(Collectors.toList());
if (matchedJava.isEmpty()) {
runInFX(() -> {
setJava("Default");
});
FXUtils.runInFX(() -> setJava("Auto"));
return JavaVersion.fromCurrentEnvironment();
} else {
return matchedJava.stream()
@@ -670,6 +695,7 @@ public final class VersionSetting implements Cloneable {
noJVMArgsProperty.addListener(listener);
notCheckGameProperty.addListener(listener);
notCheckJVMProperty.addListener(listener);
notPatchNativesProperty.addListener(listener);
showLogsProperty.addListener(listener);
serverIpProperty.addListener(listener);
fullscreenProperty.addListener(listener);
@@ -678,6 +704,7 @@ public final class VersionSetting implements Cloneable {
gameDirTypeProperty.addListener(listener);
gameDirProperty.addListener(listener);
processPriorityProperty.addListener(listener);
useSoftwareRenderer.addListener(listener);
useNativeGLFW.addListener(listener);
useNativeOpenAL.addListener(listener);
launcherVisibilityProperty.addListener(listener);
@@ -706,6 +733,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setNoJVMArgs(isNoJVMArgs());
versionSetting.setNotCheckGame(isNotCheckGame());
versionSetting.setNotCheckJVM(isNotCheckJVM());
versionSetting.setNotPatchNatives(isNotPatchNatives());
versionSetting.setShowLogs(isShowLogs());
versionSetting.setServerIp(getServerIp());
versionSetting.setFullscreen(isFullscreen());
@@ -714,6 +742,7 @@ public final class VersionSetting implements Cloneable {
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setProcessPriority(getProcessPriority());
versionSetting.setUseSoftwareRenderer(isUseSoftwareRenderer());
versionSetting.setUseNativeGLFW(isUseNativeGLFW());
versionSetting.setUseNativeOpenAL(isUseNativeOpenAL());
versionSetting.setLauncherVisibility(getLauncherVisibility());
@@ -747,10 +776,12 @@ public final class VersionSetting implements Cloneable {
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("useSoftwareRenderer", src.isUseSoftwareRenderer());
obj.addProperty("useNativeGLFW", src.isUseNativeGLFW());
obj.addProperty("useNativeOpenAL", src.isUseNativeOpenAL());
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
@@ -762,9 +793,17 @@ public final class VersionSetting implements Cloneable {
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;
@@ -793,15 +832,17 @@ public final class VersionSetting implements Cloneable {
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(LauncherVisibility.HIDE.ordinal())]);
vs.setProcessPriority(ProcessPriority.values()[Optional.ofNullable(obj.get("processPriority")).map(JsonElement::getAsInt).orElse(ProcessPriority.NORMAL.ordinal())]);
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
vs.setUseSoftwareRenderer(Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false));
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(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(GameDirectoryType.ROOT_FOLDER.ordinal())]);
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(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
vs.setVersionIcon(VersionIconType.values()[Optional.ofNullable(obj.get("versionIcon")).map(JsonElement::getAsInt).orElse(0)]);
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
return vs;
}
@@ -9,8 +9,8 @@ public final class 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);
@@ -31,7 +31,7 @@ import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.java.JavaRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Profiles;
@@ -45,19 +45,20 @@ 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.multiplayer.MultiplayerPage;
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.Lazy;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
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.setting.ConfigHolder.globalConfig;
@@ -75,7 +76,7 @@ public final class Controllers {
GameListPage gameListPage = new GameListPage();
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
});
@@ -91,7 +92,6 @@ public final class Controllers {
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
return accountListPage;
});
private static Lazy<MultiplayerPage> multiplayerPage = new Lazy<>(MultiplayerPage::new);
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
private Controllers() {
@@ -120,11 +120,6 @@ public final class Controllers {
return rootPage.get();
}
// FXThread
public static MultiplayerPage getMultiplayerPage() {
return multiplayerPage.get();
}
// FXThread
public static LauncherSettingsPage getSettingsPage() {
return settingsPage.get();
@@ -146,10 +141,16 @@ public final class Controllers {
}
public static void onApplicationStop() {
config().setHeight(stageHeight.get());
config().setWidth(stageWidth.get());
stageHeight = null;
stageWidth = null;
if (stageHeight != null) {
config().setHeight(stageHeight.get());
stageHeight.unbind();
stageHeight = null;
}
if (stageWidth != null) {
config().setWidth(stageWidth.get());
stageWidth.unbind();
stageWidth = null;
}
}
public static void initialize(Stage stage) {
@@ -172,7 +173,7 @@ public final class Controllers {
dialog(i18n("launcher.cache_directory.invalid"));
}
Task.runAsync(JavaVersion::initialize).thenRunAsync(JavaRepository::initialize).start();
Task.runAsync(JavaVersion::initialize).start();
scene = new Scene(decorator.getDecorator());
scene.setFill(Color.TRANSPARENT);
@@ -187,6 +188,19 @@ public final class Controllers {
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
if (!Architecture.SYSTEM_ARCH.isX86() && globalConfig().getPlatformPromptVersion() < 1) {
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
Controllers.dialog(i18n("fatal.unsupported_platform.osx_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 {
Controllers.dialog(i18n("fatal.unsupported_platform"), null, MessageType.WARNING, continueAction);
}
}
if (globalConfig().getAgreementVersion() < 1) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
@@ -262,11 +276,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);
@@ -274,7 +284,7 @@ public final class Controllers {
return pane;
}
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, Consumer<Region> onCancel) {
public static TaskExecutorDialogPane taskDialog(Task<?> task, String title, TaskCancellationAction onCancel) {
TaskExecutor executor = task.executor();
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(onCancel);
pane.setTitle(title);
@@ -311,9 +321,12 @@ public final class Controllers {
rootPage = null;
versionPage = null;
gameListPage = null;
downloadPage = null;
accountListPage = null;
settingsPage = null;
decorator = null;
stage = null;
scene = null;
onApplicationStop();
}
}
@@ -34,10 +34,8 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.input.*;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority;
@@ -49,6 +47,7 @@ import javafx.scene.text.TextFlow;
import javafx.util.Callback;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.util.Logging;
@@ -58,6 +57,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -72,6 +72,7 @@ import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URI;
@@ -357,45 +358,77 @@ public final class FXUtils {
public static void openFolder(File file) {
if (!FileUtils.makeDirectory(file)) {
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
LOG.log(Level.SEVERE, "Unable to make directory " + file);
return;
}
String path = file.getAbsolutePath();
switch (OperatingSystem.CURRENT_OS) {
case OSX:
String openCommand;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
openCommand = "explorer.exe";
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
openCommand = "/usr/bin/open";
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && new File("/usr/bin/xdg-open").exists())
openCommand = "/usr/bin/xdg-open";
else
openCommand = null;
thread(() -> {
if (openCommand != null) {
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path});
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by executing /usr/bin/open", e);
int exitCode = SystemUtils.callExternalProcess(openCommand, path);
// explorer.exe always return 1
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
return;
else
LOG.warning("Open " + path + " failed with code " + exitCode);
} catch (Throwable e) {
LOG.log(Level.WARNING, "Unable to open " + path + " by executing " + openCommand, e);
}
break;
default:
thread(() -> {
if (java.awt.Desktop.isDesktopSupported()) {
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
}
});
}
}
// Fallback to java.awt.Desktop::open
try {
java.awt.Desktop.getDesktop().open(file);
} catch (Throwable e) {
LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
}
});
}
public static void showFileInExplorer(Path file) {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
String path = file.toAbsolutePath().toString();
String[] openCommands;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
openCommands = new String[]{"explorer.exe", "/select,", path};
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
openCommands = new String[]{"/usr/bin/open", "-R", path};
else
openCommands = null;
if (openCommands != null) {
thread(() -> {
try {
Runtime.getRuntime().exec(new String[]{"explorer.exe", "/select,", file.toAbsolutePath().toString()});
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing explorer /select", e);
int exitCode = SystemUtils.callExternalProcess(openCommands);
// explorer.exe always return 1
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
return;
else
LOG.warning("Show " + path + " in explorer failed with code " + exitCode);
} catch (Throwable e) {
LOG.log(Level.WARNING, "Unable to show " + path + " in explorer", e);
}
break;
default:
// Currently unsupported.
break;
// Fallback to open folder
openFolder(file.getParent().toFile());
});
} else {
// We do not have a universal method to show file in file manager.
openFolder(file.getParent().toFile());
}
}
@@ -418,34 +451,38 @@ public final class FXUtils {
if (link == null)
return;
if (java.awt.Desktop.isDesktopSupported()) {
thread(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
for (String browser : linuxBrowsers) {
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
if (is.read() != -1) {
Runtime.getRuntime().exec(new String[]{browser, link});
return;
}
} catch (Throwable ignored) {
}
Logging.LOG.log(Level.WARNING, "No known browser found");
}
}
thread(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
java.awt.Desktop.getDesktop().browse(new URI(link));
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
return;
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
LOG.log(Level.WARNING, "An exception occurred while calling rundll32", e);
}
});
}
} if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
for (String browser : linuxBrowsers) {
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
if (is.read() != -1) {
Runtime.getRuntime().exec(new String[]{browser, link});
return;
}
} catch (Throwable ignored) {
}
Logging.LOG.log(Level.WARNING, "No known browser found");
}
}
try {
java.awt.Desktop.getDesktop().browse(new URI(link));
} catch (Throwable e) {
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
try {
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
} catch (IOException ex) {
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
}
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
}
});
}
public static void showWebDialog(String title, String content) {
@@ -461,12 +498,12 @@ public final class FXUtils {
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
LOG.log(Level.WARNING, "WebView is missing or initialization failed, use JEditorPane replaced", e);
SwingUtils.initLookAndFeel();
SwingUtilities.invokeLater(() -> {
final JFrame frame = new JFrame(title);
frame.setSize(width, height);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.setLocationByPlatform(true);
//noinspection ConstantConditions
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
frame.setLayout(new BorderLayout());
@@ -494,20 +531,85 @@ public final class FXUtils {
}
}
public static void bindInt(JFXTextField textField, Property<Number> property) {
textField.textProperty().bindBidirectional(property, SafeStringConverter.fromInteger());
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
textField.setText(converter == null ? (String) property.getValue() : converter.toString(property.getValue()));
TextFieldBindingListener<T> listener = new TextFieldBindingListener<>(textField, property, converter);
textField.focusedProperty().addListener((ChangeListener<Boolean>) listener);
property.addListener(listener);
}
public static void unbindInt(JFXTextField textField, Property<Number> property) {
textField.textProperty().unbindBidirectional(property);
public static void bindInt(JFXTextField textField, Property<Number> property) {
bind(textField, property, SafeStringConverter.fromInteger());
}
public static void bindString(JFXTextField textField, Property<String> property) {
textField.textProperty().bindBidirectional(property);
bind(textField, property, null);
}
public static void unbindString(JFXTextField textField, Property<String> property) {
textField.textProperty().unbindBidirectional(property);
public static void unbind(JFXTextField textField, Property<?> property) {
TextFieldBindingListener<?> listener = new TextFieldBindingListener<>(textField, property, null);
textField.focusedProperty().removeListener((ChangeListener<Boolean>) listener);
property.removeListener(listener);
}
private static final class TextFieldBindingListener<T> implements ChangeListener<Boolean>, InvalidationListener {
private final int hashCode;
private final WeakReference<JFXTextField> textFieldRef;
private final WeakReference<Property<T>> propertyRef;
private final StringConverter<T> converter;
TextFieldBindingListener(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
this.textFieldRef = new WeakReference<>(textField);
this.propertyRef = new WeakReference<>(property);
this.converter = converter;
this.hashCode = System.identityHashCode(textField) ^ System.identityHashCode(property);
}
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean focused) { // On TextField changed
JFXTextField textField = textFieldRef.get();
Property<T> property = this.propertyRef.get();
if (textField != null && property != null && oldValue == Boolean.TRUE && focused == Boolean.FALSE) {
if (textField.validate()) {
String newText = textField.getText();
@SuppressWarnings("unchecked")
T newValue = converter == null ? (T) newText : converter.fromString(newText);
if (!Objects.equals(newValue, property.getValue()))
property.setValue(newValue);
} else {
// Rollback to old value
invalidated(null);
}
}
}
@Override
public void invalidated(Observable observable) { // On property change
JFXTextField textField = textFieldRef.get();
Property<T> property = this.propertyRef.get();
if (textField != null && property != null) {
T value = property.getValue();
textField.setText(converter == null ? (String) value : converter.toString(value));
}
}
@Override
public int hashCode() {
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof TextFieldBindingListener))
return false;
TextFieldBindingListener<?> other = (TextFieldBindingListener<?>) obj;
return this.hashCode == other.hashCode
&& this.textFieldRef.get() == other.textFieldRef.get()
&& this.propertyRef.get() == other.propertyRef.get();
}
}
public static void bindBoolean(JFXToggleButton toggleButton, Property<Boolean> property) {
@@ -624,10 +726,17 @@ public final class FXUtils {
}
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
MutableObject<Object> lastCell = new MutableObject<>();
return view -> new JFXListCell<T>() {
@Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
return;
lastCell.setValue(this);
if (!empty) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(graphicBuilder.apply(item));
@@ -680,11 +789,12 @@ public final class FXUtils {
// Based on https://stackoverflow.com/a/57552025
// Fix #874: Use it instead of SwingFXUtils.toFXImage
public 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();
WritableImage wr = new WritableImage(iw, ih);
PixelWriter pw = wr.getPixelWriter();
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
pw.setArgb(x, y, image.getRGB(x, y));
@@ -693,6 +803,21 @@ public final class FXUtils {
return wr;
}
public static BufferedImage fromFXImage(Image image) {
final int iw = (int) image.getWidth();
final int ih = (int) image.getHeight();
PixelReader pr = image.getPixelReader();
BufferedImage bufferedImage = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_ARGB);
for (int x = 0; x < iw; x++) {
for (int y = 0; y < ih; y++) {
bufferedImage.setRGB(x, y, pr.getArgb(x, y));
}
}
return bufferedImage;
}
public static void copyText(String text) {
ClipboardContent content = new ClipboardContent();
content.putString(text);
@@ -702,6 +827,9 @@ public final class FXUtils {
}
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
if (segment.indexOf('<') < 0)
return Collections.singletonList(new Text(segment));
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
@@ -34,6 +34,7 @@ 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;
@@ -41,13 +42,13 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -76,7 +77,7 @@ public class GameCrashWindow extends Stage {
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
private final TextFlow reasonTextFlow = new TextFlow(new Text(i18n("game.crash.reason.unknown")));
private final BooleanProperty loading = new SimpleBooleanProperty();
private final Label feedbackLabel = new Label(i18n("game.crash.feedback"));
private final TextFlow feedbackTextFlow = new TextFlow();
private final ManagedProcess managedProcess;
private final DefaultGameRepository repository;
@@ -84,9 +85,9 @@ public class GameCrashWindow extends Stage {
private final LaunchOptions launchOptions;
private final View view;
private final List<Pair<String, Log4jLevel>> logs;
private final Collection<Pair<String, Log4jLevel>> logs;
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Pair<String, Log4jLevel>> logs) {
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, Collection<Pair<String, Log4jLevel>> logs) {
this.managedProcess = managedProcess;
this.exitType = exitType;
this.repository = repository;
@@ -103,6 +104,8 @@ public class GameCrashWindow extends Stage {
this.view = new View();
this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction));
setScene(new Scene(view, 800, 480));
getScene().getStylesheets().addAll(config().getTheme().getStylesheets(config().getLauncherFontFamily()));
setTitle(i18n("game.crash.title"));
@@ -136,7 +139,7 @@ public class GameCrashWindow extends Stage {
if (exception != null) {
LOG.log(Level.WARNING, "Failed to analyze crash report", exception);
reasonTextFlow.getChildren().setAll(new Text(i18n("game.crash.reason.unknown")));
reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction));
} else {
List<CrashReportAnalyzer.Result> results = pair.getKey();
Set<String> keywords = pair.getValue();
@@ -176,12 +179,12 @@ public class GameCrashWindow extends Stage {
if (!keywords.isEmpty()) {
reasonTextFlow.getChildren().setAll(new Text(i18n("game.crash.reason.stacktrace", String.join(", ", keywords))));
} else {
reasonTextFlow.getChildren().setAll(new Text(i18n("game.crash.reason.unknown")));
reasonTextFlow.getChildren().setAll(FXUtils.parseSegment(i18n("game.crash.reason.unknown"), Controllers::onHyperlinkAction));
}
feedbackLabel.setVisible(true);
feedbackTextFlow.setVisible(true);
} else {
feedbackLabel.setVisible(false);
feedbackTextFlow.setVisible(false);
reasonTextFlow.getChildren().setAll(segments);
}
}
@@ -220,8 +223,8 @@ public class GameCrashWindow extends Stage {
private void showLogWindow() {
LogWindow logWindow = new LogWindow();
logWindow.logLine("Command: " + new CommandBuilder().addAll(managedProcess.getCommands()).toString(), Log4jLevel.INFO);
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
logWindow.logLine(Logging.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
if (managedProcess.getClasspath() != null) logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
@@ -236,15 +239,11 @@ public class GameCrashWindow extends Stage {
.thenComposeAsync(logs ->
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
.thenRunAsync(() -> {
FXUtils.showFileInExplorer(logFile);
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();
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException | IllegalArgumentException ignored) {
}
}
}, Schedulers.javafx())
.exceptionally(e -> {
LOG.log(Level.WARNING, "Failed to export game crash info", e);
@@ -281,6 +280,11 @@ public class GameCrashWindow extends Stage {
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("archive.game_version"));
@@ -306,7 +310,7 @@ public class GameCrashWindow extends Stage {
arch.setTitle(i18n("system.architecture"));
arch.subtitleProperty().bind(GameCrashWindow.this.arch);
infoPane.getChildren().setAll(version, memory, java, os, arch);
infoPane.getChildren().setAll(launcher, version, memory, java, os, arch);
}
HBox moddedPane = new HBox(8);
@@ -315,13 +319,15 @@ public class GameCrashWindow extends Stage {
moddedPane.setAlignment(Pos.CENTER_LEFT);
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
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);
});
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);
});
}
}
}
@@ -342,7 +348,7 @@ public class GameCrashWindow extends Stage {
gameDirPane.setPadding(new Insets(8));
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonTextFlow));
gameDirPane.getChildren().setAll(gameDir, javaDir, new VBox(reasonTitle, reasonTextFlow, feedbackTextFlow));
}
HBox toolBar = new HBox();
@@ -360,7 +366,7 @@ public class GameCrashWindow extends Stage {
toolBar.setPadding(new Insets(8));
toolBar.setSpacing(8);
toolBar.getStyleClass().add("jfx-tool-bar");
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, feedbackLabel);
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton);
}
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -32,9 +33,7 @@ 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.layout.*;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
@@ -59,6 +58,13 @@ public class InstallerItem extends Control {
public final ObjectProperty<EventHandler<? super MouseEvent>> removeAction = new SimpleObjectProperty<>();
public final ObjectProperty<EventHandler<? super MouseEvent>> action = new SimpleObjectProperty<>();
private Style style = Style.LIST_ITEM;
public enum Style {
LIST_ITEM,
CARD,
}
public InstallerItem(LibraryAnalyzer.LibraryType id) {
this(id.getPatchId());
}
@@ -83,12 +89,20 @@ public class InstallerItem extends Control {
case "optifine":
imageUrl = "/assets/img/command.png";
break;
case "quilt":
case "quilt-api":
imageUrl = "/assets/img/quilt.png";
break;
default:
imageUrl = null;
break;
}
}
public void setStyleMode(Style style) {
this.style = style;
}
public void setState(String libraryVersion, boolean incompatibleWithGame, boolean removable) {
this.libraryVersion.set(libraryVersion);
this.incompatibleWithGame.set(incompatibleWithGame);
@@ -111,76 +125,112 @@ public class InstallerItem extends Control {
public final InstallerItem forge = new InstallerItem(FORGE);
public final InstallerItem liteLoader = new InstallerItem(LITELOADER);
public final InstallerItem optiFine = new InstallerItem(OPTIFINE);
public final InstallerItem quilt = new InstallerItem(QUILT);
public final InstallerItem quiltApi = new InstallerItem(QUILT_API);
public InstallerItemGroup() {
forge.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
if (quilt.libraryVersion.get() != null) return QUILT.getPatchId();
return null;
}, fabric.libraryVersion));
}, fabric.libraryVersion, quilt.libraryVersion));
liteLoader.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
if (quilt.libraryVersion.get() != null) return QUILT.getPatchId();
return null;
}, fabric.libraryVersion));
}, fabric.libraryVersion, quilt.libraryVersion));
optiFine.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
if (quilt.libraryVersion.get() != null) return QUILT.getPatchId();
return null;
}, fabric.libraryVersion));
}, fabric.libraryVersion, quilt.libraryVersion));
for (InstallerItem fabric : new InstallerItem[]{fabric, fabricApi}) {
fabric.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (forge.libraryVersion.get() != null) return FORGE.getPatchId();
if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId();
if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId();
if (forge.libraryVersion.get() != null) return FORGE.getPatchId();
if (quilt.libraryVersion.get() != null) return QUILT.getPatchId();
if (quiltApi.libraryVersion.get() != null) return QUILT_API.getPatchId();
return null;
}, optiFine.libraryVersion, forge.libraryVersion));
}, forge.libraryVersion, liteLoader.libraryVersion, optiFine.libraryVersion, quilt.libraryVersion, quiltApi.libraryVersion));
}
fabricApi.dependencyName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() == null) return FABRIC.getPatchId();
else return null;
}, fabric.libraryVersion));
for (InstallerItem quilt : new InstallerItem[]{quilt, quiltApi}) {
quilt.incompatibleLibraryName.bind(Bindings.createStringBinding(() -> {
if (fabric.libraryVersion.get() != null) return FABRIC.getPatchId();
if (fabricApi.libraryVersion.get() != null) return FABRIC_API.getPatchId();
if (forge.libraryVersion.get() != null) return FORGE.getPatchId();
if (liteLoader.libraryVersion.get() != null) return LITELOADER.getPatchId();
if (optiFine.libraryVersion.get() != null) return OPTIFINE.getPatchId();
return null;
}, fabric.libraryVersion, fabricApi.libraryVersion, forge.libraryVersion, liteLoader.libraryVersion, optiFine.libraryVersion));
}
quiltApi.dependencyName.bind(Bindings.createStringBinding(() -> {
if (quilt.libraryVersion.get() == null) return QUILT.getPatchId();
else return null;
}, quilt.libraryVersion));
}
public InstallerItem[] getLibraries() {
return new InstallerItem[]{game, forge, liteLoader, optiFine, fabric, fabricApi};
return new InstallerItem[]{game, forge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
}
}
public static class InstallerItemSkin extends SkinBase<InstallerItem> {
private static final PseudoClass LIST_ITEM = PseudoClass.getPseudoClass("list-item");
private static final PseudoClass CARD = PseudoClass.getPseudoClass("card");
InstallerItemSkin(InstallerItem control) {
super(control);
HBox hbox = new HBox();
hbox.getStyleClass().add("md-list-cell");
hbox.setPadding(new Insets(8));
RipplerContainer container = new RipplerContainer(hbox);
Pane pane;
if (control.style == Style.CARD) {
pane = new VBox();
} else {
pane = new HBox();
}
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) {
ImageView view = new ImageView(new Image(control.imageUrl, 32, 32, true, true));
Node node = FXUtils.limitingSize(view, 32, 32);
node.setMouseTransparent(true);
hbox.getChildren().add(node);
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();
nameLabel.getStyleClass().add("installer-item-name");
nameLabel.setMouseTransparent(true);
hbox.getChildren().add(nameLabel);
nameLabel.setPrefWidth(80);
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();
label.setMouseTransparent(true);
hbox.getChildren().add(label);
label.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(label, Priority.ALWAYS);
label.textProperty().bind(Bindings.createStringBinding(() -> {
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(() -> {
String incompatibleWith = control.incompatibleLibraryName.get();
String version = control.libraryVersion.get();
if (control.incompatibleWithGame.get()) {
@@ -193,8 +243,13 @@ public class InstallerItem extends Control {
return i18n("install.installer.version", version);
}
}, control.incompatibleLibraryName, control.incompatibleWithGame, control.libraryVersion));
BorderPane.setMargin(label, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
BorderPane.setMargin(statusLabel, new Insets(0, 0, 0, 8));
BorderPane.setAlignment(statusLabel, Pos.CENTER_LEFT);
HBox buttonsContainer = new HBox();
buttonsContainer.setSpacing(8);
buttonsContainer.setAlignment(Pos.CENTER);
pane.getChildren().add(buttonsContainer);
JFXButton closeButton = new JFXButton();
closeButton.setGraphic(SVG.close(Theme.blackFillBinding(), -1, -1));
@@ -202,7 +257,7 @@ public class InstallerItem extends Control {
closeButton.visibleProperty().bind(control.removable);
closeButton.managedProperty().bind(closeButton.visibleProperty());
closeButton.onMouseClickedProperty().bind(control.removeAction);
hbox.getChildren().add(closeButton);
buttonsContainer.getChildren().add(closeButton);
JFXButton arrowButton = new JFXButton();
arrowButton.graphicProperty().bind(Bindings.createObjectBinding(() -> control.upgradable.get()
@@ -215,16 +270,16 @@ public class InstallerItem extends Control {
control.installable, control.incompatibleLibraryName));
arrowButton.managedProperty().bind(arrowButton.visibleProperty());
arrowButton.onMouseClickedProperty().bind(control.action);
hbox.getChildren().add(arrowButton);
buttonsContainer.getChildren().add(arrowButton);
FXUtils.onChangeAndOperate(arrowButton.visibleProperty(), clickable -> {
if (clickable) {
container.onMouseClickedProperty().bind(control.action);
hbox.setCursor(Cursor.HAND);
pane.setCursor(Cursor.HAND);
} else {
container.onMouseClickedProperty().unbind();
container.onMouseClickedProperty().set(null);
hbox.setCursor(Cursor.DEFAULT);
pane.setCursor(Cursor.DEFAULT);
}
});
}
@@ -34,22 +34,18 @@ import javafx.scene.control.Label;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
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.*;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -98,8 +94,8 @@ public final class LogWindow extends Stage {
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
}
public void logLine(String line, Log4jLevel level) {
Log log = new Log(parseEscapeSequence(line), level);
public void logLine(String filteredLine, Log4jLevel level) {
Log log = new Log(parseEscapeSequence(filteredLine), level);
logs.add(log);
if (levelShownMap.get(level).get())
impl.listView.getItems().add(log);
@@ -154,12 +150,12 @@ public final class LogWindow extends Stage {
public 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 BooleanProperty showCrashReport = new SimpleBooleanProperty();
private final ListView<Log> listView = new JFXListView<>();
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
LogWindowImpl() {
getStyleClass().add("log-window");
@@ -206,13 +202,13 @@ public final class LogWindow extends Stage {
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);
});
}
@@ -228,13 +224,13 @@ 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 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 ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
ToggleButton button = new ToggleButton();
@@ -291,6 +287,7 @@ public final class LogWindow extends Stage {
listView.scrollTo(listView.getItems().size() - 1);
});
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
MutableObject<Object> lastCell = new MutableObject<>();
listView.setCellFactory(x -> new ListCell<Log>() {
{
getStyleClass().add("log-window-list-cell");
@@ -307,6 +304,12 @@ public final class LogWindow extends Stage {
@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.getValue() && !isVisible())
return;
lastCell.setValue(this);
pseudoClassStateChanged(EMPTY, empty);
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
@@ -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);
}
}
@@ -41,7 +41,7 @@ public class UpgradeDialog extends JFXDialogLayout {
}
{
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html#nowchange";
try {
WebView webView = new WebView();
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
@@ -24,11 +24,11 @@ import javafx.beans.value.WeakChangeListener;
import javafx.collections.ListChangeListener;
import javafx.collections.WeakListChangeListener;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
public class WeakListenerHolder {
private List<Object> refs = new LinkedList<>();
private final List<Object> refs = new ArrayList<>(0);
public WeakListenerHolder() {
}
@@ -22,9 +22,8 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
@@ -34,20 +33,18 @@ import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import static javafx.beans.binding.Bindings.createStringBinding;
import static org.jackhuang.hmcl.setting.Accounts.getAccountFactory;
import static org.jackhuang.hmcl.setting.Accounts.getLocalizedLoginTypeName;
import static org.jackhuang.hmcl.ui.FXUtils.toFXImage;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class AccountAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final ImageView imageView;
private final Canvas canvas;
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
private final ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
@Override
protected void invalidated() {
@@ -55,17 +52,19 @@ public class AccountAdvancedListItem extends AdvancedListItem {
if (account == null) {
titleProperty().unbind();
subtitleProperty().unbind();
imageView.imageProperty().unbind();
tooltip.textProperty().unbind();
setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add"));
imageView.setImage(toFXImage(TexturesLoader.toAvatar(TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage(), 32)));
tooltip.setText(i18n("account.create"));
TexturesLoader.unbindAvatar(canvas);
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage());
} else {
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
subtitleProperty().bind(accountSubtitle(account));
imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
tooltip.textProperty().bind(accountTooltip(account));
TexturesLoader.bindAvatar(canvas, account);
}
}
};
@@ -74,9 +73,8 @@ public class AccountAdvancedListItem extends AdvancedListItem {
tooltip = new Tooltip();
FXUtils.installFastTooltip(this, tooltip);
Pair<Node, ImageView> view = createImageView(null);
setLeftGraphic(view.getKey());
imageView = view.getValue();
canvas = new Canvas(32, 32);
setLeftGraphic(canvas);
setActionButtonVisible(false);
@@ -20,30 +20,28 @@ package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import javafx.scene.image.Image;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CredentialExpiredException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogController;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
@@ -68,7 +66,6 @@ public class AccountListItem extends RadioButton {
private final Account account;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
public AccountListItem(Account account) {
this.account = account;
@@ -93,8 +90,6 @@ public class AccountListItem extends RadioButton {
account.getUsername().isEmpty() ? characterName :
Bindings.concat(account.getUsername(), " - ", characterName));
}
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
}
@Override
@@ -137,7 +132,7 @@ public class AccountListItem extends RadioButton {
} else {
return createBooleanBinding(() -> true);
}
} else if (account instanceof OfflineAccount) {
} else if (account instanceof OfflineAccount || account instanceof MicrosoftAccount) {
return createBooleanBinding(() -> true);
} else {
return createBooleanBinding(() -> false);
@@ -153,6 +148,10 @@ public class AccountListItem extends RadioButton {
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
return null;
}
if (account instanceof MicrosoftAccount) {
FXUtils.openLink("https://www.minecraft.net/msaprofile/mygames/editskin");
return null;
}
if (!(account instanceof YggdrasilAccount)) {
return null;
}
@@ -220,16 +219,4 @@ public class AccountListItem extends RadioButton {
public StringProperty subtitleProperty() {
return subtitle;
}
public Image getImage() {
return image.get();
}
public void setImage(Image image) {
this.image.set(image);
}
public ObjectProperty<Image> imageProperty() {
return image;
}
}
@@ -21,15 +21,16 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
@@ -64,9 +65,8 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
center.setSpacing(8);
center.setAlignment(Pos.CENTER_LEFT);
ImageView imageView = new ImageView();
FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().bind(skinnable.imageProperty());
Canvas canvas = new Canvas(32, 32);
TexturesLoader.bindAvatar(canvas, skinnable.getAccount());
Label title = new Label();
title.getStyleClass().add("title");
@@ -84,7 +84,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
item.getStyleClass().add("two-line-list-item");
BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item);
center.getChildren().setAll(canvas, item);
root.setCenter(center);
HBox right = new HBox();
@@ -45,8 +45,10 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import java.net.URI;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
@@ -122,6 +124,14 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
microsoftItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_MICROSOFT)));
boxMethods.getChildren().add(microsoftItem);
AdvancedListItem littleSkinItem = new AdvancedListItem();
littleSkinItem.getStyleClass().add("navigation-drawer-item");
littleSkinItem.setActionButtonVisible(false);
littleSkinItem.setTitle(i18n("account.methods.little_skin"));
littleSkinItem.setLeftGraphic(wrap(SVG::server));
littleSkinItem.setOnAction(e -> Controllers.dialog(new CreateAccountPane(Accounts.FACTORY_LITTLE_SKIN)));
boxMethods.getChildren().add(littleSkinItem);
VBox boxAuthServers = new VBox();
authServerItems = MappedObservableList.create(skinnable.authServersProperty(), server -> {
AdvancedListItem item = new AdvancedListItem();
@@ -140,7 +150,13 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
item.titleProperty().bind(title);
item.subtitleProperty().set(URI.create(server.getUrl()).getHost());
String host = "";
try {
host = URI.create(server.getUrl()).getHost();
} catch (IllegalArgumentException e) {
LOG.log(Level.WARNING, "Unparsable authlib-injector server url " + server.getUrl(), e);
}
item.subtitleProperty().set(host);
Tooltip tooltip = new Tooltip();
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
FXUtils.installFastTooltip(item, tooltip);
@@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXTextField;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.task.Schedulers;
@@ -67,6 +68,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
loadFXML(this, "/assets/fxml/authlib-injector-server-add.fxml");
root.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
lblCreationWarning.maxWidthProperty().bind(((FlowPane) lblCreationWarning.getParent()).widthProperty());
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
nextPane.hideSpinner();
@@ -30,16 +30,17 @@ import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TextInputControl;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.NoSelectedCharacterException;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.authlibinjector.BoundAuthlibInjectorAccountFactory;
import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccountFactory;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
@@ -194,9 +195,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
setPrefWidth(560);
}
public CreateAccountPane(AuthlibInjectorServer authserver) {
this(Accounts.FACTORY_AUTHLIB_INJECTOR);
((AccountDetailsInputPane) detailsPane).selectAuthServer(authserver);
public CreateAccountPane(AuthlibInjectorServer authServer) {
this(Accounts.getAccountFactoryByAuthlibInjectorServer(authServer));
}
private void onAccept() {
@@ -267,39 +267,72 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
if (factory == Accounts.FACTORY_MICROSOFT) {
VBox vbox = new VBox(8);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
}
});
hintPane.setOnMouseClicked(e -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
});
if (!Accounts.OAUTH_CALLBACK.getClientId().isEmpty()) {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
}
});
hintPane.setOnMouseClicked(e -> {
if (deviceCode.get() != null) {
FXUtils.copyText(deviceCode.get().getUserCode());
}
});
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value -> {
runInFX(() -> deviceCode.set(value));
}));
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value -> {
runInFX(() -> deviceCode.set(value));
}));
FlowPane box = new FlowPane();
box.setHgap(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a"));
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
deauthorizeLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A"));
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
forgotpasswordLink.setOnAction(e -> FXUtils.openLink("https://www.minecraft.net/password/forgot"));
JFXHyperlink createProfileLink = new JFXHyperlink(i18n("account.methods.microsoft.makegameidsettings"));
createProfileLink.setOnAction(e -> FXUtils.openLink("https://www.minecraft.net/msaprofile/mygames/editprofile"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, forgotpasswordLink, createProfileLink);
GridPane.setColumnSpan(box, 2);
HBox box = new HBox(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a"));
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
box.getChildren().setAll(profileLink, birthLink, purchaseLink);
GridPane.setColumnSpan(box, 2);
vbox.getChildren().setAll(hintPane, box);
vbox.getChildren().setAll(hintPane, box);
btnAccept.setDisable(false);
} else {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setText(i18n("account.methods.microsoft.snapshot"));
vbox.getChildren().setAll(hintPane);
btnAccept.setDisable(true);
}
detailsPane = vbox;
btnAccept.setDisable(false);
} else if (factory == Accounts.FACTORY_MOJANG) {
VBox vbox = new VBox(8);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING);
hintPane.setText(i18n("account.methods.yggdrasil.migration.hint"));
HBox linkPane = new HBox(8);
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
migrationLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PROFILE_URL));
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
migrationHowLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.MIGRATION_FAQ_URL));
linkPane.getChildren().setAll(migrationLink, migrationHowLink);
vbox.getChildren().setAll(hintPane, linkPane);
detailsPane = vbox;
btnAccept.setDisable(true);
} else {
detailsPane = new AccountDetailsInputPane(factory, btnAccept::fire);
btnAccept.disableProperty().bind(((AccountDetailsInputPane) detailsPane).validProperty().not());
@@ -310,7 +343,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
private static class AccountDetailsInputPane extends GridPane {
// ==== authlib-injector hyperlinks ====
private static final String[] ALLOWED_LINKS = { "register" };
private static final String[] ALLOWED_LINKS = { "homepage", "register" };
private static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
if (server == null) {
@@ -332,12 +365,13 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
// =====
private AccountFactory<?> factory;
private final AccountFactory<?> factory;
private @Nullable AuthlibInjectorServer server;
private @Nullable JFXComboBox<AuthlibInjectorServer> cboServers;
private @Nullable JFXTextField txtUsername;
private @Nullable JFXPasswordField txtPassword;
private @Nullable JFXTextField txtUUID;
private BooleanBinding valid;
private final BooleanBinding valid;
public AccountDetailsInputPane(AccountFactory<?> factory, Runnable onAction) {
this.factory = factory;
@@ -355,7 +389,28 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
int rowIndex = 0;
if (factory instanceof AuthlibInjectorAccountFactory) {
if (factory instanceof BoundAuthlibInjectorAccountFactory) {
this.server = ((BoundAuthlibInjectorAccountFactory) factory).getServer();
Label lblServers = new Label(i18n("account.injector.server"));
setHalignment(lblServers, HPos.LEFT);
add(lblServers, 0, rowIndex);
Label lblServerName = new Label(this.server.getName());
lblServerName.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(lblServerName, Priority.ALWAYS);
HBox linksContainer = new HBox();
linksContainer.setAlignment(Pos.CENTER);
linksContainer.getChildren().setAll(createHyperlinks(this.server));
linksContainer.setMinWidth(USE_PREF_SIZE);
HBox boxServers = new HBox(lblServerName, linksContainer);
boxServers.setAlignment(Pos.CENTER_LEFT);
add(boxServers, 1, rowIndex);
rowIndex++;
} else if (factory instanceof AuthlibInjectorAccountFactory) {
Label lblServers = new Label(i18n("account.injector.server"));
setHalignment(lblServers, HPos.LEFT);
add(lblServers, 0, rowIndex);
@@ -378,7 +433,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
HBox linksContainer = new HBox();
linksContainer.setAlignment(Pos.CENTER);
onChangeAndOperate(cboServers.valueProperty(), server -> linksContainer.getChildren().setAll(createHyperlinks(server)));
onChangeAndOperate(cboServers.valueProperty(), server -> {
this.server = server;
linksContainer.getChildren().setAll(createHyperlinks(server));
});
linksContainer.setMinWidth(USE_PREF_SIZE);
JFXButton btnAddServer = new JFXButton();
@@ -435,10 +493,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
GridPane.setColumnSpan(box, 2);
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
migrationLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PROFILE_URL));
migrationLink.setOnAction(e -> FXUtils.openLink("https://aka.ms/MinecraftMigration"));
JFXHyperlink migrationHowLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration.how"));
migrationHowLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.MIGRATION_FAQ_URL));
migrationHowLink.setOnAction(e -> FXUtils.openLink("https://help.minecraft.net/hc/articles/4411173197709"));
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
@@ -450,6 +508,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
if (factory instanceof OfflineAccountFactory) {
txtUsername.setPromptText(i18n("account.methods.offline.name.special_characters"));
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
HBox linkPane = new HBox(purchaseLink);
@@ -523,11 +583,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
private boolean requiresEmailAsUsername() {
if (factory instanceof YggdrasilAccountFactory) {
return true;
} else if ((factory instanceof AuthlibInjectorAccountFactory) && cboServers != null) {
AuthlibInjectorServer server = cboServers.getValue();
if (server != null && !server.isNonEmailLogin()) {
return true;
}
} else if ((factory instanceof AuthlibInjectorAccountFactory) && this.server != null) {
return !server.isNonEmailLogin();
}
return false;
}
@@ -544,7 +601,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
public @Nullable AuthlibInjectorServer getAuthServer() {
return cboServers == null ? null : cboServers.getValue();
return this.server;
}
public @Nullable String getUsername() {
@@ -559,10 +616,6 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
return valid;
}
public void selectAuthServer(AuthlibInjectorServer authserver) {
cboServers.getSelectionModel().select(authserver);
}
public void focus() {
if (txtUsername != null) {
txtUsername.requestFocus();
@@ -601,12 +654,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
Platform.runLater(() -> {
for (GameProfile profile : profiles) {
ImageView portraitView = new ImageView();
portraitView.setSmooth(false);
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
FXUtils.limitSize(portraitView, 32, 32);
Canvas portraitCanvas = new Canvas(32, 32);
TexturesLoader.bindAvatar(portraitCanvas, service, profile.getId());
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
IconedItem accountItem = new IconedItem(portraitCanvas, profile.getName());
accountItem.setOnMouseClicked(e -> {
selectedProfile = profile;
latch.countDown();
@@ -671,5 +722,5 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
}
}
private static final String MICROSOFT_ACCOUNT_EDIT_PROFILE_URL = "https://support.microsoft.com/zh-cn/account-billing/%E5%A6%82%E4%BD%95%E6%9B%B4%E6%94%B9-microsoft-%E5%B8%90%E6%88%B7%E4%B8%8A%E7%9A%84%E5%87%BA%E7%94%9F%E6%97%A5%E6%9C%9F-837badbc-999e-54d2-2617-d19206b9540a";
private static final String MICROSOFT_ACCOUNT_EDIT_PROFILE_URL = "https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a";
}
@@ -48,9 +48,9 @@ public class OAuthAccountLoginDialog extends DialogPane {
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
if (deviceCode != null) {
FXUtils.copyText(deviceCode.getUserCode());
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
hintPane.setSegment(i18n("account.login.refresh.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
} else {
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
hintPane.setSegment(i18n("account.login.refresh.microsoft.hint"));
}
});
hintPane.setOnMouseClicked(e -> {
@@ -61,7 +61,7 @@ public class OAuthAccountLoginDialog extends DialogPane {
HBox box = new HBox(8);
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a"));
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/account-billing/how-to-change-a-birth-date-on-a-microsoft-account-837badbc-999e-54d2-2617-d19206b9540a"));
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
@@ -76,7 +76,7 @@ public class OfflineAccountSkinPane extends StackPane {
canvasPane.setPrefWidth(300);
canvasPane.setPrefHeight(300);
pane.setCenter(canvas);
canvas.getAnimationplayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas));
canvas.getAnimationPlayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas));
canvas.enableRotation(.5);
canvas.addEventHandler(DragEvent.DRAG_OVER, e -> {
@@ -136,11 +136,14 @@ public class OfflineAccountSkinPane extends StackPane {
LOG.log(Level.WARNING, "Failed to load skin", exception);
Controllers.showToast(i18n("message.failed"));
} else {
if (result == null || result.getSkin() == null) {
canvas.updateSkin(getDefaultTexture(), isDefaultSlim());
if (result == null || result.getSkin() == null && result.getCape() == null) {
canvas.updateSkin(getDefaultTexture(), isDefaultSlim(), null);
return;
}
canvas.updateSkin(new Image(result.getSkin().getInputStream()), result.getModel() == TextureModel.ALEX);
canvas.updateSkin(
result.getSkin() != null ? new Image(result.getSkin().getInputStream()) : getDefaultTexture(),
result.getModel() == TextureModel.ALEX,
result.getCape() != null ? new Image(result.getCape().getInputStream()) : null);
}
}).start();
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
@@ -183,7 +186,7 @@ public class OfflineAccountSkinPane extends StackPane {
});
JFXHyperlink littleSkinLink = new JFXHyperlink(i18n("account.skin.type.little_skin"));
littleSkinLink.setOnAction(e -> FXUtils.openLink("https://mcskin.littleservice.cn/"));
littleSkinLink.setOnAction(e -> FXUtils.openLink("https://littleskin.cn/"));
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
@@ -19,26 +19,37 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.function.Consumer;
public class AdvancedListBox extends VBox {
public class AdvancedListBox extends ScrollPane {
private final VBox container = new VBox();
{
getStyleClass().add("advanced-list-box-content");
setContent(container);
FXUtils.smoothScrolling(this);
setFitToHeight(true);
setFitToWidth(true);
setHbarPolicy(ScrollBarPolicy.NEVER);
container.getStyleClass().add("advanced-list-box-content");
}
public AdvancedListBox add(Node child) {
if (child instanceof Pane || child instanceof AdvancedListItem)
getChildren().add(child);
container.getChildren().add(child);
else {
StackPane pane = new StackPane();
pane.getStyleClass().add("advanced-list-box-item");
pane.getChildren().setAll(child);
getChildren().add(pane);
container.getChildren().add(pane);
}
return this;
}
@@ -53,27 +64,27 @@ public class AdvancedListBox extends VBox {
public AdvancedListBox add(int index, Node child) {
if (child instanceof Pane || child instanceof AdvancedListItem)
getChildren().add(index, child);
container.getChildren().add(index, child);
else {
StackPane pane = new StackPane();
pane.getStyleClass().add("advanced-list-box-item");
pane.getChildren().setAll(child);
getChildren().add(index, pane);
container.getChildren().add(index, pane);
}
return this;
}
public AdvancedListBox remove(Node child) {
getChildren().remove(indexOf(child));
container.getChildren().remove(indexOf(child));
return this;
}
public int indexOf(Node child) {
if (child instanceof Pane) {
return getChildren().indexOf(child);
return container.getChildren().indexOf(child);
} else {
for (int i = 0; i < getChildren().size(); ++i) {
Node node = getChildren().get(i);
for (int i = 0; i < container.getChildren().size(); ++i) {
Node node = container.getChildren().get(i);
if (node instanceof StackPane) {
ObservableList<Node> list = ((StackPane) node).getChildren();
if (list.size() == 1 && list.get(0) == child)
@@ -88,7 +99,11 @@ public class AdvancedListBox extends VBox {
return add(new ClassTitle(category));
}
public void setSpacing(double spacing) {
container.setSpacing(spacing);
}
public void clear() {
getChildren().clear();
container.getChildren().clear();
}
}
@@ -28,11 +28,11 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
@@ -115,12 +115,17 @@ public class ComponentList extends Control {
expanded = true;
}
@Override
public Orientation getContentBias() {
return Orientation.HORIZONTAL;
}
@Override
protected javafx.scene.control.Skin<?> createDefaultSkin() {
return new Skin(this);
}
protected static class Skin extends SkinBase<ComponentList> {
protected static class Skin extends ControlSkinBase<ComponentList> {
private static final PseudoClass PSEUDO_CLASS_FIRST = PseudoClass.getPseudoClass("first");
private static final PseudoClass PSEUDO_CLASS_LAST = PseudoClass.getPseudoClass("last");
@@ -166,7 +171,7 @@ public class ComponentList extends Control {
VBox vbox = new VBox();
vbox.setFillWidth(true);
Bindings.bindContent(vbox.getChildren(), list);
getChildren().setAll(vbox);
node = vbox;
}
}
@@ -0,0 +1,55 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import java.util.Objects;
public abstract class ControlSkinBase<C extends Control> implements Skin<C> {
private final C control;
protected Node node;
/**
* Constructor for all SkinBase instances.
*
* @param control The control for which this Skin should attach to.
*/
protected ControlSkinBase(C control) {
this.control = control;
}
@Override
public C getSkinnable() {
return control;
}
@Override
public Node getNode() {
Objects.requireNonNull(node);
return node;
}
@Override
public void dispose() {
}
}
@@ -34,7 +34,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DialogPane extends JFXDialogLayout {
private final StringProperty title = new SimpleStringProperty();
private final BooleanProperty valid = new SimpleBooleanProperty();
private final BooleanProperty valid = new SimpleBooleanProperty(true);
protected final SpinnerPane acceptPane = new SpinnerPane();
protected final JFXButton cancelButton = new JFXButton();
protected final Label warningLabel = new Label();
@@ -0,0 +1,57 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
public class DoubleValidator extends ValidatorBase {
private final boolean nullable;
public DoubleValidator() {
this(false);
}
public DoubleValidator(@NamedArg("nullable") boolean nullable) {
this.nullable = nullable;
}
public DoubleValidator(@NamedArg("message") String message, @NamedArg("nullable") boolean nullable) {
super(message);
this.nullable = nullable;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
evalTextInputField();
}
}
private void evalTextInputField() {
TextInputControl textField = ((TextInputControl) srcControl.get());
if (StringUtils.isBlank(textField.getText()))
hasErrors.set(!nullable);
else
hasErrors.set(Lang.toDoubleOrNull(textField.getText()) == null);
}
}
@@ -22,6 +22,7 @@ import javafx.css.PseudoClass;
import javafx.scene.control.ListCell;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.ui.FXUtils;
public abstract class MDListCell<T> extends ListCell<T> {
@@ -29,8 +30,11 @@ public abstract class MDListCell<T> extends ListCell<T> {
private final StackPane container = new StackPane();
private final StackPane root = new StackPane();
private final MutableObject<Object> lastCell;
public MDListCell(JFXListView<T> listView, MutableObject<Object> lastCell) {
this.lastCell = lastCell;
public MDListCell(JFXListView<T> listView) {
setText(null);
setGraphic(null);
@@ -50,6 +54,14 @@ public abstract class MDListCell<T> extends ListCell<T> {
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (lastCell != null) {
if (this == lastCell.getValue() && !isVisible())
return;
lastCell.setValue(this);
}
updateControl(item, empty);
if (empty) {
setGraphic(null);
@@ -23,9 +23,14 @@ import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.Lang;
@@ -55,7 +60,7 @@ public final class MessageDialogPane extends StackPane {
}
@FXML
private Label content;
private StackPane content;
@FXML
private Label graphic;
@FXML
@@ -68,7 +73,17 @@ public final class MessageDialogPane extends StackPane {
public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull MessageType type) {
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
content.setText(text);
EnhancedTextFlow textFlow = new EnhancedTextFlow();
textFlow.getChildren().setAll(FXUtils.parseSegment(text, Controllers::onHyperlinkAction));
if (textFlow.computePrefHeight(400.0) <= 350.0)
content.getChildren().setAll(textFlow);
else {
ScrollPane scrollPane = new ScrollPane(textFlow);
VBox.setVgrow(scrollPane, Priority.ALWAYS);
scrollPane.setFitToWidth(true);
content.getChildren().setAll(scrollPane);
}
if (title != null)
this.title.setText(title);
@@ -113,6 +128,13 @@ public final class MessageDialogPane extends StackPane {
return cancelButton;
}
private static final class EnhancedTextFlow extends TextFlow {
@Override
public double computePrefHeight(double width) {
return super.computePrefHeight(width);
}
}
public static class Builder {
private final MessageDialogPane dialog;
@@ -122,10 +144,11 @@ public final class MessageDialogPane extends StackPane {
public Builder addAction(Node actionNode) {
dialog.addButton(actionNode);
actionNode.getStyleClass().add("dialog-accept");
return this;
}
public Builder ok(Runnable ok) {
public Builder ok(@Nullable Runnable ok) {
JFXButton btnOk = new JFXButton(i18n("button.ok"));
btnOk.getStyleClass().add("dialog-accept");
if (ok != null) {
@@ -136,7 +159,22 @@ public final class MessageDialogPane extends StackPane {
return this;
}
public Builder yesOrNo(Runnable yes, Runnable no) {
public Builder addCancel(@Nullable Runnable cancel) {
return addCancel(i18n("button.cancel"), cancel);
}
public Builder addCancel(String cancelText, @Nullable Runnable cancel) {
JFXButton btnCancel = new JFXButton(cancelText);
btnCancel.getStyleClass().add("dialog-cancel");
if (cancel != null) {
btnCancel.setOnAction(e -> cancel.run());
}
dialog.addButton(btnCancel);
dialog.setCancelButton(btnCancel);
return this;
}
public Builder yesOrNo(@Nullable Runnable yes, @Nullable Runnable no) {
JFXButton btnYes = new JFXButton(i18n("button.yes"));
btnYes.getStyleClass().add("dialog-accept");
if (yes != null) {
@@ -144,27 +182,14 @@ public final class MessageDialogPane extends StackPane {
}
dialog.addButton(btnYes);
JFXButton btnNo = new JFXButton(i18n("button.no"));
btnNo.getStyleClass().add("dialog-cancel");
if (no != null) {
btnNo.setOnAction(e -> no.run());
}
dialog.addButton(btnNo);
dialog.setCancelButton(btnNo);
addCancel(i18n("button.no"), no);
return this;
}
public Builder actionOrCancel(ButtonBase actionButton, Runnable cancel) {
dialog.addButton(actionButton);
JFXButton btnCancel = new JFXButton(i18n("button.cancel"));
btnCancel.getStyleClass().add("dialog-cancel");
if (cancel != null) {
btnCancel.setOnAction(e -> cancel.run());
}
dialog.addButton(btnCancel);
dialog.setCancelButton(btnCancel);
addCancel(cancel);
return this;
}
@@ -28,7 +28,6 @@ import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
@@ -165,19 +164,12 @@ public class MultiFileItem<T> extends VBox {
pane.setLeft(left);
if (StringUtils.isNotBlank(subtitle)) {
Optional<String> shortSubtitle = StringUtils.truncate(subtitle);
Label right;
if (shortSubtitle.isPresent()) {
right = new Label(shortSubtitle.get());
right.setTooltip(new Tooltip(subtitle));
} else {
right = new Label(subtitle);
}
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
right.setWrapText(true);
right.getStyleClass().add("subtitle-label");
right.setStyle("-fx-font-size: 10;");
pane.setRight(right);
Label center = new Label(subtitle);
BorderPane.setAlignment(center, Pos.CENTER_RIGHT);
center.setWrapText(true);
center.getStyleClass().add("subtitle-label");
center.setStyle("-fx-font-size: 10;");
pane.setCenter(center);
}
return pane;
@@ -50,8 +50,9 @@ public class OptionToggleButton extends StackPane {
titleLabel.textProperty().bind(title);
Label subtitleLabel = new Label();
subtitleLabel.setMouseTransparent(true);
subtitleLabel.setWrapText(true);
subtitleLabel.textProperty().bind(subtitle);
pane.setLeft(left);
pane.setCenter(left);
left.setAlignment(Pos.CENTER_LEFT);
JFXToggleButton toggleButton = new JFXToggleButton();
@@ -0,0 +1,62 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ServerAddressValidator extends ValidatorBase {
private final boolean nullable;
public ServerAddressValidator() {
this(false);
}
public ServerAddressValidator(@NamedArg("nullable") boolean nullable) {
this(i18n("input.url"), nullable);
}
public ServerAddressValidator(@NamedArg("message") String message, @NamedArg("nullable") boolean nullable) {
super(message);
this.nullable = nullable;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
evalTextInputField();
}
}
private static final Pattern PATTERN = Pattern.compile("[-a-zA-Z0-9@:%._+~#=]{1,256}(:\\d+)?");
private void evalTextInputField() {
TextInputControl textField = ((TextInputControl) srcControl.get());
if (StringUtils.isBlank(textField.getText()))
hasErrors.set(!nullable);
else
hasErrors.set(!PATTERN.matcher(textField.getText()).matches());
}
}
@@ -22,12 +22,13 @@ import javafx.application.Platform;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.function.Consumer;
@@ -37,7 +38,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class TaskExecutorDialogPane extends StackPane {
private TaskExecutor executor;
private Consumer<Region> onCancel;
private TaskCancellationAction onCancel;
private final Consumer<FileDownloadTask.SpeedEvent> speedEventHandler;
@FXML
@@ -49,14 +50,14 @@ public class TaskExecutorDialogPane extends StackPane {
@FXML
private TaskListPane taskListPane;
public TaskExecutorDialogPane(Consumer<Region> cancel) {
public TaskExecutorDialogPane(@NotNull TaskCancellationAction cancel) {
FXUtils.loadFXML(this, "/assets/fxml/task-dialog.fxml");
setCancel(cancel);
btnCancel.setOnAction(e -> {
Optional.ofNullable(executor).ifPresent(TaskExecutor::cancel);
onCancel.accept(this);
onCancel.getCancellationAction().accept(this);
});
speedEventHandler = speedEvent -> {
@@ -113,7 +114,7 @@ public class TaskExecutorDialogPane extends StackPane {
lblTitle.setText(currentState);
}
public void setCancel(Consumer<Region> onCancel) {
public void setCancel(TaskCancellationAction onCancel) {
this.onCancel = onCancel;
runInFX(() -> btnCancel.setDisable(onCancel == null));
@@ -34,6 +34,7 @@ import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.java.JavaDownloadTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
@@ -42,10 +43,15 @@ import org.jackhuang.hmcl.mod.ModpackInstallTask;
import org.jackhuang.hmcl.mod.ModpackUpdateTask;
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
import org.jackhuang.hmcl.mod.curse.CurseInstallTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthCompletionTask;
import org.jackhuang.hmcl.mod.modrinth.ModrinthInstallTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackExportTask;
import org.jackhuang.hmcl.mod.multimc.MultiMCModpackInstallTask;
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
import org.jackhuang.hmcl.mod.server.ServerModpackExportTask;
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -123,8 +129,8 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("install.installer.install", i18n("install.installer.fabric")));
} else if (task instanceof FabricAPIInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.fabric-api")));
} else if (task instanceof CurseCompletionTask) {
task.setName(i18n("modpack.type.curse.completion"));
} else if (task instanceof CurseCompletionTask || task instanceof ModrinthCompletionTask || task instanceof ServerModpackCompletionTask || task instanceof McbbsModpackCompletionTask) {
task.setName(i18n("modpack.completion"));
} else if (task instanceof ModpackInstallTask) {
task.setName(i18n("modpack.installing"));
} else if (task instanceof ModpackUpdateTask) {
@@ -133,12 +139,18 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.install", i18n("modpack.type.curse")));
} else if (task instanceof MultiMCModpackInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.multimc")));
} else if (task instanceof ModrinthInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.modrinth")));
} else if (task instanceof ServerModpackLocalInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.server")));
} else if (task instanceof HMCLModpackInstallTask) {
task.setName(i18n("modpack.install", i18n("modpack.type.hmcl")));
} else if (task instanceof McbbsModpackExportTask || task instanceof MultiMCModpackExportTask || task instanceof ServerModpackExportTask) {
task.setName(i18n("modpack.export"));
} else if (task instanceof MinecraftInstanceTask) {
task.setName(i18n("modpack.scan"));
} else if (task instanceof JavaDownloadTask) {
task.setName(i18n("download.java"));
}
Platform.runLater(() -> {
@@ -186,7 +198,7 @@ public final class TaskListPane extends StackPane {
if (task instanceof Task.CountTask) {
runInFX(() -> {
stageNodes.stream()
.filter(x -> x.stage.equals(((Task.CountTask) task).getCountStage()))
.filter(x -> x.stage.equals(((Task<?>.CountTask) task).getCountStage()))
.findAny()
.ifPresent(StageNode::count);
});
@@ -234,6 +246,7 @@ public final class TaskListPane extends StackPane {
case "hmcl.install.optifine": message = i18n("install.installer.install", i18n("install.installer.optifine") + " " + stageValue); break;
case "hmcl.install.fabric": message = i18n("install.installer.install", i18n("install.installer.fabric") + " " + stageValue); break;
case "hmcl.install.fabric-api": message = i18n("install.installer.install", i18n("install.installer.fabric-api") + " " + stageValue); break;
case "hmcl.install.quilt": message = i18n("install.installer.install", i18n("install.installer.quilt") + " " + stageValue); break;
default: message = i18n(stageKey); break;
}
// @formatter:on
@@ -1,3 +1,20 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2022 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
@@ -163,10 +163,10 @@ public class DecoratorController {
config().backgroundImageUrlProperty()));
}
private Image defaultBackground = newImage("/assets/img/background.jpg");
private Image defaultBackground;
/**
* Load background image from bg/, background.png, background.jpg
* Load background image from bg/, background.png, background.jpg, background.gif
*/
private Image loadDefaultBackgroundImage() {
Optional<Image> image = randomImageIn(Paths.get("bg"));
@@ -176,7 +176,15 @@ public class DecoratorController {
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.jpg"));
}
return image.orElse(defaultBackground);
if (!image.isPresent()) {
image = tryLoadImage(Paths.get("background.gif"));
}
return image.orElseGet(() -> {
if (defaultBackground == null)
defaultBackground = newImage("/assets/img/background.jpg");
return defaultBackground;
});
}
private Optional<Image> randomImageIn(Path imageDir) {
@@ -190,7 +198,7 @@ public class DecoratorController {
.filter(Files::isReadable)
.filter(it -> {
String ext = getExtension(it).toLowerCase();
return ext.equals("png") || ext.equals("jpg");
return ext.equals("png") || ext.equals("jpg") || ext.equals("gif");
})
.collect(toList());
} catch (IOException e) {
@@ -91,9 +91,11 @@ class AdditionalInstallersPage extends InstallersPage {
String optiFine = analyzer.getVersion(OPTIFINE).orElse(null);
String fabric = analyzer.getVersion(FABRIC).orElse(null);
String fabricApi = analyzer.getVersion(FABRIC_API).orElse(null);
String quilt = analyzer.getVersion(QUILT).orElse(null);
String quiltApi = analyzer.getVersion(QUILT_API).orElse(null);
InstallerItem[] libraries = group.getLibraries();
String[] versions = new String[]{game, forge, liteLoader, optiFine, fabric, fabricApi};
String[] versions = new String[]{game, forge, liteLoader, optiFine, fabric, fabricApi, quilt, quiltApi};
String currentGameVersion = Lang.nonNull(getVersion("game"), game);
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -40,17 +39,15 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TabControl;
import org.jackhuang.hmcl.ui.construct.TabHeader;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.DownloadListPage;
import org.jackhuang.hmcl.ui.versions.ModDownloadListPage;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.ui.versions.*;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
@@ -83,7 +80,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
"game", versionPageNavigator::onGameSelected)));
modpackTab.setNodeSupplier(loadVersionFor(() -> {
DownloadListPage page = new DownloadListPage(CurseForgeRemoteModRepository.MODPACKS, Versions::downloadModpackImpl);
ModpackDownloadListPage page = new ModpackDownloadListPage(Versions::downloadModpackImpl, false);
JFXButton installLocalModpackButton = new JFXButton(i18n("install.modpack"));
installLocalModpackButton.setButtonType(JFXButton.ButtonType.RAISED);
@@ -132,19 +129,19 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
item.setTitle(i18n("resourcepack"));
item.setLeftGraphic(wrap(SVG::textureBox));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab));
item.setOnAction(e -> tab.select(resourcePackTab));
item.setOnAction(e -> selectTabIfCurseForgeAvailable(resourcePackTab));
})
// .addNavigationDrawerItem(item -> {
// item.setTitle(i18n("download.curseforge.customization"));
// item.setLeftGraphic(wrap(SVG::script));
// item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(customizationTab));
// item.setOnAction(e -> tab.select(customizationTab));
// item.setOnAction(e -> selectTabIfCurseForgeAvailable(customizationTab));
// })
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("world"));
item.setLeftGraphic(wrap(SVG::earth));
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(worldTab));
item.setOnAction(e -> tab.select(worldTab));
item.setOnAction(e -> selectTabIfCurseForgeAvailable(worldTab));
});
FXUtils.setLimitWidth(sideBar, 200);
setLeft(sideBar);
@@ -153,7 +150,14 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
setCenter(transitionPane);
}
private <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
private void selectTabIfCurseForgeAvailable(TabControl.Tab<?> newTab) {
if (CurseForgeRemoteModRepository.isAvailable())
tab.select(newTab);
else
Controllers.dialog(i18n("download.curseforge.unavailable"));
}
private static <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
return () -> {
T node = nodeSupplier.get();
if (node instanceof VersionPage.VersionLoadable) {
@@ -163,7 +167,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
};
}
private void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
private static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
if (version == null) version = profile.getSelectedVersion();
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
@@ -175,10 +179,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
}
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
TaskExecutorDialogPane downloadingPane = new TaskExecutorDialogPane(it -> {
});
TaskExecutor executor = Task.composeAsync(() -> {
Controllers.taskDialog(Task.composeAsync(() -> {
FileDownloadTask task = new FileDownloadTask(NetworkUtils.toURL(file.getFile().getUrl()), dest.toFile());
task.setName(file.getName());
return task;
@@ -192,11 +193,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
} else {
Controllers.showToast(i18n("install.success"));
}
}).executor(false);
downloadingPane.setExecutor(executor, true);
Controllers.dialog(downloadingPane);
executor.start();
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
resolve.run();
}, file.getFile().getFilename());
@@ -245,7 +242,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
tab.select(worldTab);
}
private class DownloadNavigator implements Navigation {
private static final class DownloadNavigator implements Navigation {
private final Map<String, Object> settings = new HashMap<>();
@Override
@@ -29,14 +29,13 @@ import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.Validator;
@@ -67,6 +66,10 @@ public class InstallersPage extends Control implements WizardPage {
group.game.installable.setValue(false);
for (InstallerItem item : group.getLibraries()) {
item.setStyleMode(InstallerItem.Style.CARD);
}
for (InstallerItem library : group.getLibraries()) {
String libraryId = library.getLibraryId();
if (libraryId.equals("game")) continue;
@@ -140,22 +143,23 @@ public class InstallersPage extends Control implements WizardPage {
BorderPane root = new BorderPane();
root.setPadding(new Insets(16));
ComponentList list = new ComponentList();
list.getStyleClass().add("no-padding");
root.setCenter(list);
{
HBox versionNamePane = new HBox(8);
versionNamePane.getStyleClass().add("card-non-transparent");
versionNamePane.setStyle("-fx-padding: 20 8 20 16");
versionNamePane.setAlignment(Pos.CENTER_LEFT);
versionNamePane.setPadding(new Insets(20, 8, 20, 16));
control.txtName.setMaxWidth(300);
versionNamePane.getChildren().setAll(new Label(i18n("archive.name")), control.txtName);
list.getContent().add(versionNamePane);
root.setTop(versionNamePane);
}
{
VBox libraryPane = new VBox(control.group.getLibraries());
list.getContent().add(libraryPane);
FlowPane libraryPane = new FlowPane(control.group.getLibraries());
BorderPane.setMargin(libraryPane, new Insets(16, 0, 16, 0));
libraryPane.setVgap(16);
libraryPane.setHgap(16);
root.setCenter(libraryPane);
}
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
import org.jackhuang.hmcl.mod.ModpackCompletionException;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
@@ -125,7 +125,7 @@ public class ModpackInstallWizardProvider implements WizardProvider {
settings.put("failure_callback", new FailureCallback() {
@Override
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
if (exception instanceof CurseCompletionException) {
if (exception instanceof ModpackCompletionException) {
if (exception.getCause() instanceof FileNotFoundException) {
Controllers.dialog(i18n("modpack.type.curse.not_found"), i18n("install.failed"), MessageType.ERROR, next);
} else {
@@ -23,6 +23,7 @@ import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.server.ServerModpackManifest;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.GetTask;
@@ -31,8 +32,8 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.io.IOException;
@@ -66,7 +67,7 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
Platform.runLater(controller::onNext);
}
FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
FXUtils.applyDragListener(this, ModpackHelper::isFileModpackByExtension, modpacks -> {
File modpack = modpacks.get(0);
controller.getSettings().put(MODPACK_FILE, modpack);
controller.onNext();
@@ -77,7 +78,7 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
private void onChooseLocalFile() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("modpack.choose"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("modpack"), "*.zip", "*.mrpack"));
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
if (selectedFile == null) {
Platform.runLater(controller::onEnd);
@@ -106,7 +107,7 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
} else {
reject.accept(e.getMessage());
}
}).executor(true), i18n("message.downloading"));
}).executor(true), i18n("message.downloading"), TaskCancellationAction.NORMAL);
} else {
// otherwise we still consider the file as modpack zip file
// since casually the url may not ends with ".zip"
@@ -124,7 +125,8 @@ public final class ModpackSelectionPage extends StackPane implements WizardPage
reject.accept(e.getMessage());
}
}).executor(true),
i18n("message.downloading")
i18n("message.downloading"),
TaskCancellationAction.NORMAL
);
}
} catch (IOException e) {
@@ -41,6 +41,7 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.zip.ZipException;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -173,6 +174,8 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
} else if (exception instanceof VersionMismatchException) {
VersionMismatchException e = ((VersionMismatchException) exception);
Controllers.dialog(i18n("install.failed.version_mismatch", e.getExpect(), e.getActual()), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
} else if (exception instanceof CancellationException) {
// Ignore cancel
} else {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR, next);
}
@@ -31,6 +31,7 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.VersionList;
@@ -40,7 +41,10 @@ import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
@@ -52,9 +56,9 @@ import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.Locales;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@@ -96,7 +100,7 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
@FXML
private StackPane center;
private VersionList<?> versionList;
private final VersionList<?> versionList;
private CompletableFuture<?> executor;
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
@@ -122,86 +126,9 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1)));
list.setCellFactory(listView -> new ListCell<RemoteVersion>() {
IconedTwoLineListItem content = new IconedTwoLineListItem();
RipplerContainer ripplerContainer = new RipplerContainer(content);
StackPane pane = new StackPane();
{
pane.getStyleClass().add("md-list-cell");
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
pane.getChildren().setAll(ripplerContainer);
}
@Override
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
super.updateItem(remoteVersion, empty);
if (empty) {
setGraphic(null);
return;
}
setGraphic(pane);
content.setTitle(remoteVersion.getSelfVersion());
if (remoteVersion.getReleaseDate() != null) {
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate()));
} else {
content.setSubtitle("");
}
if (remoteVersion instanceof GameRemoteVersion) {
switch (remoteVersion.getVersionType()) {
case RELEASE:
content.getTags().setAll(i18n("version.game.release"));
content.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
break;
case SNAPSHOT:
content.getTags().setAll(i18n("version.game.snapshot"));
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
break;
default:
content.getTags().setAll(i18n("version.game.old"));
content.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
break;
}
} else if (remoteVersion instanceof LiteLoaderRemoteVersion) {
content.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof OptiFineRemoteVersion) {
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof ForgeRemoteVersion) {
content.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof FabricRemoteVersion) {
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
} else if (remoteVersion instanceof FabricAPIRemoteVersion) {
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
if (StringUtils.isNotBlank(content.getSubtitle())) {
content.getTags().setAll(remoteVersion.getGameVersion());
} else {
content.setSubtitle(remoteVersion.getGameVersion());
}
}
}
});
MutableObject<RemoteVersionListCell> lastCell = new MutableObject<>();
EnumMap<VersionIconType, Image> icons = new EnumMap<>(VersionIconType.class);
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, icons));
list.setOnMouseClicked(e -> {
if (list.getSelectionModel().getSelectedIndex() < 0)
@@ -290,4 +217,86 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
private void onSponsor() {
HMCLService.openRedirectLink("bmclapi_sponsor");
}
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
final IconedTwoLineListItem content = new IconedTwoLineListItem();
final RipplerContainer ripplerContainer = new RipplerContainer(content);
final StackPane pane = new StackPane();
private final MutableObject<RemoteVersionListCell> lastCell;
private final EnumMap<VersionIconType, Image> icons;
RemoteVersionListCell(MutableObject<RemoteVersionListCell> lastCell, EnumMap<VersionIconType, Image> icons) {
this.lastCell = lastCell;
this.icons = icons;
pane.getStyleClass().add("md-list-cell");
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
pane.getChildren().setAll(ripplerContainer);
}
private Image getIcon(VersionIconType type) {
return icons.computeIfAbsent(type, iconType -> new Image(iconType.getResourceUrl(), 32, 32, false, true));
}
@Override
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
super.updateItem(remoteVersion, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
return;
lastCell.setValue(this);
if (empty) {
setGraphic(null);
return;
}
setGraphic(pane);
content.setTitle(remoteVersion.getSelfVersion());
if (remoteVersion.getReleaseDate() != null) {
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant()));
} else {
content.setSubtitle(null);
}
if (remoteVersion instanceof GameRemoteVersion) {
switch (remoteVersion.getVersionType()) {
case RELEASE:
content.getTags().setAll(i18n("version.game.release"));
content.setImage(getIcon(VersionIconType.GRASS));
break;
case SNAPSHOT:
content.getTags().setAll(i18n("version.game.snapshot"));
content.setImage(getIcon(VersionIconType.COMMAND));
break;
default:
content.getTags().setAll(i18n("version.game.old"));
content.setImage(getIcon(VersionIconType.CRAFT_TABLE));
break;
}
} else {
VersionIconType iconType;
if (remoteVersion instanceof LiteLoaderRemoteVersion)
iconType = VersionIconType.CHICKEN;
else if (remoteVersion instanceof OptiFineRemoteVersion)
iconType = VersionIconType.COMMAND;
else if (remoteVersion instanceof ForgeRemoteVersion)
iconType = VersionIconType.FORGE;
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
iconType = VersionIconType.FABRIC;
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
iconType = VersionIconType.QUILT;
else
iconType = null;
content.setImage(iconType != null ? getIcon(iconType) : null);
if (content.getSubtitle() == null)
content.setSubtitle(remoteVersion.getGameVersion());
else
content.getTags().setAll(remoteVersion.getGameVersion());
}
}
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.export;
import javafx.scene.Node;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.ModpackExportInfo;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackExportTask;
@@ -33,14 +32,13 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.Zipper;
import java.io.File;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.nio.file.Path;
import java.util.*;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
@@ -70,8 +68,8 @@ public final class ExportWizardProvider implements WizardProvider {
}
private Task<?> exportWithLauncher(String modpackType, ModpackExportInfo exportInfo, File modpackFile) {
List<File> launcherJar = Launcher.getCurrentJarFiles();
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar != null;
Optional<Path> launcherJar = JarUtils.thisJar();
boolean packWithLauncher = exportInfo.isPackWithLauncher() && launcherJar.isPresent();
return new Task<Object>() {
File tempModpack;
Task<?> exportTask;
@@ -139,8 +137,11 @@ public final class ExportWizardProvider implements WizardProvider {
if (background_jpg.isFile())
zip.putFile(background_jpg, "background.jpg");
for (File jar : launcherJar)
zip.putFile(jar, jar.getName());
File background_gif = new File("background.gif").getAbsoluteFile();
if (background_gif.isFile())
zip.putFile(background_gif, "background.gif");
zip.putFile(launcherJar.get(), launcherJar.get().getFileName().toString());
}
}
};
@@ -35,7 +35,7 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -147,7 +147,7 @@ public final class ModpackFileSelectionPage extends StackPane implements WizardP
@FXML
private void onNext() {
LinkedList<String> list = new LinkedList<>();
ArrayList<String> list = new ArrayList<>();
getFilesNeeded(rootNode, "minecraft", list);
controller.getSettings().put(MODPACK_FILE_SELECTION, list);
controller.onFinish();
@@ -29,7 +29,6 @@ import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
@@ -43,6 +42,7 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.wizard.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
@@ -96,8 +96,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
launchArguments.set(versionSetting.getMinecraftArgs());
javaArguments.set(versionSetting.getJavaArgs());
List<File> launcherJar = Launcher.getCurrentJarFiles();
canIncludeLauncher = launcherJar != null;
canIncludeLauncher = JarUtils.thisJar().isPresent();
next.set(e -> onNext());
}
@@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
* 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
@@ -62,7 +62,13 @@ public class AboutPage extends StackPane {
bangbang93.setTitle("bangbang93");
bangbang93.setSubtitle(i18n("about.thanks_to.bangbang93.statement"));
bangbang93.setExternalLink("https://bmclapi2.bangbang93.com/");
IconedTwoLineListItem glavo = new IconedTwoLineListItem();
glavo.setImage(new Image("/assets/img/glavo.jpg", 32, 32, false, true));
glavo.setTitle("Glavo");
glavo.setSubtitle(i18n("about.thanks_to.glavo.statement"));
glavo.setExternalLink("https://github.com/Glavo");
IconedTwoLineListItem gamerteam = new IconedTwoLineListItem();
gamerteam.setTitle("gamerteam");
gamerteam.setImage(new Image("/assets/img/gamerteam.jpg", 32, 32, false, true));
@@ -86,19 +92,13 @@ public class AboutPage extends StackPane {
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
mcmod.setExternalLink("https://www.mcmod.cn/");
IconedTwoLineListItem noin = new IconedTwoLineListItem();
noin.setImage(new Image("/assets/img/noin.png", 32, 32, false, true));
noin.setTitle(i18n("about.thanks_to.noin"));
noin.setSubtitle(i18n("about.thanks_to.noin.statement"));
noin.setExternalLink("https://noin.cn/");
IconedTwoLineListItem contributors = new IconedTwoLineListItem();
contributors.setImage(new Image("/assets/img/github.png", 32, 32, false, true));
contributors.setTitle(i18n("about.thanks_to.contributors"));
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
contributors.setExternalLink("https://github.com/huanghongxun/HMCL/graphs/contributors");
thanks.getContent().setAll(yushijinhun, bangbang93, mcbbs, mcmod, noin, gamerteam, redLnn, contributors);
thanks.getContent().setAll(yushijinhun, bangbang93, glavo, mcbbs, mcmod, gamerteam, redLnn, contributors);
}
ComponentList community = new ComponentList();
@@ -115,30 +115,30 @@ public class AboutPage extends StackPane {
discord.setSubtitle(i18n("about.community.discord.statement"));
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
IconedTwoLineListItem kaiheila = new IconedTwoLineListItem();
kaiheila.setImage(new Image("/assets/img/kaiheila.jpeg", 32, 32, false, true));
kaiheila.setTitle(i18n("about.community.kaiheila"));
kaiheila.setSubtitle(i18n("about.community.kaiheila.statement"));
kaiheila.setExternalLink("https://kaihei.co/Kx7n3t");
IconedTwoLineListItem kookapp = new IconedTwoLineListItem();
kookapp.setImage(new Image("/assets/img/kookapp.png", 32, 32, false, true));
kookapp.setTitle(i18n("about.community.kookapp"));
kookapp.setSubtitle(i18n("about.community.kookapp.statement"));
kookapp.setExternalLink("https://kook.top/Kx7n3t");
community.getContent().setAll(users, discord, kaiheila);
community.getContent().setAll(users, discord, kookapp);
}
ComponentList dep = new ComponentList();
{
IconedTwoLineListItem javafx = new IconedTwoLineListItem();
javafx.setTitle("JavaFX");
javafx.setSubtitle("Copyright (c) 2013, 2021, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
javafx.setSubtitle("Copyright © 2013, 2021, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
javafx.setExternalLink("https://openjfx.io/");
IconedTwoLineListItem jfoenix = new IconedTwoLineListItem();
jfoenix.setTitle("JFoenix");
jfoenix.setSubtitle("Copyright (c) 2016 JFoenix.\nLicensed under the MIT License.");
jfoenix.setSubtitle("Copyright © 2016 JFoenix.\nLicensed under the MIT License.");
jfoenix.setExternalLink("http://www.jfoenix.com/");
IconedTwoLineListItem gson = new IconedTwoLineListItem();
gson.setTitle("Gson");
gson.setSubtitle("Copyright 2008 Google Inc.\nLicensed under the Apache 2.0 License.");
gson.setSubtitle("Copyright © 2008 Google Inc.\nLicensed under the Apache 2.0 License.");
gson.setExternalLink("https://github.com/google/gson");
IconedTwoLineListItem xz = new IconedTwoLineListItem();
@@ -148,28 +148,28 @@ public class AboutPage extends StackPane {
IconedTwoLineListItem fxgson = new IconedTwoLineListItem();
fxgson.setTitle("fx-gson");
fxgson.setSubtitle("Copyright (c) 2016 Joffrey Bion.\nLicensed under the MIT License.");
fxgson.setSubtitle("Copyright © 2016 Joffrey Bion.\nLicensed under the MIT License.");
fxgson.setExternalLink("https://github.com/joffrey-bion/fx-gson");
IconedTwoLineListItem constantPoolScanner = new IconedTwoLineListItem();
constantPoolScanner.setTitle("Constant Pool Scanner");
constantPoolScanner.setSubtitle("Copyright 1997-2010 Oracle and/or its affiliates.\nLicensed under the GPL 2 or the CDDL.");
constantPoolScanner.setSubtitle("Copyright © 1997-2010 Oracle and/or its affiliates.\nLicensed under the GPL 2 or the CDDL.");
constantPoolScanner.setExternalLink("https://github.com/jenkinsci/constant-pool-scanner");
IconedTwoLineListItem openNBT = new IconedTwoLineListItem();
openNBT.setTitle("OpenNBT");
openNBT.setSubtitle("Copyright (C) 2013-2021 Steveice10.\nLicensed under the MIT License.");
openNBT.setSubtitle("Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.");
openNBT.setExternalLink("https://github.com/Steveice10/OpenNBT");
IconedTwoLineListItem jstun = new IconedTwoLineListItem();
jstun.setTitle("JSTUN");
jstun.setSubtitle("Copyright (c) 2005 Thomas King <king@t-king.de>.\nLicensed under the Apache License 2.0.");
jstun.setSubtitle("Copyright © 2005 Thomas King <king@t-king.de>.\nLicensed under the Apache License 2.0.");
jstun.setExternalLink("https://github.com/tking/JSTUN");
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
jstun.setTitle("minecraft-jfx-skin");
jstun.setSubtitle("Copyright (c) 2016 InfinityStudio.\nLicensed under the GPL 3.");
jstun.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
minecraftJFXSkin.setTitle("minecraft-jfx-skin");
minecraftJFXSkin.setSubtitle("Copyright © 2016 InfinityStudio.\nLicensed under the GPL 3.");
minecraftJFXSkin.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, jstun, minecraftJFXSkin);
}
@@ -53,10 +53,9 @@ public class DownloadSettingsPage extends StackPane {
getChildren().setAll(scrollPane);
{
ComponentList downloadSource = new ComponentList();
VBox downloadSource = new VBox(8);
downloadSource.getStyleClass().add("card-non-transparent");
{
VBox pane = new VBox(8);
VBox chooseWrapper = new VBox();
chooseWrapper.setPadding(new Insets(8, 0, 8, 0));
@@ -75,7 +74,7 @@ public class DownloadSettingsPage extends StackPane {
JFXComboBox<String> cboVersionListSource = new JFXComboBox<>();
cboVersionListSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
versionListSourcePane.setRight(cboVersionListSource);
FXUtils.setLimitWidth(cboVersionListSource, 420);
FXUtils.setLimitWidth(cboVersionListSource, 400);
cboVersionListSource.getItems().setAll(DownloadProviders.providersById.keySet());
selectedItemPropertyFor(cboVersionListSource).bindBidirectional(config().versionListSourceProperty());
@@ -91,31 +90,28 @@ public class DownloadSettingsPage extends StackPane {
JFXComboBox<String> cboDownloadSource = new JFXComboBox<>();
cboDownloadSource.setConverter(stringConverter(key -> i18n("download.provider." + key)));
downloadSourcePane.setRight(cboDownloadSource);
downloadSourcePane.setCenter(cboDownloadSource);
FXUtils.setLimitWidth(cboDownloadSource, 420);
cboDownloadSource.getItems().setAll(DownloadProviders.rawProviders.keySet());
selectedItemPropertyFor(cboDownloadSource).bindBidirectional(config().downloadTypeProperty());
}
pane.getChildren().setAll(chooseWrapper, versionListSourcePane, downloadSourcePane);
downloadSource.getContent().add(pane);
downloadSource.getChildren().setAll(chooseWrapper, versionListSourcePane, downloadSourcePane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.version_list_source")), downloadSource);
}
{
ComponentList downloadThreads = new ComponentList();
VBox downloadThreads = new VBox(16);
downloadThreads.getStyleClass().add("card-non-transparent");
{
VBox pane = new VBox(16);
pane.setPadding(new Insets(8, 0, 8, 0));
{
JFXCheckBox chkAutoDownloadThreads = new JFXCheckBox(i18n("settings.launcher.download.threads.auto"));
VBox.setMargin(chkAutoDownloadThreads, new Insets(8, 0, 0, 0));
chkAutoDownloadThreads.selectedProperty().bindBidirectional(config().autoDownloadThreadsProperty());
pane.getChildren().add(chkAutoDownloadThreads);
downloadThreads.getChildren().add(chkAutoDownloadThreads);
chkAutoDownloadThreads.selectedProperty().addListener((a, b, newValue) -> {
if (newValue) {
@@ -136,7 +132,7 @@ public class DownloadSettingsPage extends StackPane {
JFXTextField threadsField = new JFXTextField();
FXUtils.setLimitWidth(threadsField, 60);
threadsField.textProperty().bindBidirectional(config().downloadThreadsProperty(), SafeStringConverter.fromInteger());
FXUtils.bindInt(threadsField, config().downloadThreadsProperty());
AtomicBoolean changedByTextField = new AtomicBoolean(false);
FXUtils.onChangeAndOperate(config().downloadThreadsProperty(), value -> {
@@ -150,7 +146,7 @@ public class DownloadSettingsPage extends StackPane {
});
hbox.getChildren().setAll(label, slider, threadsField);
pane.getChildren().add(hbox);
downloadThreads.getChildren().add(hbox);
}
{
@@ -158,26 +154,22 @@ public class DownloadSettingsPage extends StackPane {
VBox.setMargin(hintPane, new Insets(0, 0, 0, 30));
hintPane.disableProperty().bind(config().autoDownloadThreadsProperty());
hintPane.setText(i18n("settings.launcher.download.threads.hint"));
pane.getChildren().add(hintPane);
downloadThreads.getChildren().add(hintPane);
}
downloadThreads.getContent().add(pane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("download")), downloadThreads);
}
{
ComponentList proxyList = new ComponentList();
VBox proxyWrapper = new VBox();
proxyWrapper.setPadding(new Insets(8, 0, 8, 0));
proxyWrapper.setSpacing(10);
VBox proxyList = new VBox(10);
proxyList.getStyleClass().add("card-non-transparent");
VBox proxyPane = new VBox();
{
JFXCheckBox chkDisableProxy = new JFXCheckBox(i18n("settings.launcher.proxy.disable"));
proxyWrapper.getChildren().add(chkDisableProxy);
VBox.setMargin(chkDisableProxy, new Insets(8, 0, 0, 0));
proxyList.getChildren().add(chkDisableProxy);
reversedSelectedPropertyFor(chkDisableProxy).bindBidirectional(config().hasProxyProperty());
proxyPane.disableProperty().bind(chkDisableProxy.selectedProperty());
}
@@ -221,7 +213,7 @@ public class DownloadSettingsPage extends StackPane {
GridPane.setRowIndex(txtProxyHost, 1);
GridPane.setColumnIndex(txtProxyHost, 1);
gridPane.getChildren().add(txtProxyHost);
txtProxyHost.textProperty().bindBidirectional(config().proxyHostProperty());
FXUtils.bindString(txtProxyHost, config().proxyHostProperty());
txtProxyHost.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
}
@@ -240,11 +232,10 @@ public class DownloadSettingsPage extends StackPane {
FXUtils.setValidateWhileTextChanged(txtProxyPort, true);
gridPane.getChildren().add(txtProxyPort);
txtProxyPort.textProperty().bindBidirectional(config().proxyPortProperty(),
SafeStringConverter.fromInteger()
.restrict(it -> it >= 0 && it <= 0xFFFF)
.fallbackTo(0)
.asPredicate(Validator.addTo(txtProxyPort)));
FXUtils.bind(txtProxyPort, config().proxyPortProperty(), SafeStringConverter.fromInteger()
.restrict(it -> it >= 0 && it <= 0xFFFF)
.fallbackTo(0)
.asPredicate(Validator.addTo(txtProxyPort)));
}
proxyPane.getChildren().add(gridPane);
}
@@ -281,7 +272,7 @@ public class DownloadSettingsPage extends StackPane {
GridPane.setRowIndex(txtProxyUsername, 0);
GridPane.setColumnIndex(txtProxyUsername, 1);
authPane.getChildren().add(txtProxyUsername);
txtProxyUsername.textProperty().bindBidirectional(config().proxyUserProperty());
FXUtils.bindString(txtProxyUsername, config().proxyUserProperty());
}
{
@@ -310,9 +301,8 @@ public class DownloadSettingsPage extends StackPane {
proxyPane.getChildren().add(authPane);
}
proxyWrapper.getChildren().add(proxyPane);
proxyList.getChildren().add(proxyPane);
}
proxyList.getContent().add(proxyWrapper);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.proxy")), proxyList);
}
@@ -30,6 +30,7 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.OAuthServer;
import org.jackhuang.hmcl.setting.Accounts;
@@ -117,7 +118,8 @@ public class FeedbackPage extends VBox implements PageAware {
JFXListView<FeedbackResponse> listView = new JFXListView<>();
spinnerPane.setContent(listView);
Bindings.bindContent(listView.getItems(), feedbacks);
listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView) {
MutableObject<Object> lastCell = new MutableObject<>();
listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView, lastCell) {
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton likeButton = new JFXButton();
private final JFXButton unlikeButton = new JFXButton();
@@ -230,7 +232,7 @@ public class FeedbackPage extends VBox implements PageAware {
Controllers.dialog(new AddFeedbackDialog());
}
private class LoginDialog extends JFXDialogLayout {
private static final class LoginDialog extends JFXDialogLayout {
private final SpinnerPane spinnerPane = new SpinnerPane();
private final Label errorLabel = new Label();
private final BooleanProperty logging = new SimpleBooleanProperty();
@@ -241,10 +243,7 @@ public class FeedbackPage extends VBox implements PageAware {
VBox vbox = new VBox(8);
setBody(vbox);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
logging
? i18n("account.methods.microsoft.manual")
: i18n("account.methods.microsoft.hint")));
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging -> i18n("account.hmcl.hint")));
hintPane.setOnMouseClicked(e -> {
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
@@ -63,7 +63,7 @@ public class HelpPage extends SpinnerPane {
private void loadHelp() {
showSpinner();
Task.<List<HelpCategory>>supplyAsync(() -> HttpRequest.GET("https://hmcl.huangyuhui.net/api/help/").getJson(new TypeToken<List<HelpCategory>>() {
Task.<List<HelpCategory>>supplyAsync(() -> HttpRequest.GET("https://docs.hmcl.net/index.json").getJson(new TypeToken<List<HelpCategory>>() {
}.getType()))
.thenAcceptAsync(Schedulers.javafx(), helpCategories -> {
for (HelpCategory category : helpCategories) {
@@ -57,7 +57,10 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
@@ -89,7 +92,16 @@ public final class MainPage extends StackPane implements DecoratorPage {
titleNode.setAlignment(Pos.CENTER_LEFT);
ImageView titleIcon = new ImageView();
titleIcon.setImage(new Image("/assets/img/icon.png", 20, 20, false, false));
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9) {
// JavaFX 8 has some problems with @2x images
// Force load the original icon
try (InputStream is = MainPage.class.getResourceAsStream("/assets/img/icon.png")) {
titleIcon.setImage(new Image(is, 20, 20, false, false));
} catch (IOException ignored) {
}
} else {
titleIcon.setImage(new Image("/assets/img/icon.png", 20, 20, false, false));
}
Label titleLabel = new Label(Metadata.FULL_TITLE);
titleLabel.getStyleClass().add("jfx-decorator-title");
@@ -146,12 +146,10 @@ public class PersonalizationPage extends StackPane {
JFXTextField txtLogFontSize = new JFXTextField();
FXUtils.setLimitWidth(txtLogFontSize, 50);
txtLogFontSize.textProperty().bindBidirectional(config().fontSizeProperty(),
SafeStringConverter.fromFiniteDouble()
.restrict(it -> it > 0)
.fallbackTo(12.0)
.asPredicate(Validator.addTo(txtLogFontSize)));
FXUtils.bind(txtLogFontSize, config().fontSizeProperty(), SafeStringConverter.fromFiniteDouble()
.restrict(it -> it > 0)
.fallbackTo(12.0)
.asPredicate(Validator.addTo(txtLogFontSize)));
hBox.getChildren().setAll(cboLogFont, txtLogFontSize);
@@ -34,14 +34,16 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.File;
@@ -82,7 +84,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
private MainPage getMainPage() {
if (mainPage == null) {
MainPage mainPage = new MainPage();
FXUtils.applyDragListener(mainPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
FXUtils.applyDragListener(mainPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(
new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack),
@@ -153,7 +155,13 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
multiplayerItem.setLeftGraphic(wrap(SVG::lan));
multiplayerItem.setActionButtonVisible(false);
multiplayerItem.setTitle(i18n("multiplayer"));
multiplayerItem.setOnAction(e -> Controllers.navigate(Controllers.getMultiplayerPage()));
JFXHyperlink link = new JFXHyperlink(i18n("multiplayer.hint.details"));
link.setOnAction(e -> FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/multiplayer-migrate"));
multiplayerItem.setOnAction(e -> Controllers.dialog(
new MessageDialogPane.Builder(i18n("multiplayer.hint"), null, MessageDialogPane.MessageType.INFO)
.addAction(link)
.ok(null)
.build()));
// sixth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
@@ -199,7 +207,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
modpack)
.executor())
.thenAcceptAsync(Schedulers.javafx(), executor -> {
Controllers.taskDialog(executor, i18n("modpack.installing"));
Controllers.taskDialog(executor, i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL);
executor.start();
}).start();
}
@@ -35,7 +35,6 @@ import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.i18n.Locales;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@@ -139,12 +138,7 @@ public final class SettingsPage extends SettingsView {
}
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().open(logFile.toFile());
} catch (IOException ignored) {
}
}
FXUtils.showFileInExplorer(logFile);
});
}
@@ -27,6 +27,7 @@ import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import org.apache.commons.lang3.mutable.MutableObject;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -84,10 +85,17 @@ public class SponsorPage extends StackPane {
StackPane pane = new StackPane();
pane.getStyleClass().add("card");
listView = new JFXListView<>();
MutableObject<Object> lastCell = new MutableObject<>();
listView.setCellFactory((listView) -> new JFXListCell<Sponsor>() {
@Override
public void updateItem(Sponsor item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.getValue() && !isVisible())
return;
lastCell.setValue(this);
if (!empty) {
setText(item.getName());
setGraphic(null);
@@ -1,153 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.FutureCallback;
import java.util.Objects;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAware {
private final FutureCallback<CreationRequest> callback;
private final LocalServerDetector lanServerDetectorThread;
private final BooleanProperty allowAllJoinRequests = new SimpleBooleanProperty(true);
private LocalServerDetector.PingResponse server;
CreateMultiplayerRoomDialog(FutureCallback<CreationRequest> callback) {
this.callback = callback;
setTitle(i18n("multiplayer.session.create"));
GridPane body = new GridPane();
body.setMaxWidth(500);
body.getColumnConstraints().addAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
body.setVgap(8);
body.setHgap(16);
body.setDisable(true);
setBody(body);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.setText(i18n("multiplayer.session.create.hint"));
GridPane.setColumnSpan(hintPane, 2);
body.addRow(0, hintPane);
Label nameField = new Label();
nameField.setText(Optional.ofNullable(Accounts.getSelectedAccount())
.map(Account::getUsername)
.map(username -> i18n("multiplayer.session.name.format", username))
.orElse(""));
body.addRow(1, new Label(i18n("multiplayer.session.create.name")), nameField);
Label portLabel = new Label(i18n("multiplayer.nat.testing"));
portLabel.setText(i18n("multiplayer.nat.testing"));
body.addRow(2, new Label(i18n("multiplayer.session.create.port")), portLabel);
JFXCheckBox allowAllJoinRequestsCheckBox = new JFXCheckBox(i18n("multiplayer.session.create.join.allow"));
allowAllJoinRequestsCheckBox.selectedProperty().bindBidirectional(allowAllJoinRequests);
GridPane.setColumnSpan(allowAllJoinRequestsCheckBox, 2);
body.addRow(3, allowAllJoinRequestsCheckBox);
setValid(false);
JFXHyperlink noinLink = new JFXHyperlink();
noinLink.setText("noin.cn");
noinLink.setOnAction(e -> FXUtils.openLink("https://noin.cn"));
setActions(warningLabel, noinLink, acceptPane, cancelButton);
lanServerDetectorThread = new LocalServerDetector(3);
lanServerDetectorThread.onDetectedLanServer().register(event -> {
runInFX(() -> {
if (event.getLanServer() != null && event.getLanServer().isValid()) {
nameField.setText(event.getLanServer().getMotd());
portLabel.setText(event.getLanServer().getAd().toString());
setValid(true);
} else {
nameField.setText("");
portLabel.setText("");
onFailure(i18n("multiplayer.session.create.port.error"));
setValid(false);
}
server = event.getLanServer();
body.setDisable(false);
getProgressBar().setVisible(false);
});
});
}
@Override
protected void onAccept() {
setLoading();
callback.call(new CreationRequest(
Objects.requireNonNull(server),
allowAllJoinRequests.get()
), () -> {
runInFX(this::onSuccess);
}, msg -> {
runInFX(() -> onFailure(msg));
});
}
@Override
public void onDialogShown() {
getProgressBar().setVisible(true);
getProgressBar().setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
lanServerDetectorThread.start();
}
@Override
public void onDialogClosed() {
lanServerDetectorThread.interrupt();
}
public static class CreationRequest {
private final LocalServerDetector.PingResponse server;
private final boolean allowAllJoinRequests;
public CreationRequest(LocalServerDetector.PingResponse server, boolean allowAllJoinRequests) {
this.server = server;
this.allowAllJoinRequests = allowAllJoinRequests;
}
public LocalServerDetector.PingResponse getServer() {
return server;
}
public boolean isAllowAllJoinRequests() {
return allowAllJoinRequests;
}
}
}
@@ -1,74 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class LocalServerBroadcaster implements Runnable {
private final int port;
private final MultiplayerManager.CatoSession session;
public LocalServerBroadcaster(int port, MultiplayerManager.CatoSession session) {
this.port = port;
this.session = session;
}
public int getPort() {
return port;
}
@Override
public void run() {
DatagramSocket socket;
InetAddress broadcastAddress;
try {
socket = new DatagramSocket();
broadcastAddress = InetAddress.getByName("224.0.2.60");
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create datagram socket", e);
return;
}
while (session.isRunning()) {
try {
byte[] data = String.format("[MOTD]%s[/MOTD][AD]%d[/AD]", i18n("multiplayer.session.name.motd", session.getName()), port).getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(data, 0, data.length, broadcastAddress, 4445);
socket.send(packet);
LOG.finest("Broadcast server 0.0.0.0:" + port);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to send motd packet", e);
}
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
return;
}
}
socket.close();
}
}
@@ -1,141 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class LocalServerDetector extends Thread {
private final EventManager<DetectedLanServerEvent> onDetectedLanServer = new EventManager<>();
private final int retry;
public LocalServerDetector(int retry) {
this.retry = retry;
setName("LocalServerDetector");
setDaemon(true);
}
public EventManager<DetectedLanServerEvent> onDetectedLanServer() {
return onDetectedLanServer;
}
@Override
public void run() {
MulticastSocket socket;
InetAddress broadcastAddress;
try {
socket = new MulticastSocket(4445);
socket.setSoTimeout(5000);
socket.joinGroup(broadcastAddress = InetAddress.getByName("224.0.2.60"));
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create datagram socket", e);
return;
}
byte[] buf = new byte[1024];
int tried = 0;
while (!isInterrupted()) {
DatagramPacket packet = new DatagramPacket(buf, 1024);
try {
socket.receive(packet);
} catch (SocketTimeoutException e) {
if (tried++ > retry) {
onDetectedLanServer.fireEvent(new DetectedLanServerEvent(this, null));
break;
}
continue;
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to detect lan server", e);
break;
}
String response = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
LOG.fine("Local server " + packet.getAddress() + ":" + packet.getPort() + " broadcast message: " + response);
onDetectedLanServer.fireEvent(new DetectedLanServerEvent(this, PingResponse.parsePingResponse(response)));
break;
}
try {
socket.leaveGroup(broadcastAddress);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to leave multicast listening group", e);
}
socket.close();
}
public static class DetectedLanServerEvent extends Event {
private final PingResponse lanServer;
public DetectedLanServerEvent(Object source, PingResponse lanServer) {
super(source);
this.lanServer = lanServer;
}
public PingResponse getLanServer() {
return lanServer;
}
}
public static class PingResponse {
private final String motd;
private final Integer ad;
public PingResponse(String motd, Integer ad) {
this.motd = motd;
this.ad = ad;
}
public String getMotd() {
return motd;
}
public Integer getAd() {
return ad;
}
public boolean isValid() {
return ad != null;
}
public static PingResponse parsePingResponse(String message) {
return new PingResponse(
StringUtils.substringBefore(
StringUtils.substringAfter(message, "[MOTD]"),
"[/MOTD]"),
Lang.toIntOrNull(StringUtils.substringBefore(
StringUtils.substringAfter(message, "[AD]"),
"[/AD]"))
);
}
}
}
@@ -1,154 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.util.gson.JsonSubtype;
import org.jackhuang.hmcl.util.gson.JsonType;
public final class MultiplayerChannel {
private MultiplayerChannel() {
}
@JsonType(
property = "type",
subtypes = {
@JsonSubtype(clazz = HandshakeRequest.class, name = "handshake"),
@JsonSubtype(clazz = JoinRequest.class, name = "join"),
@JsonSubtype(clazz = KeepAliveRequest.class, name = "keepalive")
}
)
public static class Request {
}
public static class HandshakeRequest extends Request {
}
public static class JoinRequest extends Request {
private final String clientVersion;
private final String username;
public JoinRequest(String clientVersion, String username) {
this.clientVersion = clientVersion;
this.username = username;
}
public String getClientVersion() {
return clientVersion;
}
public String getUsername() {
return username;
}
}
public static class KeepAliveRequest extends Request {
private final long timestamp;
public KeepAliveRequest(long timestamp) {
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
}
@JsonType(
property = "type",
subtypes = {
@JsonSubtype(clazz = HandshakeResponse.class, name = "handshake"),
@JsonSubtype(clazz = JoinResponse.class, name = "join"),
@JsonSubtype(clazz = KeepAliveResponse.class, name = "keepalive"),
@JsonSubtype(clazz = KickResponse.class, name = "kick")
}
)
public static class Response {
}
public static class HandshakeResponse extends Response {
}
public static class JoinResponse extends Response {
private final String sessionName;
private final int port;
public JoinResponse(String sessionName, int port) {
this.sessionName = sessionName;
this.port = port;
}
public String getSessionName() {
return sessionName;
}
public int getPort() {
return port;
}
}
public static class KeepAliveResponse extends Response {
private final long timestamp;
public KeepAliveResponse(long timestamp) {
this.timestamp = timestamp;
}
public long getTimestamp() {
return timestamp;
}
}
public static class KickResponse extends Response {
private final String msg;
public KickResponse(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
public static final String VERSION_NOT_MATCHED = "version_not_matched";
public static final String KICKED = "kicked";
public static final String JOIN_ACEEPTANCE_TIMEOUT = "join_acceptance_timeout";
}
public static class CatoClient extends Event {
private final String username;
public CatoClient(Object source, String username) {
super(source);
this.username = username;
}
public String getUsername() {
return username;
}
}
public static String verifyJson(String jsonString) {
if (jsonString.indexOf('\r') >= 0 || jsonString.indexOf('\n') >= 0) {
throw new IllegalArgumentException();
}
return jsonString;
}
}
@@ -1,227 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.*;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.TimerTask;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.*;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class MultiplayerClient extends Thread {
private final String id;
private final int port;
private int gamePort;
private boolean connected = false;
private final EventManager<ConnectedEvent> onConnected = new EventManager<>();
private final EventManager<Event> onDisconnected = new EventManager<>();
private final EventManager<KickEvent> onKicked = new EventManager<>();
private final EventManager<Event> onHandshake = new EventManager<>();
public MultiplayerClient(String id, int port) {
this.id = id;
this.port = port;
setName("MultiplayerClient");
setDaemon(true);
}
public void setGamePort(int gamePort) {
this.gamePort = gamePort;
}
public int getGamePort() {
return gamePort;
}
public EventManager<ConnectedEvent> onConnected() {
return onConnected;
}
public EventManager<Event> onDisconnected() {
return onDisconnected;
}
public EventManager<KickEvent> onKicked() {
return onKicked;
}
public EventManager<Event> onHandshake() {
return onHandshake;
}
@Override
public void run() {
LOG.info("Connecting to 127.0.0.1:" + port);
for (int i = 0; i < 5; i++) {
KeepAliveThread keepAliveThread = null;
try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) {
MultiplayerServer.Endpoint endpoint = new MultiplayerServer.Endpoint(socket, writer);
socket.setSoTimeout(30000);
LOG.info("Connected to 127.0.0.1:" + port);
endpoint.write(new HandshakeRequest());
endpoint.write(new JoinRequest(MultiplayerManager.CATO_VERSION, id));
LOG.fine("Sent join request with id=" + id);
keepAliveThread = new KeepAliveThread(endpoint);
keepAliveThread.start();
TimerTask task = Lang.setTimeout(() -> {
// If after 15 seconds, we didn't receive the HandshakeResponse,
// We fail to establish the connection with server.
try {
LOG.log(Level.WARNING, "Socket connection timeout, closing socket");
socket.close();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to close socket", e);
}
}, 25 * 1000);
String line;
while ((line = reader.readLine()) != null) {
if (isInterrupted()) {
return;
}
LOG.fine("Message from server:" + line);
Response response = JsonUtils.fromNonNullJson(line, Response.class);
if (response instanceof JoinResponse) {
JoinResponse joinResponse = JsonUtils.fromNonNullJson(line, JoinResponse.class);
setGamePort(joinResponse.getPort());
connected = true;
onConnected.fireEvent(new ConnectedEvent(this, joinResponse.getSessionName(), joinResponse.getPort()));
LOG.fine("Received join response with port " + joinResponse.getPort());
} else if (response instanceof KickResponse) {
LOG.fine("Kicked by the server");
onKicked.fireEvent(new KickEvent(this, ((KickResponse) response).getMsg()));
return;
} else if (response instanceof KeepAliveResponse) {
} else if (response instanceof HandshakeResponse) {
LOG.fine("Established connection with server");
onHandshake.fireEvent(new Event(this));
task.cancel();
} else {
LOG.log(Level.WARNING, "Unrecognized packet from server:" + line);
}
}
} catch (ConnectException e) {
LOG.info("Failed to connect to 127.0.0.1:" + port + ", tried " + i + " time(s)");
try {
Thread.sleep(2000);
} catch (InterruptedException ex) {
LOG.warning("MultiplayerClient interrupted");
return;
}
continue;
} catch (IOException | JsonParseException e) {
e.printStackTrace();
} finally {
if (keepAliveThread != null) {
keepAliveThread.interrupt();
}
}
}
LOG.info("Lost connection to 127.0.0.1:" + port);
onDisconnected.fireEvent(new Event(this));
}
public boolean isConnected() {
return connected;
}
private static class KeepAliveThread extends Thread {
private final MultiplayerServer.Endpoint endpoint;
public KeepAliveThread(MultiplayerServer.Endpoint endpoint) {
this.endpoint = endpoint;
}
@Override
public void run() {
while (!isInterrupted()) {
try {
endpoint.write(new KeepAliveRequest(System.currentTimeMillis()));
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to send keep alive packet", e);
break;
}
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
break;
}
}
}
}
public static class ConnectedEvent extends Event {
private final String sessionName;
private final int port;
public ConnectedEvent(Object source, String sessionName, int port) {
super(source);
this.sessionName = sessionName;
this.port = port;
}
public String getSessionName() {
return sessionName;
}
public int getPort() {
return port;
}
}
public static class KickEvent extends Event {
private final String reason;
public KickEvent(Object source, String reason) {
super(source);
this.reason = reason;
}
public String getReason() {
return reason;
}
}
}
@@ -1,621 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.launch.StreamPump;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.wrap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
/**
* Cato Management.
*/
public final class MultiplayerManager {
static final String CATO_VERSION = "1.1.1-3f36a56";
// private static final String CATO_DOWNLOAD_URL = "https://files.huangyuhui.net/maven/cato/cato/" + MultiplayerManager.CATO_VERSION;
private static final String CATO_DOWNLOAD_URL = "https://codechina.csdn.net/huanghongxun1/ioi_bin/-/raw/3f36a567d9381e5fa404b96bcecd6d6a555f235c/client/";
private static final String CATO_PATH = getCatoPath();
public static final int CATO_AGREEMENT_VERSION = 2;
private static final String REMOTE_ADDRESS = "127.0.0.1";
private static final String LOCAL_ADDRESS = "0.0.0.0";
private static final Map<String, String> HASH = mapOf(
pair("cato-client-darwin-amd64", "e733fa4b72be24b99026249c31c4ae8b52ad3906"),
pair("cato-client-darwin-arm64", "19ab41b7f78263cad81009ae1a391b0d0367d95f"),
pair("cato-client-freebsd-amd64", "6de8439ab9267e444d45bb9f02394023ce0bfe5e"),
pair("cato-client-freebsd-arm64", "5cd8930e7cd17d72386b085950e63467e4ce72ef"),
pair("cato-client-freebsd-arm7", "d0e54a4a1b0f8c2f769d74a6a600eec4a9c4e0da"),
pair("cato-client-freebsd-i386", "d9d22faf85e01e77b18f28f1609e87623ac9b1a4"),
pair("cato-client-js.wasm", "acb366a4d6a3c6cd464ef5919ce7d2d58a52964e"),
pair("cato-client-linux-amd64", "04e94a3adff1e4d865dead2c6c5da584b74bff0a"),
pair("cato-client-linux-arm64", "ba662bc461995dae5e78d21b9fea35c2db81b652"),
pair("cato-client-linux-arm7", "28021eff56e1e273c051853808e70e7ab3af80d9"),
pair("cato-client-linux-i386", "4f524a9cfbcea560bdf07d3468b883f946c431ed"),
pair("cato-client-linux-mips", "91c3108b046511c2aafbe88f82b38a06c03d88e1"),
pair("cato-client-linux-mips64", "04d1b66494dbdb0443eaae85bda6b61408e300ed"),
pair("cato-client-linux-mips64le", "d838ac71052f7873b28f4b4a2f23470b918ef85d"),
pair("cato-client-linux-mipsle", "54918ed9cdbe48f7adf11680947281f5fc7c5a7d"),
pair("cato-client-linux-ppc64", "f2679259516b0db0422f85afcce35bfa6f7b3192"),
pair("cato-client-linux-ppc64le", "463d9b86316dfc83e668ddfae96e51181a9e3ffa"),
pair("cato-client-openbsd-amd64", "b776155e955340485faf62c3b2aad3a788c57e2b"),
pair("cato-client-openbsd-arm64", "fd6ec21248fd9c8be3d9d2876faf65b95c63b3a9"),
pair("cato-client-openbsd-arm7", "19eabe71530bb4eac7a32f6b8aeef72f6f9382e9"),
pair("cato-client-openbsd-i386", "3e755798628a379997dbc43dfd71765d62263ebe"),
pair("cato-client-windows-amd64.exe", "21a862f455c909c9fb786cd308e969629eb3c8d9"),
pair("cato-client-windows-arm64.exe", "7f2fe5d054ebc3265edb046badafdd6ac94bf7eb"),
pair("cato-client-windows-i386.exe", "4e03e640c6cabfd7e1ddf60bba1726353bc3afee")
);
private MultiplayerManager() {
}
public static Task<Void> downloadCato() {
return new FileDownloadTask(
NetworkUtils.toURL(CATO_DOWNLOAD_URL + getCatoFileName()),
getCatoExecutable().toFile(),
new FileDownloadTask.IntegrityCheck("SHA-1", HASH.get(getCatoFileName()))
).thenRunAsync(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
Set<PosixFilePermission> perm = Files.getPosixFilePermissions(getCatoExecutable());
perm.add(PosixFilePermission.OWNER_EXECUTE);
Files.setPosixFilePermissions(getCatoExecutable(), perm);
}
});
}
public static Path getCatoExecutable() {
return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve(CATO_PATH);
}
private static CompletableFuture<CatoSession> startCato(String token, State state) {
return CompletableFuture.completedFuture(null).thenApplyAsync(wrap(unused -> {
Path exe = getCatoExecutable();
if (!Files.isRegularFile(exe)) {
throw new CatoNotExistsException(exe);
}
if (!isPortAvailable(3478)) {
throw new CatoAlreadyStartedException();
}
try {
ChecksumMismatchException.verifyChecksum(exe, "SHA-1", HASH.get(getCatoFileName()));
} catch (IOException e) {
Files.deleteIfExists(exe);
throw e;
}
String[] commands = new String[]{exe.toString(), "--token", StringUtils.isBlank(token) ? "new" : token};
Process process = new ProcessBuilder()
.command(commands)
.start();
return new CatoSession(state, process, Arrays.asList(commands));
}));
}
public static CompletableFuture<CatoSession> joinSession(String token, String peer, Mode mode, int remotePort, int localPort, JoinSessionHandler handler) throws IncompatibleCatoVersionException {
LOG.info(String.format("Joining session (token=%s,peer=%s,mode=%s,remotePort=%d,localPort=%d)", token, peer, mode, remotePort, localPort));
return startCato(token, State.SLAVE).thenComposeAsync(wrap(session -> {
CompletableFuture<CatoSession> future = new CompletableFuture<>();
session.forwardPort(peer, LOCAL_ADDRESS, localPort, REMOTE_ADDRESS, remotePort, mode);
Consumer<CatoExitEvent> onExit = event -> {
boolean ready = session.isReady();
switch (event.getExitCode()) {
case 1:
if (!ready) {
future.completeExceptionally(new CatoExitTimeoutException());
}
break;
}
future.completeExceptionally(new CatoExitException(event.getExitCode(), ready));
};
session.onExit.register(onExit);
TimerTask peerConnectionTimeoutTask = Lang.setTimeout(() -> {
future.completeExceptionally(new PeerConnectionTimeoutException());
session.stop();
}, 15 * 1000);
session.onPeerConnected.register(event -> {
peerConnectionTimeoutTask.cancel();
MultiplayerClient client = new MultiplayerClient(session.getId(), localPort);
session.addRelatedThread(client);
session.setClient(client);
TimerTask task = Lang.setTimeout(() -> {
future.completeExceptionally(new JoinRequestTimeoutException());
session.stop();
}, 30 * 1000);
client.onConnected().register(connectedEvent -> {
try {
int port = findAvailablePort();
session.forwardPort(peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort(), mode);
session.addRelatedThread(Lang.thread(new LocalServerBroadcaster(port, session), "LocalServerBroadcaster", true));
session.setName(connectedEvent.getSessionName());
client.setGamePort(port);
session.onExit.unregister(onExit);
future.complete(session);
} catch (IOException e) {
future.completeExceptionally(e);
session.stop();
}
task.cancel();
});
client.onKicked().register(kickedEvent -> {
future.completeExceptionally(new KickedException(kickedEvent.getReason()));
session.stop();
task.cancel();
});
client.onDisconnected().register(disconnectedEvent -> {
if (!client.isConnected()) {
// We fail to establish connection with server
future.completeExceptionally(new ConnectionErrorException());
}
});
client.onHandshake().register(handshakeEvent -> {
if (handler != null) {
handler.onWaitingForJoinResponse();
}
});
client.start();
});
return future;
}));
}
public static CompletableFuture<CatoSession> createSession(String token, String sessionName, int gamePort, boolean allowAllJoinRequests) {
LOG.info(String.format("Creating session (token=%s,sessionName=%s,gamePort=%d)", token, sessionName, gamePort));
return startCato(token, State.MASTER).thenComposeAsync(wrap(session -> {
CompletableFuture<CatoSession> future = new CompletableFuture<>();
MultiplayerServer server = new MultiplayerServer(sessionName, gamePort, allowAllJoinRequests);
server.startServer();
session.setName(sessionName);
session.allowForwardingAddress(REMOTE_ADDRESS, server.getPort());
session.allowForwardingAddress(REMOTE_ADDRESS, gamePort);
session.showAllowedAddress();
Consumer<CatoExitEvent> onExit = event -> {
boolean ready = session.isReady();
switch (event.getExitCode()) {
case 1:
if (!ready) {
future.completeExceptionally(new CatoExitTimeoutException());
}
break;
}
future.completeExceptionally(new CatoExitException(event.getExitCode(), ready));
};
session.onExit.register(onExit);
session.setServer(server);
session.addRelatedThread(server);
TimerTask peerConnectionTimeoutTask = Lang.setTimeout(() -> {
future.completeExceptionally(new PeerConnectionTimeoutException());
session.stop();
}, 15 * 1000);
session.onPeerConnected.register(event -> {
session.onExit.unregister(onExit);
future.complete(session);
peerConnectionTimeoutTask.cancel();
});
return future;
}));
}
public static final Pattern INVITATION_CODE_PATTERN = Pattern.compile("^(?<id>.*?)#(?<port>\\d{2,5})$");
public static Invitation parseInvitationCode(String invitationCode) throws JsonParseException {
Matcher matcher = INVITATION_CODE_PATTERN.matcher(invitationCode);
if (!matcher.find()) throw new IllegalArgumentException("Invalid invitation code");
return new Invitation(matcher.group("id"), Integer.parseInt(matcher.group("port")));
}
public static int findAvailablePort() throws IOException {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
}
}
public static boolean isPortAvailable(int port) {
try (ServerSocket socket = new ServerSocket(port)) {
return true;
} catch (IOException e) {
return false;
}
}
private static String getCatoFileName() {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "cato-client-windows-amd64.exe";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "cato-client-windows-arm64.exe";
} else if (Architecture.SYSTEM_ARCH == Architecture.X86) {
return "cato-client-windows-i386.exe";
} else {
return "";
}
case OSX:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "cato-client-darwin-amd64";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "cato-client-darwin-arm64";
} else {
return "";
}
case LINUX:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
return "cato-client-linux-amd64";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM32) {
return "cato-client-linux-arm7";
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
return "cato-client-linux-arm64";
} else {
return "";
}
default:
return "";
}
}
public static String getCatoPath() {
String name = getCatoFileName();
if (StringUtils.isBlank(name)) return "";
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/" + name;
}
public static class CatoSession extends ManagedProcess {
private final EventManager<CatoExitEvent> onExit = new EventManager<>();
private final EventManager<CatoIdEvent> onIdGenerated = new EventManager<>();
private final EventManager<Event> onPeerConnected = new EventManager<>();
private String name;
private final State type;
private String id;
private boolean peerConnected = false;
private MultiplayerClient client;
private MultiplayerServer server;
private final BufferedWriter writer;
CatoSession(State type, Process process, List<String> commands) {
super(process, commands);
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
LOG.info("Started cato with command: " + new CommandBuilder().addAll(commands));
this.type = type;
addRelatedThread(Lang.thread(this::waitFor, "CatoExitWaiter", true));
addRelatedThread(Lang.thread(new StreamPump(process.getInputStream(), this::checkCatoLog), "CatoInputStreamPump", true));
addRelatedThread(Lang.thread(new StreamPump(process.getErrorStream(), this::checkCatoLog), "CatoErrorStreamPump", true));
writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8));
}
public MultiplayerClient getClient() {
return client;
}
public CatoSession setClient(MultiplayerClient client) {
this.client = client;
return this;
}
public MultiplayerServer getServer() {
return server;
}
public CatoSession setServer(MultiplayerServer server) {
this.server = server;
return this;
}
private void checkCatoLog(String log) {
LOG.info("Cato: " + log);
if (id == null) {
Matcher matcher = TEMP_TOKEN_PATTERN.matcher(log);
if (matcher.find()) {
id = matcher.group("id");
onIdGenerated.fireEvent(new CatoIdEvent(this, id));
}
}
if (!peerConnected) {
Matcher matcher = PEER_CONNECTED_PATTERN.matcher(log);
if (matcher.find()) {
peerConnected = true;
onPeerConnected.fireEvent(new Event(this));
}
}
}
private void waitFor() {
try {
int exitCode = getProcess().waitFor();
LOG.info("cato exited with exitcode " + exitCode);
onExit.fireEvent(new CatoExitEvent(this, exitCode));
} catch (InterruptedException e) {
onExit.fireEvent(new CatoExitEvent(this, CatoExitEvent.EXIT_CODE_INTERRUPTED));
} finally {
try {
if (writer != null)
writer.close();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to close cato stdin writer", e);
}
}
destroyRelatedThreads();
}
public boolean isReady() {
return id != null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public State getType() {
return type;
}
@Nullable
public String getId() {
return id;
}
public String generateInvitationCode(int serverPort) {
if (id == null) {
throw new IllegalStateException("id not generated");
}
return id + "#" + serverPort;
}
public synchronized void invokeCommand(String command) throws IOException {
LOG.info("Invoking cato: " + command);
writer.write(command);
writer.newLine();
writer.flush();
}
public void forwardPort(String peerId, String localAddress, int localPort, String remoteAddress, int remotePort, Mode mode) throws IOException {
invokeCommand(String.format("net add %s %s:%d %s:%d %s", peerId, localAddress, localPort, remoteAddress, remotePort, mode.getName()));
}
public void allowForwardingAddress(String address, int port) throws IOException {
invokeCommand(String.format("ufw net open %s:%d", address, port));
}
public void showAllowedAddress() throws IOException {
invokeCommand("ufw net whitelist");
}
public EventManager<CatoExitEvent> onExit() {
return onExit;
}
public EventManager<CatoIdEvent> onIdGenerated() {
return onIdGenerated;
}
public EventManager<Event> onPeerConnected() {
return onPeerConnected;
}
private static final Pattern TEMP_TOKEN_PATTERN = Pattern.compile("id\\((?<id>\\w+)\\)");
private static final Pattern PEER_CONNECTED_PATTERN = Pattern.compile("Connected to main net");
private static final Pattern LOG_PATTERN = Pattern.compile("(\\[\\d+])\\s+(\\w+)\\s+(\\w+-{0,1}\\w+):\\s(.*)");
}
public static class CatoExitEvent extends Event {
private final int exitCode;
public CatoExitEvent(Object source, int exitCode) {
super(source);
this.exitCode = exitCode;
}
public int getExitCode() {
return exitCode;
}
public static final int EXIT_CODE_INTERRUPTED = -1;
public static final int EXIT_CODE_SESSION_EXPIRED = 10;
}
public static class CatoIdEvent extends Event {
private final String id;
public CatoIdEvent(Object source, String id) {
super(source);
this.id = id;
}
public String getId() {
return id;
}
}
enum State {
DISCONNECTED,
CONNECTING,
MASTER,
SLAVE
}
public static class Invitation {
private final String id;
@SerializedName("p")
private final int channelPort;
public Invitation(String id, int channelPort) {
this.id = id;
this.channelPort = channelPort;
}
public String getId() {
return id;
}
public int getChannelPort() {
return channelPort;
}
}
public interface JoinSessionHandler {
void onWaitingForJoinResponse();
}
public static class IncompatibleCatoVersionException extends Exception {
private final String expected;
private final String actual;
public IncompatibleCatoVersionException(String expected, String actual) {
this.expected = expected;
this.actual = actual;
}
public String getExpected() {
return expected;
}
public String getActual() {
return actual;
}
}
public enum Mode {
P2P,
BRIDGE;
String getName() {
return name().toLowerCase(Locale.ROOT);
}
}
public static class CatoExitException extends RuntimeException {
private final int exitCode;
private final boolean ready;
public CatoExitException(int exitCode, boolean ready) {
this.exitCode = exitCode;
this.ready = ready;
}
public int getExitCode() {
return exitCode;
}
public boolean isReady() {
return ready;
}
}
public static class CatoExitTimeoutException extends RuntimeException {
}
public static class CatoSessionExpiredException extends RuntimeException {
}
public static class CatoAlreadyStartedException extends RuntimeException {
}
public static class JoinRequestTimeoutException extends RuntimeException {
}
public static class PeerConnectionTimeoutException extends RuntimeException {
}
public static class ConnectionErrorException extends RuntimeException {
}
public static class KickedException extends RuntimeException {
private final String reason;
public KickedException(String reason) {
this.reason = reason;
}
public String getReason() {
return reason;
}
}
public static class CatoNotExistsException extends RuntimeException {
private final Path file;
public CatoNotExistsException(Path file) {
this.file = file;
}
public Path getFile() {
return file;
}
}
}
@@ -1,515 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import de.javawi.jstun.test.DiscoveryInfo;
import de.javawi.jstun.test.DiscoveryTest;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.Result;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.KickResponse.*;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("multiplayer")));
private final ObjectProperty<MultiplayerManager.State> multiplayerState = new SimpleObjectProperty<>(MultiplayerManager.State.DISCONNECTED);
private final ReadOnlyStringWrapper token = new ReadOnlyStringWrapper();
private final ReadOnlyObjectWrapper<@Nullable Result<DiscoveryInfo>> natState = new ReadOnlyObjectWrapper<>();
private final ReadOnlyIntegerWrapper gamePort = new ReadOnlyIntegerWrapper(-1);
private final ReadOnlyObjectWrapper<MultiplayerManager.CatoSession> session = new ReadOnlyObjectWrapper<>();
private final ObservableList<MultiplayerChannel.CatoClient> clients = FXCollections.observableArrayList();
private Consumer<MultiplayerManager.CatoExitEvent> onExit;
private Consumer<MultiplayerManager.CatoIdEvent> onIdGenerated;
private Consumer<Event> onPeerConnected;
public MultiplayerPage() {
testNAT();
}
@Override
public void onPageShown() {
checkAgreement(this::downloadCatoIfNecessary);
}
@Override
protected Skin<?> createDefaultSkin() {
return new MultiplayerPageSkin(this);
}
public ObservableList<MultiplayerChannel.CatoClient> getClients() {
return clients;
}
public MultiplayerManager.State getMultiplayerState() {
return multiplayerState.get();
}
public ObjectProperty<MultiplayerManager.State> multiplayerStateProperty() {
return multiplayerState;
}
public void setMultiplayerState(MultiplayerManager.State multiplayerState) {
this.multiplayerState.set(multiplayerState);
}
public Result<DiscoveryInfo> getNatState() {
return natState.get();
}
public ReadOnlyObjectProperty<Result<DiscoveryInfo>> natStateProperty() {
return natState.getReadOnlyProperty();
}
public String getToken() {
return token.get();
}
public ReadOnlyStringProperty tokenProperty() {
return token.getReadOnlyProperty();
}
public int getGamePort() {
return gamePort.get();
}
public ReadOnlyIntegerProperty gamePortProperty() {
return gamePort.getReadOnlyProperty();
}
public MultiplayerManager.CatoSession getSession() {
return session.get();
}
public ReadOnlyObjectProperty<MultiplayerManager.CatoSession> sessionProperty() {
return session.getReadOnlyProperty();
}
private void testNAT() {
Task.supplyAsync(() -> {
DiscoveryTest tester = new DiscoveryTest(null, 0, "stun.stunprotocol.org", 3478);
return tester.test();
}).whenComplete(Schedulers.javafx(), (info, exception) -> {
if (exception == null) {
natState.set(Result.ok(info));
} else {
natState.set(Result.error());
}
LOG.log(Level.INFO, "Nat test result " + MultiplayerPageSkin.getNATType(natState.get()), exception);
}).start();
}
private void checkAgreement(Runnable runnable) {
if (globalConfig().getMultiplayerAgreementVersion() < MultiplayerManager.CATO_AGREEMENT_VERSION) {
JFXDialogLayout agreementPane = new JFXDialogLayout();
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
agreementPane.setBody(new Label(i18n("multiplayer.agreement.prompt")));
JFXHyperlink agreementLink = new JFXHyperlink(i18n("launcher.agreement"));
agreementLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-agreement"));
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
yesButton.getStyleClass().add("dialog-accept");
yesButton.setOnAction(e -> {
globalConfig().setMultiplayerAgreementVersion(MultiplayerManager.CATO_AGREEMENT_VERSION);
runnable.run();
agreementPane.fireEvent(new DialogCloseEvent());
});
JFXButton noButton = new JFXButton(i18n("launcher.agreement.decline"));
noButton.getStyleClass().add("dialog-cancel");
noButton.setOnAction(e -> {
agreementPane.fireEvent(new DialogCloseEvent());
fireEvent(new PageCloseEvent());
});
agreementPane.setActions(agreementLink, yesButton, noButton);
Controllers.dialog(agreementPane);
} else {
runnable.run();
}
}
private void downloadCatoIfNecessary() {
if (StringUtils.isBlank(MultiplayerManager.getCatoPath())) {
Controllers.dialog(i18n("multiplayer.download.unsupported"), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
fireEvent(new PageCloseEvent());
return;
}
if (!MultiplayerManager.getCatoExecutable().toFile().exists()) {
setDisabled(true);
TaskExecutor executor = MultiplayerManager.downloadCato()
.whenComplete(Schedulers.javafx(), exception -> {
setDisabled(false);
if (exception != null) {
if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
} else {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
fireEvent(new PageCloseEvent());
}
} else {
Controllers.showToast(i18n("multiplayer.download.success"));
}
}).executor();
Controllers.taskDialog(executor, i18n("multiplayer.download"));
executor.start();
} else {
setDisabled(false);
}
}
public void copyInvitationCode() {
if (getSession() == null || !getSession().isReady() || gamePort.get() < 0 || getMultiplayerState() != MultiplayerManager.State.MASTER) {
throw new IllegalStateException("CatoSession not ready");
}
FXUtils.copyText(getSession().generateInvitationCode(getSession().getServer().getPort()));
}
public void createRoom() {
if (getSession() != null || getMultiplayerState() != MultiplayerManager.State.DISCONNECTED) {
throw new IllegalStateException("CatoSession already ready");
}
Controllers.dialog(new CreateMultiplayerRoomDialog((result, resolve, reject) -> {
int gamePort = result.getServer().getAd();
boolean isStaticToken = StringUtils.isNotBlank(globalConfig().getMultiplayerToken());
MultiplayerManager.createSession(globalConfig().getMultiplayerToken(), result.getServer().getMotd(), gamePort, result.isAllowAllJoinRequests())
.thenAcceptAsync(session -> {
session.getServer().setOnClientAdding((client, resolveClient, rejectClient) -> {
runInFX(() -> {
Controllers.dialog(new MessageDialogPane.Builder(i18n("multiplayer.session.create.join.prompt", client.getUsername()), i18n("multiplayer.session.create.join"), MessageDialogPane.MessageType.INFO)
.yesOrNo(resolveClient, () -> rejectClient.accept(MultiplayerChannel.KickResponse.JOIN_ACEEPTANCE_TIMEOUT))
.cancelOnTimeout(30 * 1000)
.build());
});
});
session.getServer().onClientAdded().register(event -> {
runInFX(() -> {
clients.add(event);
});
});
session.getServer().onClientDisconnected().register(event -> {
runInFX(() -> {
clients.remove(event);
});
});
initCatoSession(session);
this.gamePort.set(gamePort);
setMultiplayerState(MultiplayerManager.State.MASTER);
resolve.run();
}, Platform::runLater)
.exceptionally(throwable -> {
reject.accept(localizeCreateErrorMessage(throwable, isStaticToken));
return null;
});
}));
}
public void joinRoom() {
if (getSession() != null || getMultiplayerState() != MultiplayerManager.State.DISCONNECTED) {
throw new IllegalStateException("CatoSession already ready");
}
Controllers.prompt(new PromptDialogPane.Builder(i18n("multiplayer.session.join"), (result, resolve, reject) -> {
PromptDialogPane.Builder.HintQuestion hintQuestion = (PromptDialogPane.Builder.HintQuestion) result.get(0);
boolean isStaticToken = StringUtils.isNotBlank(globalConfig().getMultiplayerToken());
String invitationCode = ((PromptDialogPane.Builder.StringQuestion) result.get(1)).getValue();
MultiplayerManager.Invitation invitation;
try {
invitation = MultiplayerManager.parseInvitationCode(invitationCode);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to join session", e);
reject.accept(i18n("multiplayer.session.join.invitation_code.error"));
return;
}
int localPort; // invitation channel
try {
localPort = MultiplayerManager.findAvailablePort();
} catch (Exception e) {
reject.accept(i18n("multiplayer.session.join.port.error"));
return;
}
try {
MultiplayerManager.joinSession(
globalConfig().getMultiplayerToken(),
invitation.getId(),
globalConfig().isMultiplayerRelay() && (StringUtils.isNotBlank(globalConfig().getMultiplayerToken()) || StringUtils.isNotBlank(System.getProperty("hmcl.multiplayer.relay")))
? MultiplayerManager.Mode.BRIDGE
: MultiplayerManager.Mode.P2P,
invitation.getChannelPort(),
localPort, new MultiplayerManager.JoinSessionHandler() {
@Override
public void onWaitingForJoinResponse() {
runInFX(() -> {
hintQuestion.setQuestion(i18n("multiplayer.session.join.wait"));
});
}
})
.thenAcceptAsync(session -> {
initCatoSession(session);
AtomicBoolean kicked = new AtomicBoolean();
session.getClient().onDisconnected().register(() -> {
runInFX(() -> {
stopCatoSession();
if (!kicked.get()) {
Controllers.dialog(i18n("multiplayer.session.join.lost_connection"));
}
});
});
session.getClient().onKicked().register(kickedEvent -> {
runInFX(() -> {
kicked.set(true);
Controllers.dialog(i18n("multiplayer.session.join.kicked", localizeKickMessage(kickedEvent.getReason())));
});
});
gamePort.set(session.getClient().getGamePort());
setMultiplayerState(MultiplayerManager.State.SLAVE);
resolve.run();
}, Platform::runLater)
.exceptionally(throwable -> {
reject.accept(localizeJoinErrorMessage(throwable, isStaticToken));
return null;
});
} catch (MultiplayerManager.IncompatibleCatoVersionException e) {
reject.accept(i18n("multiplayer.session.join.invitation_code.version"));
}
})
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("multiplayer.session.join.hint")))
.addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("multiplayer.session.join.invitation_code"), "", new RequiredValidator())));
}
private String localizeKickMessage(String message) {
if (VERSION_NOT_MATCHED.equals(message)) {
return i18n("multiplayer.session.join.kicked.version_not_matched");
} else if (KICKED.equals(message)) {
return i18n("multiplayer.session.join.kicked.kicked");
} else if (JOIN_ACEEPTANCE_TIMEOUT.equals(message)) {
return i18n("multiplayer.session.join.kicked.join_acceptance_timeout");
} else {
return message;
}
}
private String localizeErrorMessage(Throwable t, boolean isStaticToken, Function<Throwable, String> fallback) {
Throwable e = resolveException(t);
if (e instanceof CancellationException) {
LOG.info("Connection rejected by the server");
return i18n("message.cancelled");
} else if (e instanceof MultiplayerManager.KickedException) {
LOG.info("Kicked by server");
return i18n("multiplayer.session.join.kicked", localizeKickMessage(((MultiplayerManager.KickedException) e).getReason()));
} else if (e instanceof MultiplayerManager.CatoAlreadyStartedException) {
LOG.info("Cato already started");
return i18n("multiplayer.session.error.already_started");
} else if (e instanceof MultiplayerManager.CatoNotExistsException) {
LOG.log(Level.WARNING, "Cato not found " + ((MultiplayerManager.CatoNotExistsException) e).getFile(), e);
return i18n("multiplayer.session.error.file_not_found");
} else if (e instanceof MultiplayerManager.JoinRequestTimeoutException) {
LOG.info("Cato already started");
return i18n("multiplayer.session.join.wait_timeout");
} else if (e instanceof MultiplayerManager.ConnectionErrorException) {
LOG.info("Failed to establish connection with server");
return i18n("multiplayer.session.join.error.connection");
} else if (e instanceof MultiplayerManager.CatoExitTimeoutException) {
LOG.info("Cato failed to connect to main net");
if (isStaticToken) {
return i18n("multiplayer.exit.timeout.static_token");
} else {
return i18n("multiplayer.exit.timeout.dynamic_token");
}
} else if (e instanceof MultiplayerManager.CatoExitException) {
LOG.info("Cato exited accidentally");
if (!((MultiplayerManager.CatoExitException) e).isReady()) {
return i18n("multiplayer.exit.before_ready");
} else {
return i18n("multiplayer.exit.after_ready");
}
} else if (e instanceof ChecksumMismatchException) {
return i18n("exception.artifact_malformed");
} else {
return fallback.apply(e);
}
}
private String localizeCreateErrorMessage(Throwable t, boolean isStaticToken) {
return localizeErrorMessage(t, isStaticToken, e -> {
LOG.log(Level.WARNING, "Failed to create session", e);
if (isStaticToken) {
return i18n("multiplayer.session.create.error.static_token") + e.getLocalizedMessage();
} else {
return i18n("multiplayer.session.create.error.dynamic_token") + e.getLocalizedMessage();
}
});
}
private String localizeJoinErrorMessage(Throwable t, boolean isStaticToken) {
return localizeErrorMessage(t, isStaticToken, e -> {
LOG.log(Level.WARNING, "Failed to join session", e);
return i18n("multiplayer.session.join.error");
});
}
public void kickPlayer(MultiplayerChannel.CatoClient client) {
if (getSession() == null || !getSession().isReady() || getMultiplayerState() != MultiplayerManager.State.MASTER) {
throw new IllegalStateException("CatoSession not ready");
}
Controllers.confirm(i18n("multiplayer.session.create.members.kick.prompt"), i18n("multiplayer.session.create.members.kick"), MessageDialogPane.MessageType.WARNING,
() -> {
getSession().getServer().kickPlayer(client);
}, null);
}
public void closeRoom() {
if (getSession() == null || !getSession().isReady() || getMultiplayerState() != MultiplayerManager.State.MASTER) {
throw new IllegalStateException("CatoSession not ready");
}
Controllers.confirm(i18n("multiplayer.session.close.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING,
this::stopCatoSession, null);
}
public void quitRoom() {
if (getSession() == null || !getSession().isReady() || getMultiplayerState() != MultiplayerManager.State.SLAVE) {
throw new IllegalStateException("CatoSession not ready");
}
Controllers.confirm(i18n("multiplayer.session.quit.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING,
this::stopCatoSession, null);
}
public void cancelRoom() {
if (getSession() == null || getSession().isReady() || getMultiplayerState() != MultiplayerManager.State.CONNECTING) {
throw new IllegalStateException("CatoSession not existing or already ready");
}
stopCatoSession();
}
private void initCatoSession(MultiplayerManager.CatoSession session) {
runInFX(() -> {
onExit = session.onExit().registerWeak(this::onCatoExit);
onIdGenerated = session.onIdGenerated().registerWeak(this::onCatoIdGenerated);
onPeerConnected = session.onPeerConnected().registerWeak(this::onCatoPeerConnected);
this.clients.clear();
this.session.set(session);
});
}
private void stopCatoSession() {
if (getSession() != null) {
getSession().stop();
}
clearCatoSession();
}
private void clearCatoSession() {
this.session.set(null);
this.token.set(null);
this.gamePort.set(-1);
this.multiplayerState.set(MultiplayerManager.State.DISCONNECTED);
}
private void onCatoExit(MultiplayerManager.CatoExitEvent event) {
runInFX(() -> {
boolean ready = ((MultiplayerManager.CatoSession) event.getSource()).isReady();
switch (event.getExitCode()) {
case 0:
break;
case MultiplayerManager.CatoExitEvent.EXIT_CODE_SESSION_EXPIRED:
Controllers.dialog(i18n("multiplayer.session.expired"));
break;
case 1:
if (!ready) {
Controllers.dialog(i18n("multiplayer.exit.timeout"));
}
break;
case -1:
// do nothing
break;
default:
if (!((MultiplayerManager.CatoSession) event.getSource()).isReady()) {
Controllers.dialog(i18n("multiplayer.exit.before_ready", event.getExitCode()));
} else {
Controllers.dialog(i18n("multiplayer.exit.after_ready", event.getExitCode()));
}
break;
}
clearCatoSession();
});
}
private void onCatoPeerConnected(Event event) {
runInFX(() -> {
});
}
private void onCatoIdGenerated(MultiplayerManager.CatoIdEvent event) {
runInFX(() -> {
token.set(event.getId());
setMultiplayerState(((MultiplayerManager.CatoSession) event.getSource()).getType());
});
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
}
@@ -1,341 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPasswordField;
import de.javawi.jstun.test.DiscoveryInfo;
import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.Result;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimatedPageSkin<MultiplayerPage> {
private ObservableList<Node> clients;
/**
* Constructor for all SkinBase instances.
*
* @param control The control for which this Skin should attach to.
*/
protected MultiplayerPageSkin(MultiplayerPage control) {
super(control);
{
VBox roomPane = new VBox();
{
AdvancedListItem createRoomItem = new AdvancedListItem();
createRoomItem.setTitle(i18n("multiplayer.session.create"));
createRoomItem.setLeftGraphic(wrap(SVG::plusCircleOutline));
createRoomItem.setActionButtonVisible(false);
createRoomItem.setOnAction(e -> control.createRoom());
AdvancedListItem joinRoomItem = new AdvancedListItem();
joinRoomItem.setTitle(i18n("multiplayer.session.join"));
joinRoomItem.setLeftGraphic(wrap(SVG::accountArrowRightOutline));
joinRoomItem.setActionButtonVisible(false);
joinRoomItem.setOnAction(e -> control.joinRoom());
AdvancedListItem copyLinkItem = new AdvancedListItem();
copyLinkItem.setTitle(i18n("multiplayer.session.copy_room_code"));
copyLinkItem.setLeftGraphic(wrap(SVG::accountArrowRightOutline));
copyLinkItem.setActionButtonVisible(false);
copyLinkItem.setOnAction(e -> control.copyInvitationCode());
AdvancedListItem cancelItem = new AdvancedListItem();
cancelItem.setTitle(i18n("button.cancel"));
cancelItem.setLeftGraphic(wrap(SVG::closeCircle));
cancelItem.setActionButtonVisible(false);
cancelItem.setOnAction(e -> control.cancelRoom());
AdvancedListItem quitItem = new AdvancedListItem();
quitItem.setTitle(i18n("multiplayer.session.quit"));
quitItem.setLeftGraphic(wrap(SVG::closeCircle));
quitItem.setActionButtonVisible(false);
quitItem.setOnAction(e -> control.quitRoom());
AdvancedListItem closeRoomItem = new AdvancedListItem();
closeRoomItem.setTitle(i18n("multiplayer.session.close"));
closeRoomItem.setLeftGraphic(wrap(SVG::closeCircle));
closeRoomItem.setActionButtonVisible(false);
closeRoomItem.setOnAction(e -> control.closeRoom());
FXUtils.onChangeAndOperate(getSkinnable().multiplayerStateProperty(), state -> {
if (state == MultiplayerManager.State.DISCONNECTED) {
roomPane.getChildren().setAll(createRoomItem, joinRoomItem);
} else if (state == MultiplayerManager.State.CONNECTING) {
roomPane.getChildren().setAll(cancelItem);
} else if (state == MultiplayerManager.State.MASTER) {
roomPane.getChildren().setAll(copyLinkItem, closeRoomItem);
} else if (state == MultiplayerManager.State.SLAVE) {
roomPane.getChildren().setAll(quitItem);
}
});
}
AdvancedListBox sideBar = new AdvancedListBox()
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("version.launch"));
item.setLeftGraphic(wrap(SVG::rocketLaunchOutline));
item.setOnAction(e -> {
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, profile.getSelectedVersion(), LauncherHelper::setKeep);
});
})
.startCategory(i18n("multiplayer.session"))
.add(roomPane)
.startCategory(i18n("help"))
.addNavigationDrawerItem(item -> {
item.setTitle(i18n("help"));
item.setLeftGraphic(wrap(SVG::gamepad));
item.setOnAction(e -> FXUtils.openLink("https://hmcl.huangyuhui.net/help/launcher/multiplayer.html"));
})
.addNavigationDrawerItem(report -> {
report.setTitle(i18n("feedback"));
report.setLeftGraphic(wrap(SVG::messageAlertOutline));
report.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-feedback"));
});
FXUtils.setLimitWidth(sideBar, 200);
setLeft(sideBar);
}
{
VBox content = new VBox(16);
content.setPadding(new Insets(10));
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
setCenter(scrollPane);
ComponentList roomPane = new ComponentList();
{
TransitionPane transitionPane = new TransitionPane();
roomPane.getContent().setAll(transitionPane);
VBox disconnectedPane = new VBox(8);
{
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.setText(i18n("multiplayer.state.disconnected.hint"));
Label label = new Label(i18n("multiplayer.state.disconnected"));
disconnectedPane.getChildren().setAll(hintPane, label);
}
VBox connectingPane = new VBox(8);
{
Label label = new Label(i18n("multiplayer.state.connecting"));
connectingPane.getChildren().setAll(label);
}
VBox masterPane = new VBox(8);
{
HintPane masterHintPane = new HintPane();
masterHintPane.setText(i18n("multiplayer.state.master.hint"));
Label label = new Label(i18n("multiplayer.state.master"));
label.textProperty().bind(Bindings.createStringBinding(() ->
i18n("multiplayer.state.master", control.getSession() == null ? "" : control.getSession().getName(), control.getGamePort()),
control.gamePortProperty(), control.sessionProperty()));
Label membersLabel = new Label(i18n("multiplayer.session.create.members"));
VBox clientsPane = new VBox(8);
clients = MappedObservableList.create(control.getClients(), ClientItem::new);
Bindings.bindContent(clientsPane.getChildren(), clients);
masterPane.getChildren().setAll(masterHintPane, label, membersLabel, clientsPane);
}
BorderPane slavePane = new BorderPane();
{
HintPane slaveHintPane = new HintPane();
slaveHintPane.setText(i18n("multiplayer.state.slave.hint"));
slavePane.setTop(slaveHintPane);
Label label = new Label();
label.textProperty().bind(Bindings.createStringBinding(() ->
i18n("multiplayer.state.slave", control.getSession() == null ? "" : control.getSession().getName(), "0.0.0.0:" + control.getGamePort()),
control.sessionProperty(), control.gamePortProperty()));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
slavePane.setCenter(label);
JFXButton copyButton = new JFXButton(i18n("multiplayer.state.slave.copy"));
copyButton.setOnAction(e -> FXUtils.copyText("0.0.0.0:" + control.getGamePort()));
slavePane.setRight(copyButton);
}
FXUtils.onChangeAndOperate(getSkinnable().multiplayerStateProperty(), state -> {
if (state == MultiplayerManager.State.DISCONNECTED) {
transitionPane.setContent(disconnectedPane, ContainerAnimations.NONE.getAnimationProducer());
} else if (state == MultiplayerManager.State.CONNECTING) {
transitionPane.setContent(connectingPane, ContainerAnimations.NONE.getAnimationProducer());
} else if (state == MultiplayerManager.State.MASTER) {
transitionPane.setContent(masterPane, ContainerAnimations.NONE.getAnimationProducer());
} else if (state == MultiplayerManager.State.SLAVE) {
transitionPane.setContent(slavePane, ContainerAnimations.NONE.getAnimationProducer());
}
});
}
ComponentList natDetectionPane = new ComponentList();
{
GridPane pane = new GridPane();
ColumnConstraints title = new ColumnConstraints();
ColumnConstraints value = new ColumnConstraints();
pane.getColumnConstraints().setAll(title, value);
value.setFillWidth(true);
value.setHgrow(Priority.ALWAYS);
pane.setHgap(16);
pane.setVgap(8);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.setText(i18n("multiplayer.nat.hint"));
GridPane.setColumnSpan(hintPane, 2);
pane.addRow(0, hintPane);
Label natResult = new Label();
natResult.textProperty().bind(BindingMapping.of(getSkinnable().natStateProperty())
.map(MultiplayerPageSkin::getNATType));
pane.addRow(1, new Label(i18n("multiplayer.nat.type")), natResult);
natDetectionPane.getContent().add(pane);
}
ComponentList thanksPane = new ComponentList();
{
GridPane gridPane = new GridPane();
gridPane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing(), new ColumnConstraints());
gridPane.setVgap(8);
gridPane.setHgap(16);
// Token acts like password, we hide it here preventing users from accidentally leaking their token when taking screenshots.
JFXPasswordField tokenField = new JFXPasswordField();
tokenField.textProperty().bindBidirectional(globalConfig().multiplayerTokenProperty());
tokenField.setPromptText(i18n("multiplayer.session.create.token.prompt"));
JFXHyperlink applyLink = new JFXHyperlink(i18n("multiplayer.session.create.token.apply"));
applyLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-static-token"));
gridPane.addRow(0, new Label(i18n("multiplayer.session.create.token")), tokenField, applyLink);
OptionToggleButton relay = new OptionToggleButton();
relay.disableProperty().bind(tokenField.textProperty().isEmpty());
relay.selectedProperty().bindBidirectional(globalConfig().multiplayerRelayProperty());
relay.setTitle(i18n("multiplayer.relay"));
relay.setSubtitle(i18n("multiplayer.relay.hint"));
HBox pane = new HBox();
pane.setAlignment(Pos.CENTER_LEFT);
JFXHyperlink aboutLink = new JFXHyperlink(i18n("about"));
aboutLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-about"));
HBox placeholder = new HBox();
HBox.setHgrow(placeholder, Priority.ALWAYS);
pane.getChildren().setAll(
new Label("cato " + MultiplayerManager.CATO_VERSION),
aboutLink,
placeholder,
FXUtils.segmentToTextFlow(i18n("multiplayer.powered_by"), Controllers::onHyperlinkAction));
thanksPane.getContent().addAll(gridPane, relay, pane);
}
content.getChildren().setAll(
ComponentList.createComponentListTitle(i18n("multiplayer.session")),
roomPane,
ComponentList.createComponentListTitle(i18n("multiplayer.nat")),
natDetectionPane,
ComponentList.createComponentListTitle(i18n("settings")),
thanksPane
);
}
}
public static String getNATType(@Nullable Result<DiscoveryInfo> info) {
if (info == null) {
return i18n("multiplayer.nat.testing");
} else if (info.isError()) {
return i18n("multiplayer.nat.failed");
} else if (info.get().isBlockedUDP()) {
return i18n("multiplayer.nat.type.blocked_udp");
} else if (info.get().isFullCone()) {
return i18n("multiplayer.nat.type.full_cone");
} else if (info.get().isOpenAccess()) {
return i18n("multiplayer.nat.type.open_access");
} else if (info.get().isPortRestrictedCone()) {
return i18n("multiplayer.nat.type.port_restricted_cone");
} else if (info.get().isRestrictedCone()) {
return i18n("multiplayer.nat.type.restricted_cone");
} else if (info.get().isSymmetric()) {
return i18n("multiplayer.nat.type.symmetric");
} else if (info.get().isSymmetricUDPFirewall()) {
return i18n("multiplayer.nat.type.symmetric_udp_firewall");
} else {
return i18n("multiplayer.nat.type.unknown");
}
}
private class ClientItem extends StackPane {
ClientItem(MultiplayerChannel.CatoClient client) {
BorderPane pane = new BorderPane();
pane.setPadding(new Insets(8));
pane.setLeft(new Label(client.getUsername()));
JFXButton kickButton = new JFXButton();
kickButton.setGraphic(SVG.close(Theme.blackFillBinding(), 16, 16));
kickButton.getStyleClass().add("toggle-icon-tiny");
kickButton.setOnAction(e -> getSkinnable().kickPlayer(client));
pane.setRight(kickButton);
RipplerContainer container = new RipplerContainer(pane);
getChildren().setAll(container);
getStyleClass().add("md-list-cell");
}
}
}
@@ -1,238 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.*;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class MultiplayerServer extends Thread {
private ServerSocket socket;
private final String sessionName;
private final int gamePort;
private final boolean allowAllJoinRequests;
private FutureCallback<CatoClient> onClientAdding;
private final EventManager<MultiplayerChannel.CatoClient> onClientAdded = new EventManager<>();
private final EventManager<MultiplayerChannel.CatoClient> onClientDisconnected = new EventManager<>();
private final EventManager<Event> onKeepAlive = new EventManager<>();
private final EventManager<Event> onHandshake = new EventManager<>();
private final Map<String, Endpoint> clients = new ConcurrentHashMap<>();
private final Map<String, Endpoint> nameClientMap = new ConcurrentHashMap<>();
public MultiplayerServer(String sessionName, int gamePort, boolean allowAllJoinRequests) {
this.sessionName = sessionName;
this.gamePort = gamePort;
this.allowAllJoinRequests = allowAllJoinRequests;
setName("MultiplayerServer");
setDaemon(true);
}
public void setOnClientAdding(FutureCallback<CatoClient> callback) {
onClientAdding = callback;
}
public EventManager<MultiplayerChannel.CatoClient> onClientAdded() {
return onClientAdded;
}
public EventManager<MultiplayerChannel.CatoClient> onClientDisconnected() {
return onClientDisconnected;
}
public EventManager<Event> onKeepAlive() {
return onKeepAlive;
}
public EventManager<Event> onHandshake() {
return onHandshake;
}
public void startServer() throws IOException {
startServer(0);
}
public void startServer(int port) throws IOException {
if (socket != null) {
throw new IllegalStateException("MultiplayerServer already started");
}
socket = new ServerSocket(port);
start();
}
public int getPort() {
if (socket == null) {
throw new IllegalStateException("MultiplayerServer not started");
}
return socket.getLocalPort();
}
@Override
public void run() {
LOG.info("Multiplayer Server listening 127.0.0.1:" + socket.getLocalPort());
try {
while (!isInterrupted()) {
Socket clientSocket = socket.accept();
clientSocket.setSoTimeout(10000);
Lang.thread(() -> handleClient(clientSocket), "MultiplayerServerClientThread", true);
}
} catch (IOException ignored) {
}
}
public void kickPlayer(CatoClient player) {
Endpoint client = nameClientMap.get(player.getUsername());
if (client == null) return;
try {
if (client.socket.isConnected()) {
client.write(new KickResponse(KickResponse.KICKED));
client.socket.close();
}
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to kick player " + player.getUsername() + ". Maybe already disconnected?", e);
}
}
private void handleClient(Socket targetSocket) {
String address = targetSocket.getRemoteSocketAddress().toString();
String clientName = null;
LOG.info("Accepted client " + address);
try (Socket clientSocket = targetSocket;
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8))) {
clientSocket.setKeepAlive(true);
Endpoint endpoint = new Endpoint(clientSocket, writer);
clients.put(address, endpoint);
String line;
while ((line = reader.readLine()) != null) {
if (isInterrupted()) {
return;
}
LOG.fine("Message from client " + targetSocket.getRemoteSocketAddress() + ":" + line);
MultiplayerChannel.Request request = JsonUtils.fromNonNullJson(line, MultiplayerChannel.Request.class);
if (request instanceof JoinRequest) {
JoinRequest joinRequest = (JoinRequest) request;
LOG.info("Received join request with clientVersion=" + joinRequest.getClientVersion() + ", id=" + joinRequest.getUsername());
clientName = joinRequest.getUsername();
if (!Objects.equals(MultiplayerManager.CATO_VERSION, joinRequest.getClientVersion())) {
try {
endpoint.write(new KickResponse(KickResponse.VERSION_NOT_MATCHED));
LOG.info("Rejected join request from id=" + joinRequest.getUsername());
socket.close();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to send kick response.", e);
return;
}
}
CatoClient catoClient = new CatoClient(this, clientName);
nameClientMap.put(clientName, endpoint);
onClientAdded.fireEvent(catoClient);
if (onClientAdding != null && !allowAllJoinRequests) {
onClientAdding.call(catoClient, () -> {
try {
endpoint.write(new JoinResponse(sessionName, gamePort));
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to send join response.", e);
try {
socket.close();
} catch (IOException ioException) {
LOG.log(Level.WARNING, "Failed to close socket caused by join response sending failure.", e);
this.interrupt();
}
}
}, msg -> {
try {
endpoint.write(new KickResponse(msg));
LOG.info("Rejected join request from id=" + joinRequest.getUsername());
socket.close();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to send kick response.", e);
}
});
} else {
// Allow all join requests.
endpoint.write(new JoinResponse(sessionName, gamePort));
}
} else if (request instanceof KeepAliveRequest) {
endpoint.write(new KeepAliveResponse(System.currentTimeMillis()));
onKeepAlive.fireEvent(new Event(this));
} else if (request instanceof HandshakeRequest) {
endpoint.write(new HandshakeResponse());
onHandshake.fireEvent(new Event(this));
} else {
LOG.log(Level.WARNING, "Unrecognized packet from client " + targetSocket.getRemoteSocketAddress() + ":" + line);
}
}
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to handle client socket.", e);
} catch (JsonParseException e) {
LOG.log(Level.SEVERE, "Failed to parse client request. This should not happen.", e);
} finally {
if (clientName != null) {
onClientDisconnected.fireEvent(new CatoClient(this, clientName));
}
clients.remove(address);
if (clientName != null) nameClientMap.remove(clientName);
}
}
public static class Endpoint {
public final Socket socket;
public final BufferedWriter writer;
public Endpoint(Socket socket, BufferedWriter writer) {
this.socket = socket;
this.writer = writer;
}
public synchronized void write(Object object) throws IOException {
writer.write(verifyJson(JsonUtils.UGLY_GSON.toJson(object)));
writer.newLine();
writer.flush();
}
}
}
Loaded 100 of 342 files, more files were not shown because too many files have changed in this diff. Show more