Compare commits

...
Author SHA1 Message Date
layue13andClaude Sonnet 5.5 f176fbd1fd chore: remove the Java runtime that was committed by mistake and ignore /java/
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Java CI / build (pull_request) Canceled after 0s
A launcher run from the repository root downloads Mojang Java into java/, and it was
picked up by git add -A in e12c7e53f. The files stay in history.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 17:12:22 +08:00
layue13 23811dad7f Merge pull request 'ci: 减少重复构建并加缓存' (#5) from ci/faster-builds into main
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Java CI / build (push) Canceled after 0s
2026-11-08 08:30:18 +00:00
layue13andClaude Sonnet 5.5 e792bfc554 ci: try installing xvfb before refreshing the package index
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Java CI / build (pull_request) Failing after 16s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 16:21:40 +08:00
layue13andClaude Sonnet 5.5 d6b4af2d96 ci: one workflow run per change, cancel superseded runs, cache the Gradle distribution
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Java CI / build (pull_request) Canceled after 0s
Pushes to feature branches built twice (push and pull request) and Check Codes
repeated the compile in a second workflow. Build now runs only on main, tags and
pull requests, cancels older runs of the same ref, folds checkstyle and
checkTranslations into the single Gradle invocation, caches the wrapper download
and skips apt when xvfb is installed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 16:19:50 +08:00
layue13 2a915460ae Merge pull request 'fix: 账号浮层添加账号入口,统一账号页与设置页样式' (#4) from fix/account-add-when-empty into main
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 7m28s
Java CI / build (push) Failing after 3h0m3s
2026-11-08 08:18:58 +00:00
layue13andClaude Sonnet 5.5 e9b577f66f style: light cards and lists, sans section titles, hairline outlined buttons, quieter sidebar labels
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Java CI / build (push) Failing after 2m7s
Check Codes / build (push) Successful in 5m52s
Check Codes / build (pull_request) Successful in 7m0s
Java CI / build (pull_request) Failing after 3h0m10s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 16:08:31 +08:00
layue13andClaude Sonnet 5.5 e12c7e53f6 fix: keep the add-account action in the account popup when there are no accounts
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Java CI / build (push) Failing after 2m30s
Check Codes / build (push) Successful in 4m58s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 15:50:52 +08:00
layue13 61caf8a925 Merge pull request 'feat: 服务器专用启动器(一键进服、新界面、精简)' (#3) from feat/server-launcher-redesign into main
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Canceled after 7m7s
Java CI / build (push) Failing after 3h0m4s
2026-11-08 07:31:54 +00:00
layue13andClaude Sonnet 5.5 a7123a389e style: fix checkstyle indentation, unused imports and a Classical Chinese spelling check
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 8m11s
Check Codes / build (pull_request) Successful in 9m17s
Java CI / build (push) Canceled after 53m53s
Java CI / build (pull_request) Canceled after 54m7s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 15:03:48 +08:00
layue13andClaude Sonnet 5.5 3a6e80da0e refactor: remove multi-instance list pages and unused wizard pages
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Failing after 3m52s
Check Codes / build (pull_request) Failing after 2m45s
Java CI / build (push) Canceled after 1h0m4s
Java CI / build (pull_request) Canceled after 56m22s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 14:57:06 +08:00
layue13andClaude Sonnet 5.5 2c8446fa33 refactor: replace the 1190-line theme system with a fixed warm theme; drop theme packs, wallpapers and their tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 14:56:14 +08:00
layue13andClaude Sonnet 5.5 21fcac1b0c refactor: single settings page; hide inherit-from-global toggles, drop global game settings tab
Check Codes / build (push) Failing after 3m51s
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Java CI / build (push) Canceled after 1h8m30s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 14:42:22 +08:00
layue13andClaude Sonnet 5.5 095d71fea7 feat: show server status (players, latency) on the home card via Server List Ping
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 14:40:43 +08:00
layue13andClaude Sonnet 5.5 07ff244cd2 feat: platform-style window buttons, 21px dialog radius, thin round progress bar, title mark
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 14:33:44 +08:00
layue13andClaude Sonnet 5.5 0269e54b95 feat: route switcher on home card, subtler transitions, flat press feedback; add ServerEndpoint tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 14:27:04 +08:00
layue13andClaude Sonnet 5.5 06ef27f0e0 feat: lightweight first-run dialog for picking a game name or signing in
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 14:22:04 +08:00
layue13andClaude Sonnet 5.5 b3f649c026 feat: account popup above the bottom-left entry with add and manage actions
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 14:21:16 +08:00
layue13andClaude Sonnet 5.5 0531d0cbc2 feat: sidebar with modpack, settings and bottom-left account; server name title
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 14:13:03 +08:00
layue13andClaude Sonnet 5.5 84b6d50421 test: point JFXSwitchTest at the warm palette
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 13:58:17 +08:00
layue13andClaude Sonnet 5.5 dfdad64e93 feat: warm cream palette, new home page with line art and join card, 987x610 window
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 13:58:07 +08:00
layue13andClaude Sonnet 5.5 323d09b104 feat: bundle Noto Sans SC and Noto Serif SC (OFL) as default launcher fonts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 05:56:03 +08:00
layue13andClaude Sonnet 5.5 0f85782312 refactor: drop forced authlib account on first run and unofficial-build hint
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 05:50:45 +08:00
layue13andClaude Sonnet 5.5 7b79945391 feat: multiple server routes, ws(s) addresses passed verbatim to --server
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 05:43:00 +08:00
layue13andClaude Sonnet 5.5 441ce7220f feat: one-click join server button using manifest serverAddress
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 05:29:26 +08:00
layue13andClaude Sonnet 5.5 e8df66dcc0 refactor: drop unused UI controls
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 05:28:31 +08:00
layue13andClaude Sonnet 5.5 f7420e284d refactor: remove personalization settings page
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 05:28:12 +08:00
layue13andClaude Sonnet 5.5 a2fa7ed5ab refactor: remove mod, resource pack, world, data pack and installer pages
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 05:27:38 +08:00
浩 刘 3f67b6bda0 fix: store managed Java under the launcher workspace
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 2m48s
Java CI / build (push) Failing after 3h0m2s
Install Mojang/runtime downloads into <working-dir>/java beside
.minecraft instead of the user-global HMCL profile java store.
2026-11-07 20:35:13 +00:00
浩 刘 4cd783d6bb feat: offer Offline and Microsoft auth; keep authlib injector
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 4m59s
Java CI / build (push) Failing after 3h0m0s
Primary account creation entries are Offline and Microsoft. External
Yggdrasil / authlib-injector servers remain listed and addable for later
use. Offline creation is no longer redirected away or region-restricted.
2026-11-07 20:27:29 +00:00
浩 刘 4118030d9c feat: auto-fetch Mojang Java 25; strip April Fools, task list, unused locales
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 3m50s
Java CI / build (push) Failing after 3h0m1s
On first-capable startup, download java-runtime-epsilon via the existing
BMCLAPI-mirrored Mojang path when missing (progress dialog only; no prompt).

Remove launcher April Fools prompts/settings/UI tags, the top-right task
center page/button, dead NBT/April Fools images, and unused locale packs
(ar/de/es/ja/ru/uk) to shrink the distributable.
2026-11-07 20:19:40 +00:00
浩 刘 bcc5cda5c0 style: clear checkstyle leftovers from TDLM fork cuts
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 4m39s
Java CI / build (push) Failing after 3h0m3s
Remove unused/redundant imports, fix SettingsPage indentation, and
collapse extra blank lines so checkstyle/checkTranslations can pass on
Gitea Actions without reviewdog.
2026-11-07 19:33:20 +00:00
浩 刘 1147892830 ci: drop unmirrored reviewdog and github-script workflows
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Failing after 10m17s
Java CI / build (push) Failing after 3h1m7s
Gitea act resolves actions from git.nest.potatolab.uk; reviewdog/action-setup
and actions/github-script 404 there (even for conditional steps). Keep Gradle
checkstyle/checkTranslations only; disable PR comment and size-label flows.
2026-11-07 19:16:41 +00:00
浩 刘 a4fff896ea ci: install xvfb before Gradle build on self-hosted runners
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Java CI / build (push) Failing after 3h0m4s
Self-hosted Gitea Actions runners lack xvfb by default, so xvfb-run
fails with exit 127. Install it in the Java CI workflow before build.
2026-11-07 19:02:04 +00:00
layue13 5e2da98061 Merge pull request 'Remove official groups, multiplayer, downloads, help, and self-update' (#1)
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Failing after 2m20s
Java CI / build (push) Failing after 4m0s
Merge pull request #1: Remove official groups, multiplayer, downloads, help, and self-update

Server-client HMCL fork cleanup for TDLM.
2026-11-07 18:56:23 +00:00
浩 刘 2b76d44ab0 Strip NBT, schematics, multi-dir, themes, and download source UI
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Failing after 14s
Check Codes / build (pull_request) Failing after 15s
PR Size Label / label (pull_request_target) Failing after 25s
Java CI / build (push) Failing after 2m48s
Java CI / build (pull_request) Failing after 6m9s
Remove NBT editor and schematic management, collapse game directories to the
fixed workspace .minecraft path, drop Countly packaging and theme-pack
marketplace UI, and hide extra download-source selectors for the server client.
2026-11-07 18:53:31 +00:00
浩 刘 cf44b2e536 Prefer Java 25 and multi China download mirrors
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Failing after 57s
Check Codes / build (pull_request) Failing after 17s
Java CI / build (push) Failing after 2m27s
PR Size Label / label (pull_request_target) Failing after 23s
Java CI / build (pull_request) Failing after 2m3s
Raise the recommended Java major to 25, default download sources to BMCLAPI
mirrors, probe and rank multiple BMCLAPI entry points, and spread concurrent
file downloads across healthy mirrors with ordered fallback.
2026-11-07 18:49:52 +00:00
浩 刘 895481ce63 Remove dead free-install hub and export UI
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Failing after 5s
Check Codes / build (pull_request) Failing after 10s
PR Size Label / label (pull_request_target) Failing after 37s
Java CI / build (push) Failing after 2m30s
Java CI / build (pull_request) Failing after 2m14s
Collapse DownloadPage to addon install helpers only, drop the orphaned
modpack export wizard package, and remove unused upstream Metadata URLs.
2026-11-07 18:42:51 +00:00
浩 刘 1e45711006 Restrict launcher to bundled server modpack installs
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Failing after 5s
Check Codes / build (pull_request) Failing after 53s
Java CI / build (push) Failing after 2m30s
PR Size Label / label (pull_request_target) Failing after 15s
Java CI / build (pull_request) Failing after 2m34s
Replace free game download with bundled modpack install, drop Help/Feedback
and upstream About links, empty the default authlib list, and remove offline
account creation while keeping Microsoft and custom authlib logins.
2026-11-07 18:40:26 +00:00
浩 刘 f4893d9f75 Remove official groups, multiplayer, downloads, help, and self-update
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
PR Size Label / label (pull_request_target) Failing after 28s
Check Codes / build (pull_request) Failing after 28s
Check Codes / build (push) Failing after 28s
Java CI / build (pull_request) Failing after 4m13s
Java CI / build (push) Failing after 4m14s
Strip Terracotta multiplayer, QQ/Discord community promo, in-launcher
mod/modpack/world download browse flows, the title-bar help button, and
launcher self-update checks/UI so this fork no longer hooks upstream HMCL
update endpoints. Local modpack import/export and resource/shader downloads
remain. Compile and setting unit tests pass.
2026-11-07 18:14:15 +00:00
快乐凤爪王 3e172608bf fix: 避免通过按钮重复刷新模组列表 (#7099) 2026-11-07 21:17:48 +08:00
鱼前三百 f0fcd28bd0 fix(datapack, ui): 删除 DataPackListPageSkin 冗余的选择逻辑 (#7216) 2026-11-07 21:16:51 +08:00
辞庐 5736ea8a24 主页更新气泡添加悬停反馈 (#7215) 2026-11-07 21:07:39 +08:00
3gf8jv4dv b510352154 chore(l10n): refine en translations on instance page toolbar (#7225) 2026-11-07 20:52:19 +08:00
3gf8jv4dv 4efd3a1e7d chore(l10n): refine en translations (#7224) 2026-11-07 20:52:01 +08:00
3gf8jv4dv c892caeba3 chore(l10n): refine zh-hant translations (#7226) 2026-11-07 20:49:58 +08:00
Glavo 3b57add252 Hide horizontal scrollbar in addon updates list (#7235) 2026-11-07 20:38:16 +08:00
Wulian233 e757b6832f 更新游戏版本/种子数据 (#7234) 2026-11-07 20:23:00 +08:00
Glavo a45ccde0a1 Limit addon update cell width in list view (#7230) 2026-11-07 14:13:49 +08:00
鱼前三百 eb14c66e1a feat(ui): 使用 JFXSwitch 改进 DataPackInfoListCell (#7201) 2026-11-06 22:07:05 +08:00
快乐凤爪王 6fa48981d5 feat: 使用 JFXSwitch 改进 ResourcePackListCell (#7200) 2026-11-06 22:05:21 +08:00
鱼前三百 3c89a012b2 refactor(datapack): 将 DataPackListPageSkin 的搜索逻辑修改为和其他列表一致 (#7217) 2026-11-06 21:50:29 +08:00
Glavo a543216e67 Exclude Cleanroom 0.7-alpha and later from available versions (#7218) 2026-11-06 21:47:04 +08:00
辞庐 86b0b855a7 GLFW / SDL -> GLFW/SDL (#6999) 2026-11-06 21:13:47 +08:00
快乐凤爪王 13d5d6b322 fix: 避免在 更换版本 弹窗选择否后关闭版本页面 (#7107) 2026-11-06 21:01:44 +08:00
gunngz cfb0a8ce68 refactor: move legacy checksum validation into Library (#7085) 2026-11-06 21:00:14 +08:00
Wulian233 fe000fb94f 统一模组列表与详情的中文名顺序,并使用模组数据文件中的英文名 (#7070)
Close #7055
2026-11-06 20:50:34 +08:00
快乐凤爪王 29acd01155 fix: 调整 ModInfoListCell 开关的大小与间距 (#7182) 2026-11-06 20:30:50 +08:00
ZZZank 7cf696e2a0 在“回退”(切换版本)按钮不可见时释放其空间 (#7199) 2026-11-06 20:11:33 +08:00
鱼前三百 09ccf73285 feat: 为游戏文件夹添加打开文件夹按钮 (#7198)
fix #7197
2026-11-06 20:07:32 +08:00
鱼前三百 7403c3ff79 fix(modpack-export): 将选择导出位置的时机移动到导出任务开始之前 (#7208)
fix #7207
2026-11-05 21:49:11 +08:00
鱼前三百 b46a07cd45 feat(modpack-export): 为整合包导出添加进度显示 (#7206)
fix #7205
2026-11-05 21:04:36 +08:00
Glavo a262a95408 Refactor tab animation handling for consistency and clarity (#7210) 2026-11-05 20:58:08 +08:00
Glavo 88222af99a Refactor animation methods to simplify signatures (#7203) 2026-11-05 17:53:05 +08:00
Glavo f1d90fe0a1 预热 GameListPage (#7204) 2026-11-05 16:49:51 +08:00
Glavo 52c84ceb29 Optimize caching and layout management for smoother animations (#7195) 2026-11-05 05:23:17 +08:00
Glavo 087b2b678d 修复无法正常启动 Minecraft 13w16a~13w23b 的问题 (#7193) 2026-11-04 22:23:24 +08:00
Glavo 3f6e7fcffd 在深色模式下默认启用原生窗口装饰 (#7192) 2026-11-04 22:12:58 +08:00
快乐凤爪王 ed59611772 feat: 为世界管理页面添加打开存档文件夹按钮 (#7177)
Closes #7175
2026-11-04 20:55:23 +08:00
Wulian233 c100f46b9d 修复悬停在iconed-menu上时鼠标指针错误 (#7178)
Fix #7179
2026-11-04 20:34:50 +08:00
快乐凤爪王 00b1b34f98 chore: tasksEmptyIcon 使用图标 ListAlt (#7185) 2026-11-04 20:30:15 +08:00
Glavo f411840199 在 updateControl 期间禁用 JFXSwitch 动画 (#7189) 2026-11-04 20:24:57 +08:00
Glavo 8d44411302 在关于页面展示机型信息 (#7186) 2026-11-04 16:23:13 +08:00
ShulkerSakura 85540268bd 修复任务列表按钮可重复打开多个任务列表的问题 (#7180) 2026-11-04 00:27:32 +08:00
Glavo e64a7b45b5 重新实现平滑滚动 (#7162) 2026-11-03 21:57:16 +08:00
Glavo 7d89e87dca 支持将任务挂起至后台 (#7030) 2026-11-03 21:46:12 +08:00
快乐凤爪王 8f002e433f fix: 实例列表禁用横向滚动条 (#7176) 2026-11-03 20:58:39 +08:00
快乐凤爪王 a639fbb1c8 feat: 使用 JFXSwitch 改进 ModInfoListCell (#7173) 2026-11-03 20:57:54 +08:00
Glavo 70e6b6ade7 改进硬件探测功能与关于页面 (#7174) 2026-11-03 18:45:01 +08:00
Glavo d3b91d439c 根据 Tab 相对位置选择动画方向 (#7172) 2026-11-03 11:10:45 +08:00
Glavo 8912ee2661 在按压 RipplerContainer 时使用控件变色代替涟漪效果 (#7171) 2026-11-03 11:01:43 +08:00
Glavo 5dd6ab5908 使用 JFXSwitch 代替 JFXToggleButton (#7167) 2026-11-03 06:08:15 +08:00
鱼前三百 57eca838e2 refactor(addon): 为 LocalAddonFile 默认实现 Comparable (#7120)
fix #7161
2026-11-02 22:27:55 +08:00
Glavo 75d070d8c9 Enhance instance manifest handling and add failed task creation method (#7163) 2026-11-02 21:27:50 +08:00
Glavo f81ba320da 在播放 UI 动画前预渲染页面 (#7160) 2026-11-02 20:37:59 +08:00
Glavo 8047775bf8 feat: add support for virtual GPU type in AboutPage (#7158) 2026-11-01 21:54:37 +08:00
Glavo 740d3c5d80 重构 TwoLineListItem (#7153) 2026-11-01 21:48:48 +08:00
Glavo 967c326219 允许修改没有 patches 的原版实例 (#7145) 2026-11-01 21:29:48 +08:00
快乐凤爪王 fdc3500cc4 style: 为 checkstyle 添加了 RegexpSingleline 规则 (#7156) 2026-11-01 21:25:13 +08:00
Glavo 2f17995955 更新关于页面“桌面环境”条目的图标 (#7157) 2026-11-01 21:22:43 +08:00
Glavo ef311560fd Enhance SHA-1 table and update task for version detection (#7155) 2026-11-01 21:10:33 +08:00
Glavo 440b42a8b7 优化关于页面运行环境信息的排版 (#7151) 2026-11-01 15:55:40 +08:00
Glavo 8b706466e7 feat: log process PID in Launcher (#7152) 2026-11-01 15:48:28 +08:00
快乐凤爪王 8e16127845 feat: 改进 GameListCell (#7144) 2026-10-31 21:56:29 +08:00
Glavo bdbf5773a8 调整自动内存分配算法 (#7134) 2026-10-31 13:59:32 +08:00
Glavo 1ca146c615 Bump mesa-loader-windows to 26.2.6 (#7132) 2026-10-30 21:28:12 +08:00
Glavo de8ae88ee4 改进 AVF 虚拟机内的硬件检测逻辑 (#7130) 2026-10-30 17:57:27 +08:00
Glavo ba62160c76 改进 GPU 类型推断逻辑 (#7124) 2026-10-30 02:05:12 +08:00
快乐凤爪王 8e7f36c681 fix: MicrosoftAccountLoginPane 在缺失 Client ID 时禁用二维码登录 (#7119) 2026-10-29 22:22:07 +08:00
快乐凤爪王 b08f3869cf fix: 关于页面的版权条目使用 LinePane (#7122) 2026-10-29 22:01:42 +08:00
Glavo f05647fc82 支持在使用 Panthor 驱动的环境探测 Mali GPU (#7115) 2026-10-29 16:01:59 +08:00
Glavo 67e9826309 改进 CPU 拓扑探测能力 (#7113) 2026-10-29 15:18:38 +08:00
Glavo f1c9b77df5 Refactor SystemInfo and improve hardware info caching and documentation (#7112) 2026-10-29 03:09:30 +08:00
Glavo c3abfcf1d9 更新 amdgpu.ids (#7111) 2026-10-29 02:37:36 +08:00
Glavo 6632a91426 优化 GPU 信息显示逻辑 (#7109) 2026-10-28 21:55:33 +08:00
Glavo ac378ad46a 预热设置页面时预加载游戏预设 (#7105) 2026-10-28 03:38:08 +08:00
Glavo 6bbb1d157c 简化 TabItem (#7106) 2026-10-28 03:19:15 +08:00
Glavo efa1012a1c 重构 TabHeader (#7104) 2026-10-28 02:26:08 +08:00
Glavo 5058c68be8 优化窗口动画性能 (#7103) 2026-10-28 00:52:29 +08:00
鱼前三百 3612f04657 docs: 修复 withRunAsync 文档错误 (#7095) 2026-10-27 22:25:55 +08:00
快乐凤爪王 a2d3bb3b42 chore: 抑制 Decorator 中未经检查的类型转换警告 (#7098) 2026-10-27 21:50:33 +08:00
Glavo d3efc81332 为侧边栏菜单项背景色添加渐变动画 (#7101) 2026-10-27 21:22:47 +08:00
Glavo 6b1799838f 重构 RipplerContainer (#7096) 2026-10-27 20:14:05 +08:00
Glavo 5ad4880182 支持使用 CurseForge CDN 访问 NeoForge 相关文件 (#7092) 2026-10-26 23:05:08 +08:00
Glavo a3781141fe 由 TransitionPane 提供节点预热能力 (#7090) 2026-10-26 21:30:43 +08:00
Glavo ed168e14d6 清理 Controllers (#7082) 2026-10-26 21:11:06 +08:00
Glavo ee81d7cc66 使用 SPDX 文件头声明版权与开源协议 (#7091) 2026-10-26 20:15:54 +08:00
Glavo 299077d1cb 恢复使用页面预热 (#7081) 2026-10-26 02:09:11 +08:00
nekoside 7448ea90b2 fix: handle odd-length UTF-16LE GPU registry values on Windows (#7080) 2026-10-26 02:00:05 +08:00
AlexYang ac578c044f chore: update avatar for yushijinhun and update a URL (#7078) 2026-10-25 21:34:53 +08:00
快乐凤爪王 fc68029adc feat: 帮助页面添加异常提示及刷新功能 (#7047) 2026-10-25 21:32:51 +08:00
Glavo f6a92c1806 过滤模组中文名中的 Emoji (#7077) 2026-10-25 20:55:54 +08:00
Glavo 58eb452e90 在播放 UI 动画前应用 UI 布局 (#7076) 2026-10-25 19:17:39 +08:00
Wulian233 fb8a96a170 优化账户页面「上传/编辑皮肤」按钮文案 (#7050)
Close #3633
2026-10-25 19:02:55 +08:00
快乐凤爪王 ced441a830 refactor: 提取 DownloadListCell (#7075) 2026-10-25 17:30:14 +08:00
Glavo c5693c3450 清理未使用的代码与资源 (#7074) 2026-10-25 16:59:13 +08:00
鱼前三百 bc27eda220 chore(event): 移除过时且远古的进程退出事件,并将下载速度事件与全局 EventBus 解耦 (#7063) 2026-10-25 16:40:47 +08:00
Matt Van Horn 0c40dcdeed fix: honor declared modern Java requirements on legacy Linux instances (#7073) 2026-10-25 16:39:11 +08:00
Glavo 4282147042 优化设备信息的检测与显示逻辑 (#7071) 2026-10-25 16:36:49 +08:00
Glavo 0874c98d21 Fix TLS security tests on JDKs that block compromised certificates (#7065) 2026-10-25 11:42:41 +08:00
ToobLac 0c3c7dd62a chore: Bump commonmark to 0.31.0 & Update deps.json (#7056) 2026-10-25 03:04:20 +08:00
Glavo f663e5568c 在关于页面显示用户环境信息 (#7040) 2026-10-24 21:32:25 +08:00
ToobLac b8c1738f96 perf(ui): MicrosoftAccountLoginPane 在缺失 client id 时直接禁用登录按钮并显示错误提示 (#7054) 2026-10-24 19:38:51 +08:00
Glavo 41c61645e7 Detect instance manifests among multiple JSON files (#7048) 2026-10-24 18:31:52 +08:00
ToobLac 9fc697751c chore(git): 补充 schematic git attr (#7053) 2026-10-24 18:21:30 +08:00
Glavo a4db0532b9 在 default_script.csv 和 sublanguages.csv 中记录数据来源 (#7049) 2026-10-24 14:14:24 +08:00
Minecraft269andMinecraft269 8954139b5e docs: update contributor count from 130 to 140 (#7043)
Co-authored-by: Minecraft269 <Minecraft269@users.noreply.github.com>
2026-10-24 13:52:30 +08:00
Glavo b2f7e724e4 使用 IANA 时区地区映射表推断设备地区 (#7046) 2026-10-24 12:38:08 +08:00
Glavo 0d20910ea3 Block compromised eMudhra CA keys in application TLS validation (#7033) 2026-10-22 21:27:52 +08:00
Glavo 5c90299073 移除下载页面提示 (#7038) 2026-10-22 21:04:39 +08:00
Glavo fc66cbcf96 更新模组数据 (2026-10-22) (#7032) 2026-10-22 02:15:14 +08:00
Glavo e7b9a95df0 为非官方构建添加醒目标识 (#7028) 2026-10-21 21:24:41 +08:00
ToobLac acc9740fa9 perf(addon): 完善 AddonUpdatesPage (#7003) 2026-10-20 21:27:59 +08:00
鱼前三百 5789f82617 fix: 在重新检查更新的时候提示正在检查更新 (#7015)
fix: #7014
2026-10-20 21:25:24 +08:00
Glavo 8827fd06d8 优化地区检测逻辑 (#7020) 2026-10-20 18:06:54 +08:00
Glavo 583e004d71 在中国大陆地区禁用游戏下载功能 (#7017) 2026-10-20 16:37:22 +08:00
Glavo 63207affc6 更新配置文件 Json Schema (#7012) 2026-10-19 20:34:27 +08:00
Glavo 8b147bf20a Windows 平台自动使用 javaw.exe 启动游戏 (#7009) 2026-10-19 20:21:15 +08:00
鱼前三百 15754119d5 fix: 修复原生窗口透明标题栏重复绘制背景 (#7010)
fix #6959
2026-10-19 20:08:39 +08:00
快乐凤爪王 8119324a92 fix: JavaFX Alert 失败时回退到 Swing (#6918) 2026-10-18 21:15:34 +08:00
辞庐 c0ad7764b0 更新用户未购买 Minecraft 时的提示文本 (#6998) 2026-10-18 20:55:43 +08:00
辞庐 3ea054ae31 将 JFXSpinner 改为顺时针旋转 (#7000) 2026-10-18 20:52:20 +08:00
快乐凤爪王 d03ad38e24 fix: ModpackInfoPage 文件选择器关闭后不清空页面栈 (#6992) 2026-10-17 20:38:35 +08:00
快乐凤爪王 93eaee317c feat: 参考 VersionsPage 样式改进 AddonUpdatesPage (#6970) 2026-10-16 21:51:41 +08:00
ToobLac 68b3a9f076 enhance(i18n): 优化拖拽以安装本地整合包的文字提示 (#6893)
Fixes #6891
2026-10-16 21:47:09 +08:00
鱼前三百 c943e4c74f fix: 在模组/资源包文件操作失败时提示 (#6922)
fix #4314, fix #6396
2026-10-16 21:44:50 +08:00
Minecraft269 4dba96e635 chore: 在 Gitee 与 CNB Issue 模板中添加 GitHub Issue 入口 (#6967) 2026-10-16 21:19:32 +08:00
Glavo e521a1b91a Fix #6969: 修复第二次打开游戏下载页面时页面空白的问题 (#6988) 2026-10-16 21:16:18 +08:00
gunngz a7e0fe7499 Fix indeterminate checkbox rendering in modpack export wizard (#6989)
Fixes #6929
2026-10-16 21:15:44 +08:00
鱼前三百andGlavo 4e2d5cac9e chore: 转换 TaskCancellationAction 为函数式接口 (#6991)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-10-16 20:49:23 +08:00
ToobLac 452d030817 refactor(addon): Make LocalAddonFile implement Comparable (#6984) 2026-10-15 20:33:13 +08:00
ToobLac da9ec2c1e1 chore: sync translations (#6983) 2026-10-15 20:31:07 +08:00
Glavo 4ebbb400ab 重构 ModTranslations (#6987) 2026-10-15 20:29:12 +08:00
Glavo 0cfc2471a0 使用 BellSoft Full JDK 运行 GitHub Action (#6978) 2026-10-13 21:13:42 +08:00
Glavo 413a7291ec 修复 DateRangeDialog 调用 JavaFX 21 API 的问题 (#6979) 2026-10-13 20:55:15 +08:00
鱼前三百 e4893a6df9 feat: 缓存 MicrosoftAccount 的 CompleteGameProfile (#6975)
Fix #5055
2026-10-13 20:16:44 +08:00
ToobLac bb16a0b6c0 fix(addon): AddonUpdatePage 显示测试版本复选框无效 (#6976)
Fixes #6974
2026-10-13 20:16:03 +08:00
5768cd446e feat(instance): 截图管理 (#6096)
Resolves #5535

Co-authored-by: Calboot <calboot39@outlook.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: CalbootOnceMore <calboot43@outlook.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-10-11 21:47:13 +08:00
ToobLac 6f406cede8 enhance(addon): addon更新功能优化 (#6531)
Fixes #6940 Fixes #5865 Fixes #3841 Fixes #3705
2026-10-11 21:11:06 +08:00
快乐凤爪王 0597282463 refactor: 调整 实例管理.浏览 列表布局 (#6914) 2026-10-11 20:33:08 +08:00
ToobLac f91fbe29b4 enhance(remote-addon): 合并冗余的依赖类型 & 下载整合包时推荐版本 (#6968) 2026-10-11 20:10:08 +08:00
快乐凤爪王 cccff7effd feat: 为 Java 管理 页面添加了为空提示 (#6961) 2026-10-11 20:09:06 +08:00
Glavo 101257cae8 总是启用 zip 编码探测 (#6964) 2026-10-11 20:06:03 +08:00
快乐凤爪王 a6e74e7c19 fix: 管理已禁用的 Java 页面内容为空时返回上一页 (#6960) 2026-10-10 21:33:21 +08:00
Glavo 615ebe0311 修复安装任务对话框中部分任务分类错误的问题 (#6962) 2026-10-10 21:23:22 +08:00
Wulian233 8b3468e2d9 无更新日志时提示暂未上传更新日志 (#6957) 2026-10-09 21:49:30 +08:00
辞庐andGlavo 7f1a812373 安装整合包时 整合包名称自动去除冒号 (#6911)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-10-09 21:44:12 +08:00
ShulkerSakuraandGlavo 22823151c3 在日志中记录当前进程是否由管理员账户运行 (#4393)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-10-09 21:10:35 +08:00
Glavo c0844834cb 修复安装 NeoForge 时会有两条进度条的问题 (#6958) 2026-10-09 20:25:16 +08:00
快乐凤爪王 7921c7abb5 refactor: 在 JavaDownloadDialog 中将 版本 与 包类型 的 ComboBox 监听 distributionBox (#6889) 2026-10-08 21:08:58 +08:00
辞庐 c610519f87 优化整合包导出页面排序 & 描述 (#6895) 2026-10-08 21:07:24 +08:00
庐庐绫 bb1949f919 feat: 为 JFXComboBoxListViewSkin 添加平滑滚动效果 (#6920) 2026-10-08 21:02:09 +08:00
Zkitefly a1e946bba6 fix(csv): 修复 CSV 转义引号和换行的格式错误 (#6931) 2026-10-08 20:51:58 +08:00
辞庐 c1bf66b972 清理冗余 Liteloader 代码 (#6952) 2026-10-08 20:32:48 +08:00
ToobLac 25952aa514 chores: 转换 LocalAddonFile$Description 为记录类 (#6956) 2026-10-08 20:26:18 +08:00
辞庐 54d8d877be 仅在打包为 EXE 时设置 AppUserModelID (#6955) 2026-10-08 20:24:14 +08:00
ToobLac 662a82f21f refactor(addon): 单例化 RemoteAddonRepository (#6900) 2026-10-05 13:08:42 +08:00
Glavo 3f0fcc1382 Separate window bounds tracking from minimum size configuration (#6905) 2026-10-05 10:57:21 +08:00
gunngz 258556b22c Fix #6600: 为自定义 LWJGL 本地库路径添加目录选择按钮 (#6887) 2026-10-05 10:20:05 +08:00
辞庐 d7e3cc4473 fix: 修改游戏目录名称后不会刷新 (#6904) 2026-10-05 10:09:28 +08:00
Glavo 93d4489b94 更新模组数据 (2026-10-04) (#6941) 2026-10-05 10:09:03 +08:00
Apostrophe 77eee17d36 更新README中contributor的人数至“130人以上”,并提高图片中的数量上限 (#6921) 2026-09-30 20:35:22 +08:00
辞庐 4118fa9729 添加实例文件夹后自动选中 (#6912) 2026-09-30 20:31:38 +08:00
Glavo 83ffc91a88 重构 DownloadProvider (#6919) 2026-09-30 20:26:28 +08:00
neveler fdf883d09d 更新 Chunkbase 种子地图 (#6892) 2026-09-30 20:25:24 +08:00
Glavo 342bcc5438 Bump Gradle to 9.8.0 (#6917) 2026-09-30 20:24:09 +08:00
Glavo 59bcc7fe62 在 JavaFX27+ 环境中使用原生窗口装饰 (#6885) 2026-09-22 20:21:30 +08:00
Glavo 77b42b6f99 使用 WebURL 代替 URI (#6883) 2026-09-20 20:38:59 +08:00
辞庐 2f5594d8c1 Cleanroom 环境移除 icu4j-core-mojang 依赖 (#6886) 2026-09-17 21:57:09 +08:00
13ad50208f [Enhancement] 原理图管理优化 (#6094)
Co-authored-by: Calboot <calboot39@outlook.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: CalbootOnceMore <calboot43@outlook.com>
2026-09-17 21:15:11 +08:00
Glavo 13910c24b7 Add version entry for Minecraft 26.3-rc-3 in versions.txt (#6877) 2026-09-16 21:41:56 +08:00
Glavo cc9bdb62ca Add support for Minecraft 26.3-snapshot-10+ to unofficial supported platforms (#6870) 2026-09-16 20:50:02 +08:00
Glavo baa7c816b7 将翻译 Task 名称的逻辑移动至 I18n 中 (#6868) 2026-09-15 21:55:44 +08:00
Apostrophe 090f0b9822 修复编辑实例文件夹时不修改名称就无法保存的问题 (#6866) 2026-09-15 20:30:31 +08:00
CaveNightingale 6d9ae24912 添加安装阶段对整合包可选文件的支持 (#1771) 2026-09-14 21:51:49 +08:00
Ivan Epikhin d03851b196 i18n: update Russian translation (#6859) 2026-09-14 21:13:22 +08:00
1116 changed files with 69366 additions and 89739 deletions

No files matched your search

+3
View File
@@ -6,3 +6,6 @@ contact_links:
- name: Discord 服务器
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 服务器。
- name: GitHub Issue
url: https://github.com/HMCL-dev/HMCL/issues/new/choose
about: 本仓库为镜像仓库,Bug 报告与功能请求请在 GitHub 上提交。
+3
View File
@@ -59,6 +59,9 @@ Jenkinsfile text eol=lf
# Minecraft schematic
*.litematic binary
*.nbt binary
*.schem binary
*.schematic binary
# Compiled / intermediate build artefacts
*.bin binary
+3
View File
@@ -3,3 +3,6 @@ contact_links:
- name: QQ 群
url: https://docs.hmcl.net/groups.html
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
- name: GitHub Issue
url: https://github.com/HMCL-dev/HMCL/issues/new/choose
about: 本仓库为镜像仓库,Bug 报告与功能请求请在 GitHub 上提交。
-40
View File
@@ -1,40 +0,0 @@
name: Check Codes Comment
on:
workflow_run:
workflows: ["Check Codes"]
types:
- completed
permissions:
actions: read
contents: read
pull-requests: write
jobs:
comment:
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'failure'
steps:
- name: Checkout Script
uses: actions/checkout@v7
with:
sparse-checkout: |
.github/scripts/check-codes-comment.js
sparse-checkout-cone-mode: false
- name: Download Artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: checkstyle-artifact
path: ${{ runner.temp }}/checkstyle-artifact
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Review Comment
uses: actions/github-script@v9
env:
REPORTS_PATH: ${{ runner.temp }}/checkstyle-artifact/ALL_REPORTS.jsonl
PULL_REQUEST_NUMBER_PATH: ${{ runner.temp }}/checkstyle-artifact/PULL_REQUEST_NUMBER
with:
script: |
const script = require("./.github/scripts/check-codes-comment.js");
await script({ github, context, core });
-55
View File
@@ -1,55 +0,0 @@
name: Check Codes
on:
push:
paths:
- '**.java'
- '**.properties'
pull_request:
paths:
- '**.java'
- '**.properties'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup JDK 17
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: '17'
java-package: 'jdk+fx'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-cleanup: never
- name: Check Codes
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
- if: failure() && github.event_name == 'pull_request'
name: Setup reviewdog
uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0
with:
reviewdog_version: v0.21.0
- if: failure() && github.event_name == 'pull_request'
name: Generate Artifact
run: |
echo "${{ github.event.number }}" > PULL_REQUEST_NUMBER
> ALL_REPORTS.jsonl
find . -path "*/build/reports/checkstyle/*.xml" | while read -r xml_file; do
if [ -s "$xml_file" ]; then
echo "Processing $xml_file..."
reviewdog -f=checkstyle -name="checkstyle" -reporter=rdjsonl < "$xml_file" >> ALL_REPORTS.jsonl
else
echo "Skipping empty or missing report: $xml_file"
fi
done
- if: failure() && github.event_name == 'pull_request'
name: Upload Artifact
uses: actions/upload-artifact@v7
with:
name: checkstyle-artifact
path: |
ALL_REPORTS.jsonl
PULL_REQUEST_NUMBER
+33 -4
View File
@@ -1,11 +1,22 @@
name: Java CI
# Pushes to main and pull requests only. A branch with an open pull request used to build twice
# (once for the push, once for the pull request), which kept the runners queued.
on:
push:
branches:
- main
tags:
- '*'
pull_request:
paths-ignore:
- '**.md'
# A newer run for the same pull request or branch replaces the older one.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
@@ -14,17 +25,35 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Set up JDK 17
uses: actions/setup-java@v5
uses: actions/setup-java@v6
with:
distribution: 'zulu'
distribution: 'liberica'
java-version: '17'
java-package: 'jdk+fx'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-cleanup: never
- name: Build with Gradle
run: xvfb-run --auto-servernum ./gradlew build --no-daemon --parallel
# Only main writes the dependency cache; pull requests read it.
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
# The repository's wrapper downloads Gradle itself, so cache that download too.
- name: Cache Gradle distribution
uses: actions/cache@v4
with:
path: |
~/.gradle/wrapper
.gradle/wrapper-neo
key: gradle-dist-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties', 'gradle/wrapper/GradleWrapperNeo.java') }}
- name: Install Xvfb
run: |
# Installing is the slowest setup step, so skip it when the runner already has xvfb, and
# only refresh the package index when a plain install fails.
command -v xvfb-run >/dev/null || sudo apt-get install -y --no-install-recommends xvfb \
|| { sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb; }
# One Gradle run builds, tests and runs the code and translation checks, so the
# project is configured and compiled once instead of in two workflows.
- name: Build and check with Gradle
run: xvfb-run --auto-servernum ./gradlew build checkstyle checkTranslations --no-daemon --parallel
env:
JAVA_TOOL_OPTIONS: -Djava.awt.headless=true
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
+6 -17
View File
@@ -1,11 +1,9 @@
name: PR Size Label
# Disabled on this Gitea fork: act resolves actions/github-script from
# git.nest.potatolab.uk and that mirror 404s. Re-enable after mirroring
# actions/github-script or replacing the labeler with a plain shell step.
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
workflow_dispatch:
permissions:
@@ -16,16 +14,7 @@ permissions:
jobs:
label:
runs-on: ubuntu-latest
if: false
steps:
- name: Checkout scripts
uses: actions/checkout@v7
with:
sparse-checkout: |
.github/scripts/pr-size-label.js
sparse-checkout-cone-mode: false
- name: Label PR by filtered changed lines
uses: actions/github-script@v9
with:
script: |
const script = require("./.github/scripts/pr-size-label.js");
await script({ github, context, core });
- name: Disabled
run: echo "pr-size-label is disabled on this fork"
+3
View File
@@ -77,3 +77,6 @@ language-subtag-registry
!/.gemini/config.yaml
.codex
/.gradle-*/
# Managed Java downloaded by the launcher when it is run from the repository root
/java/
+4 -4
View File
@@ -1,6 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Hello Minecraft! Launcher&#10;Copyright (C) &amp;#36;today.year huangyuhui &lt;huanghongxun2008@126.com&gt; and contributors&#10;&#10;This program is free software: you can redistribute it and/or modify&#10;it under the terms of the GNU General Public License as published by&#10;the Free Software Foundation, either version 3 of the License, or&#10;(at your option) any later version.&#10;&#10;This program is distributed in the hope that it will be useful,&#10;but WITHOUT ANY WARRANTY; without even the implied warranty of&#10;MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the&#10;GNU General Public License for more details.&#10;&#10;You should have received a copy of the GNU General Public License&#10;along with this program. If not, see &lt;https://www.gnu.org/licenses/&gt;." />
<option name="myName" value="HMCL" />
</copyright>
<copyright>
<option name="notice" value="SPDX-FileCopyrightText: Copyright (C) &amp;#36;today.year The HMCL Authors&#10;SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL" />
<option name="myName" value="HMCL" />
</copyright>
</component>
+1
View File
@@ -3,6 +3,7 @@
<LanguageOptions name="JAVA">
<option name="fileTypeOverride" value="3" />
<option name="addBlankAfter" value="false" />
<option name="block" value="false" />
</LanguageOptions>
</settings>
</component>
+54 -67
View File
@@ -1,4 +1,5 @@
import org.jackhuang.hmcl.gradle.TerracottaConfigUpgradeTask
import org.jackhuang.hmcl.gradle.addon.FormatJsonFile
import org.jackhuang.hmcl.gradle.addon.UpdateAddonProjectInfos
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
@@ -6,10 +7,10 @@ import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
import org.jackhuang.hmcl.gradle.l10n.SyncTranslations
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
import org.jackhuang.hmcl.gradle.pack.CreateDeb
import org.jackhuang.hmcl.gradle.pack.ReleaseType
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
import org.jackhuang.hmcl.gradle.utils.OfficialBuildValueSource
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
@@ -145,6 +146,7 @@ val addOpens = listOf(
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
"javafx.graphics/com.sun.javafx.tk.quantum",
"javafx.controls/javafx.scene.control",
"javafx.controls/javafx.scene.control.skin",
"jdk.attach/sun.tools.attach",
)
@@ -153,33 +155,28 @@ tasks.compileJava {
options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
}
val hmclProperties = buildList {
add("hmcl.version" to project.version.toString())
add("hmcl.add-opens" to addOpens.joinToString(" "))
System.getenv("GITHUB_SHA")?.let {
add("hmcl.version.hash" to it)
}
add("hmcl.version.type" to versionType)
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
add("hmcl.lwjgl-unsafe-agent.version" to libs.lwjgl.unsafe.agent.get().version!!)
}
val createPropertiesFile = tasks.register<WriteProperties>("createPropertiesFile") {
destinationFile.set(layout.buildDirectory.file("hmcl.properties"))
encoding = "UTF-8"
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
val createPropertiesFile = tasks.register("createPropertiesFile") {
outputs.file(hmclPropertiesFile)
hmclProperties.forEach { (k, v) -> inputs.property(k, v) }
doLast {
val targetFile = hmclPropertiesFile.get().asFile
targetFile.parentFile.mkdir()
targetFile.bufferedWriter().use {
for ((k, v) in hmclProperties) {
it.write("$k=$v\n")
}
}
}
property("hmcl.version", project.version.toString())
property("hmcl.add-opens", addOpens.joinToString(" "))
System.getenv("GITHUB_SHA")?.let { property("hmcl.version.hash", it) }
property("hmcl.version.type", versionType)
property("hmcl.microsoft.auth.id", microsoftAuthId)
property("hmcl.curseforge.apikey", curseForgeApiKey)
property("hmcl.authlib-injector.version", libs.authlib.injector.get().version!!)
property("hmcl.lwjgl-unsafe-agent.version", libs.lwjgl.unsafe.agent.get().version!!)
property("hmcl.official", providers.of(OfficialBuildValueSource::class) {
parameters.publicKeyFile.set(layout.projectDirectory.file("src/main/resources/assets/hmcl_signature_publickey.der"))
parameters.privateKeyLocation.set(providers.environmentVariable("HMCL_SIGNATURE_KEY"))
// Pin the official public key independently of the replaceable resource file.
// Under HMCL’s license agreement, you **MUST** change the software name or the version number in
// a reasonable way to distinguish it from the original version.
// You may modify this hash if and only if you have changed the software name or version number
// in accordance with the license requirements.
parameters.expectedSha256.set("36aadcfde8626b9d4c7a74facb3fdfda47e703c205a673fbe6a56b1d419f8b8e")
})
}
tasks.jar {
@@ -213,7 +210,7 @@ tasks.shadowJar {
}
manifest.attributes(
"Created-By" to "Copyright(c) 2013-2025 huangyuhui.",
"Created-By" to "Copyright(c) 2013-2026 The HMCL Authors",
"Implementation-Version" to project.version.toString(),
"Main-Class" to "org.jackhuang.hmcl.Main",
"Multi-Release" to "true",
@@ -235,13 +232,12 @@ tasks.shadowJar {
}
tasks.processResources {
dependsOn(createPropertiesFile)
dependsOn(upsideDownTranslate)
dependsOn(createLocaleNamesResourceBundle)
dependsOn(createLanguageList)
into("assets/") {
from(hmclPropertiesFile)
from(createPropertiesFile.flatMap { it.destinationFile })
from(embedResources)
}
@@ -251,10 +247,6 @@ tasks.processResources {
from(createLocaleNamesResourceBundle.map { it.outputDirectory })
}
inputs.property("terracotta_version", libs.versions.terracotta)
doLast {
upgradeTerracottaConfig.get().checkValid()
}
}
fun artifactFile(ext: String) = jarPath.resolveSibling(jarPath.nameWithoutExtension + '.' + ext)
@@ -365,6 +357,11 @@ fun parseToolOptions(options: String?): MutableList<String> {
return result
}
// Tests use the same JavaFX reflective access as the packaged application.
tasks.withType<Test> {
jvmArgs(addOpens.map { "--add-opens=$it=ALL-UNNAMED" })
}
// For IntelliJ IDEA
tasks.withType<JavaExec> {
if (name != "run") {
@@ -403,27 +400,6 @@ tasks.register<JavaExec>("run") {
}
}
// terracotta
val upgradeTerracottaConfig = tasks.register<TerracottaConfigUpgradeTask>("upgradeTerracottaConfig") {
val destination = layout.projectDirectory.file("src/main/resources/assets/terracotta.json")
val source = layout.projectDirectory.file("terracotta-template.json");
classifiers.set(
listOf(
"windows-x86_64", "windows-arm64",
"macos-x86_64", "macos-arm64",
"linux-x86_64", "linux-arm64", "linux-loongarch64", "linux-riscv64",
"freebsd-x86_64"
)
)
version.set(libs.versions.terracotta)
downloadURL.set($$"https://github.com/burningtnt/Terracotta/releases/download/v${version}/terracotta-${version}-${classifier}-pkg.tar.gz")
templateFile.set(source)
outputFile.set(destination)
}
// Check Translations
@@ -461,21 +437,32 @@ val createLanguageList = tasks.register<CreateLanguageList>("createLanguageList"
outputFile.set(generatedDir.map { it.file("languages.json") })
}
val createLocaleNamesResourceBundle = tasks.register<CreateLocaleNamesResourceBundle>("createLocaleNamesResourceBundle") {
dependsOn(createLanguageList)
val createLocaleNamesResourceBundle =
tasks.register<CreateLocaleNamesResourceBundle>("createLocaleNamesResourceBundle") {
dependsOn(createLanguageList)
languagesFile.set(createLanguageList.flatMap { it.outputFile })
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
}
languagesFile.set(createLanguageList.flatMap { it.outputFile })
outputDirectory.set(generatedDir.map { it.dir("generated/LocaleNames") })
}
// mcmod data
tasks.register<ParseModDataTask>("parseModData") {
inputFile.set(layout.projectDirectory.file("mod.json"))
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/mod_data.txt"))
tasks.register<FormatJsonFile>("formatModJson") {
file.set(layout.projectDirectory.file("mod.json"))
}
tasks.register<ParseModDataTask>("parseModPackData") {
inputFile.set(layout.projectDirectory.file("modpack.json"))
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/modpack_data.txt"))
tasks.register<FormatJsonFile>("formatModpackJson") {
file.set(layout.projectDirectory.file("modpack.json"))
}
tasks.register<UpdateAddonProjectInfos>("updateModProjectInfos") {
inputFile.set(layout.projectDirectory.file("mod.json"))
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/addon/mod.csv"))
projectType.set(UpdateAddonProjectInfos.AddonProjectType.MOD)
}
tasks.register<UpdateAddonProjectInfos>("updateModpackProjectInfos") {
inputFile.set(layout.projectDirectory.file("modpack.json"))
outputFile.set(layout.projectDirectory.file("src/main/resources/assets/addon/modpack.csv"))
projectType.set(UpdateAddonProjectInfos.AddonProjectType.MODPACK)
}
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package com.jfoenix.controls;
import com.jfoenix.skins.JFXCheckBoxSkin;
@@ -0,0 +1,288 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package com.jfoenix.controls;
import com.jfoenix.skins.JFXSwitchSkin;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.SimpleStyleableDoubleProperty;
import javafx.css.Styleable;
import javafx.css.StyleableDoubleProperty;
import javafx.css.StyleablePropertyFactory;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.List;
/// A Material Design 3 switch for a boolean setting.
///
/// Supports clicking, dragging, and Space-key activation. Labels belong to the surrounding layout;
/// supply an accessible text description. Size and colors are configurable through CSS.
@NotNullByDefault
public class JFXSwitch extends Control {
/// Creates an unselected switch.
public JFXSwitch() {
getStyleClass().setAll("jfx-switch");
setFocusTraversable(true);
setAccessibleRole(AccessibleRole.TOGGLE_BUTTON);
}
/// Selection state.
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected") {
/// Updates visual and accessible selection without firing an action.
@Override
protected void invalidated() {
pseudoClassStateChanged(SELECTED, get());
notifyAccessibleAttributeChanged(AccessibleAttribute.SELECTED);
}
};
/// Changes selection without firing an action event.
public final void setSelected(boolean value) {
selected.set(value);
}
/// Returns whether the switch is selected.
public final boolean isSelected() {
return selected.get();
}
/// Returns the bindable selection state; the default is false.
public final BooleanProperty selectedProperty() {
return selected;
}
/// Read-only activation state.
private final ReadOnlyBooleanWrapper armed = new ReadOnlyBooleanWrapper(this, "armed") {
/// Reflects input feedback in CSS.
@Override
protected void invalidated() {
pseudoClassStateChanged(ARMED, get());
}
};
/// Returns whether the switch is armed.
public final boolean isArmed() {
return armed.get();
}
/// Returns the read-only state indicating a pending activation.
public final ReadOnlyBooleanProperty armedProperty() {
return armed.getReadOnlyProperty();
}
/// Optional action handler.
private @Nullable ObjectProperty<@Nullable EventHandler<ActionEvent>> onAction;
/// Sets or clears the action handler.
public final void setOnAction(@Nullable EventHandler<ActionEvent> handler) {
if (onAction != null || handler != null)
onActionProperty().set(handler);
}
/// Returns the action handler, or null when none is installed.
public final @Nullable EventHandler<ActionEvent> getOnAction() {
return onAction == null ? null : onAction.get();
}
/// Returns the handler property for activation events.
public final ObjectProperty<@Nullable EventHandler<ActionEvent>> onActionProperty() {
if (onAction == null) {
onAction = new SimpleObjectProperty<>(this, "onAction") {
/// Updates the action handler.
@Override
protected void invalidated() {
setEventHandler(ActionEvent.ACTION, get());
}
};
}
return onAction;
}
/// The requested track height in logical pixels.
private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(SIZE, this, "size", 32.0) {
/// Validates the size and requests layout.
@Override
protected void invalidated() {
get();
requestLayout();
}
};
/// Sets the track height; see [#sizeProperty()].
public final void setSize(double value) {
size.set(value);
}
/// Returns the requested track height in logical pixels.
public final double getSize() {
return size.get();
}
/// Track height in logical pixels, styled by `-jfx-size`; the default is 32.
/// Scales the artwork and interaction target proportionally; nonpositive or nonfinite sizes render at the default size.
public final StyleableDoubleProperty sizeProperty() {
return size;
}
/// Optional selected-icon property.
private @Nullable BooleanProperty showSelectedIcon;
/// Sets the value of [#showSelectedIconProperty()].
public final void setShowSelectedIcon(boolean value) {
if (showSelectedIcon != null || value)
showSelectedIconProperty().set(value);
}
/// Returns whether the selected icon is shown.
public final boolean isShowSelectedIcon() {
return showSelectedIcon != null && showSelectedIcon.get();
}
/// Whether to show a check mark when selected; defaults to false.
public final BooleanProperty showSelectedIconProperty() {
if (showSelectedIcon == null)
showSelectedIcon = createOptionProperty("showSelectedIcon", false, false);
return showSelectedIcon;
}
/// Optional unselected-icon property.
private @Nullable BooleanProperty showUnselectedIcon;
/// Sets the value of [#showUnselectedIconProperty()].
public final void setShowUnselectedIcon(boolean value) {
if (showUnselectedIcon != null || value)
showUnselectedIconProperty().set(value);
}
/// Returns whether the unselected icon is shown.
public final boolean isShowUnselectedIcon() {
return showUnselectedIcon != null && showUnselectedIcon.get();
}
/// Whether to show a cross when unselected; defaults to false.
public final BooleanProperty showUnselectedIconProperty() {
if (showUnselectedIcon == null)
showUnselectedIcon = createOptionProperty("showUnselectedIcon", false, false);
return showUnselectedIcon;
}
/// Optional animation policy override.
private @Nullable BooleanProperty disableAnimation;
/// Sets the value of [#disableAnimationProperty()].
public final void setDisableAnimation(boolean value) {
if (disableAnimation != null || value == AnimationUtils.isAnimationEnabled())
disableAnimationProperty().set(value);
}
/// Returns whether animation is disabled.
public final boolean isDisableAnimation() {
return disableAnimation == null ? !AnimationUtils.isAnimationEnabled() : disableAnimation.get();
}
/// Disables animation, defaulting to the inverse of the global animation-enabled setting.
public final BooleanProperty disableAnimationProperty() {
if (disableAnimation == null)
disableAnimation = createOptionProperty("disableAnimation", !AnimationUtils.isAnimationEnabled(), true);
return disableAnimation;
}
/// Arms input feedback unless the switch is disabled or its selection is bound.
public final void arm() {
if (!isDisabled() && !selected.isBound())
armed.set(true);
}
/// Clears input feedback without firing an action.
public final void disarm() {
armed.set(false);
}
/// Toggles selection and fires one action; disabled or bound switches ignore activation.
public void fire() {
if (!isDisabled() && !selected.isBound()) {
setSelected(!isSelected());
fireEvent(new ActionEvent(this, this));
}
}
/// Creates the default switch skin.
@Override
protected Skin<?> createDefaultSkin() {
return new JFXSwitchSkin(this);
}
/// Creates a property that updates the skin, optionally settling active animations.
private BooleanProperty createOptionProperty(String name, boolean initialValue, boolean finishAnimation) {
return new SimpleBooleanProperty(this, name, initialValue) {
/// Validates the value even without a skin, so subsequent changes also reach this callback.
@Override
protected void invalidated() {
get();
if (getSkin() instanceof JFXSwitchSkin skin)
skin.updateOptions(finishAnimation);
}
};
}
/// Indicates that the switch is selected.
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
/// Indicates a pending activation.
private static final PseudoClass ARMED = PseudoClass.getPseudoClass("armed");
/// Factory for switch CSS metadata.
private static final StyleablePropertyFactory<JFXSwitch> STYLEABLES =
new StyleablePropertyFactory<>(Control.getClassCssMetaData());
/// CSS metadata for track height.
private static final CssMetaData<JFXSwitch, Number> SIZE =
STYLEABLES.createSizeCssMetaData("-jfx-size", control -> control.size, 32.0);
/// Switch and inherited CSS metadata.
private static final @Unmodifiable List<CssMetaData<? extends Styleable, ?>> CSS_META_DATA =
List.copyOf(STYLEABLES.getCssMetaData());
/// Returns immutable CSS metadata, including inherited control properties.
public static @Unmodifiable List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return CSS_META_DATA;
}
/// Returns the metadata used to style this switch.
@Override
public @Unmodifiable List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
/// Exposes selection to assistive technology.
@Override
public @Nullable Object queryAccessibleAttribute(AccessibleAttribute attribute, @Nullable Object... parameters) {
return attribute == AccessibleAttribute.SELECTED ? isSelected() : super.queryAccessibleAttribute(attribute, parameters);
}
/// Handles accessible activation.
@Override
public void executeAccessibleAction(AccessibleAction action, @Nullable Object... parameters) {
if (action == AccessibleAction.FIRE)
fire();
else
super.executeAccessibleAction(action, parameters);
}
}
@@ -1,373 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.skins.JFXToggleButtonSkin;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleButton;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* JFXToggleButton is the material design implementation of a toggle button.
* important CSS Selectors:
* <p>
* .jfx-toggle-button{
* -fx-toggle-color: color-value;
* -fx-untoggle-color: color-value;
* -fx-toggle-line-color: color-value;
* -fx-untoggle-line-color: color-value;
* }
* <p>
* To change the rippler color when toggled:
* <p>
* .jfx-toggle-button .jfx-rippler{
* -fx-rippler-fill: color-value;
* }
* <p>
* .jfx-toggle-button:selected .jfx-rippler{
* -fx-rippler-fill: color-value;
* }
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
public class JFXToggleButton extends ToggleButton {
/**
* {@inheritDoc}
*/
public JFXToggleButton() {
initialize();
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new JFXToggleButtonSkin(this);
}
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
// it's up for the user to add this behavior
// toggleColor.addListener((o, oldVal, newVal) -> {
// // update line color in case not set by the user
// if(newVal instanceof Color)
// toggleLineColor.set(((Color)newVal).desaturate().desaturate().brighter());
// });
}
/***************************************************************************
* *
* styleable Properties *
* *
**************************************************************************/
/**
* Initialize the style class to 'jfx-toggle-button'.
* <p>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-toggle-button";
/**
* default color used when the button is toggled
*/
private final StyleableObjectProperty<Paint> toggleColor = new SimpleStyleableObjectProperty<>(StyleableProperties.TOGGLE_COLOR,
JFXToggleButton.this,
"toggleColor",
Color.valueOf(
"#009688"));
public Paint getToggleColor() {
return toggleColor == null ? Color.valueOf("#009688") : toggleColor.get();
}
public StyleableObjectProperty<Paint> toggleColorProperty() {
return this.toggleColor;
}
public void setToggleColor(Paint color) {
this.toggleColor.set(color);
}
/**
* default color used when the button is not toggled
*/
private StyleableObjectProperty<Paint> untoggleColor = new SimpleStyleableObjectProperty<>(StyleableProperties.UNTOGGLE_COLOR,
JFXToggleButton.this,
"unToggleColor",
Color.valueOf(
"#FAFAFA"));
public Paint getUnToggleColor() {
return untoggleColor == null ? Color.valueOf("#FAFAFA") : untoggleColor.get();
}
public StyleableObjectProperty<Paint> unToggleColorProperty() {
return this.untoggleColor;
}
public void setUnToggleColor(Paint color) {
this.untoggleColor.set(color);
}
/**
* default line color used when the button is toggled
*/
private final StyleableObjectProperty<Paint> toggleLineColor = new SimpleStyleableObjectProperty<>(
StyleableProperties.TOGGLE_LINE_COLOR,
JFXToggleButton.this,
"toggleLineColor",
Color.valueOf("#77C2BB"));
public Paint getToggleLineColor() {
return toggleLineColor == null ? Color.valueOf("#77C2BB") : toggleLineColor.get();
}
public StyleableObjectProperty<Paint> toggleLineColorProperty() {
return this.toggleLineColor;
}
public void setToggleLineColor(Paint color) {
this.toggleLineColor.set(color);
}
/**
* default line color used when the button is not toggled
*/
private final StyleableObjectProperty<Paint> untoggleLineColor = new SimpleStyleableObjectProperty<>(
StyleableProperties.UNTOGGLE_LINE_COLOR,
JFXToggleButton.this,
"unToggleLineColor",
Color.valueOf("#999999"));
public Paint getUnToggleLineColor() {
return untoggleLineColor == null ? Color.valueOf("#999999") : untoggleLineColor.get();
}
public StyleableObjectProperty<Paint> unToggleLineColorProperty() {
return this.untoggleLineColor;
}
public void setUnToggleLineColor(Paint color) {
this.untoggleLineColor.set(color);
}
/**
* Default size of the toggle button.
*/
private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
StyleableProperties.SIZE,
JFXToggleButton.this,
"size",
10.0);
public double getSize() {
return size.get();
}
public StyleableDoubleProperty sizeProperty() {
return this.size;
}
public void setSize(double size) {
this.size.set(size);
}
/**
* Disable the visual indicator for focus
*/
private final StyleableBooleanProperty disableVisualFocus = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_VISUAL_FOCUS,
JFXToggleButton.this,
"disableVisualFocus",
false);
public final StyleableBooleanProperty disableVisualFocusProperty() {
return this.disableVisualFocus;
}
public final Boolean isDisableVisualFocus() {
return disableVisualFocus != null && this.disableVisualFocusProperty().get();
}
public final void setDisableVisualFocus(final Boolean disabled) {
this.disableVisualFocusProperty().set(disabled);
}
/**
* disable animation on button action
*/
private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION,
JFXToggleButton.this,
"disableAnimation",
!AnimationUtils.isAnimationEnabled());
public final StyleableBooleanProperty disableAnimationProperty() {
return this.disableAnimation;
}
public final Boolean isDisableAnimation() {
return disableAnimation != null && this.disableAnimationProperty().get();
}
public final void setDisableAnimation(final Boolean disabled) {
this.disableAnimationProperty().set(disabled);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXToggleButton, Paint> TOGGLE_COLOR =
new CssMetaData<>("-jfx-toggle-color",
PaintConverter.getInstance(), Color.valueOf("#009688")) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.toggleColor == null || !control.toggleColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
return control.toggleColorProperty();
}
};
private static final CssMetaData<JFXToggleButton, Paint> UNTOGGLE_COLOR =
new CssMetaData<>("-jfx-untoggle-color",
PaintConverter.getInstance(), Color.valueOf("#FAFAFA")) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.untoggleColor == null || !control.untoggleColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
return control.unToggleColorProperty();
}
};
private static final CssMetaData<JFXToggleButton, Paint> TOGGLE_LINE_COLOR =
new CssMetaData<>("-jfx-toggle-line-color",
PaintConverter.getInstance(), Color.valueOf("#77C2BB")) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.toggleLineColor == null || !control.toggleLineColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
return control.toggleLineColorProperty();
}
};
private static final CssMetaData<JFXToggleButton, Paint> UNTOGGLE_LINE_COLOR =
new CssMetaData<>("-jfx-untoggle-line-color",
PaintConverter.getInstance(), Color.valueOf("#999999")) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.untoggleLineColor == null || !control.untoggleLineColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(JFXToggleButton control) {
return control.unToggleLineColorProperty();
}
};
private static final CssMetaData<JFXToggleButton, Number> SIZE =
new CssMetaData<>("-jfx-size",
StyleConverter.getSizeConverter(), 10.0) {
@Override
public boolean isSettable(JFXToggleButton control) {
return !control.size.isBound();
}
@Override
public StyleableProperty<Number> getStyleableProperty(JFXToggleButton control) {
return control.sizeProperty();
}
};
private static final CssMetaData<JFXToggleButton, Boolean> DISABLE_VISUAL_FOCUS =
new CssMetaData<>("-jfx-disable-visual-focus",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.disableVisualFocus == null || !control.disableVisualFocus.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXToggleButton control) {
return control.disableVisualFocusProperty();
}
};
private static final CssMetaData<JFXToggleButton, Boolean> DISABLE_ANIMATION =
new CssMetaData<>("-jfx-disable-animation",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXToggleButton control) {
return control.disableAnimation == null || !control.disableAnimation.isBound();
}
@Override
public StyleableBooleanProperty getStyleableProperty(JFXToggleButton control) {
return control.disableAnimationProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(ToggleButton.getClassCssMetaData());
Collections.addAll(styleables,
SIZE,
TOGGLE_COLOR,
UNTOGGLE_COLOR,
TOGGLE_LINE_COLOR,
UNTOGGLE_LINE_COLOR,
DISABLE_VISUAL_FOCUS,
DISABLE_ANIMATION
);
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
}
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.ui.animation.AnimationUtils;
public class JFXCheckBoxSkin extends CheckBoxSkin {
private final StackPane box = new StackPane();
private final StackPane mark = new StackPane();
private final SVGPath markShape = new SVGPath();
private final double lineThick = 2.0;
private final double padding = 10.0;
private final JFXRippler rippler;
@@ -37,7 +38,11 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
private final double labelOffset = -8.0;
private final Transition transition;
private boolean invalid = true;
private JFXFillTransition select;
private boolean selected;
private boolean indeterminate;
/// The checkbox fill animation, initialized with the configured checked color.
private JFXFillTransition fillTransition;
public JFXCheckBoxSkin(JFXCheckBox control) {
super(control);
@@ -50,22 +55,17 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
boxContainer.getChildren().add(this.box);
boxContainer.setPadding(new Insets(this.padding));
this.rippler = new JFXRippler(boxContainer, RipplerMask.CIRCLE, RipplerPos.BACK);
this.updateRippleColor();
SVGPath shape = new SVGPath();
shape.setContent("M384 690l452-452 60 60-512 512-238-238 60-60z");
this.mark.setShape(shape);
this.mark.setMaxSize(15.0, 12.0);
this.mark.setStyle("-fx-background-color:-monet-on-primary; -fx-border-color:-monet-on-primary; -fx-border-width:2px;");
this.updateRippleColor(control.isSelected(), control.isIndeterminate());
this.mark.setShape(this.markShape);
this.updateMarkShape(true, false);
this.mark.setVisible(false);
this.mark.setScaleX(0.0);
this.mark.setScaleY(0.0);
boxContainer.getChildren().add(this.mark);
this.container.getChildren().add(this.rippler);
AnchorPane.setRightAnchor(this.rippler, this.labelOffset);
control.selectedProperty().addListener((o, oldVal, newVal) -> {
this.updateRippleColor();
this.playSelectAnimation(newVal);
});
this.registerChangeListener(control.selectedProperty(), ignored -> requestSelectionUpdate());
this.registerChangeListener(control.indeterminateProperty(), ignored -> requestSelectionUpdate());
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(control);
if (focusVisibleProperty == null)
@@ -82,37 +82,63 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
control.pressedProperty().addListener((o, oldVal, newVal) -> this.rippler.hideOverlay());
this.updateChildren();
this.registerChangeListener(control.checkedColorProperty(), ignored -> {
if (select != null) {
select.stop();
if (fillTransition != null) {
fillTransition.stop();
}
this.createFillTransition();
updateColors();
boolean selected = control.isSelected();
boolean indeterminate = control.isIndeterminate();
JFXFillTransition fillTransition = this.createFillTransition();
this.fillTransition = fillTransition;
fillTransition.jumpTo(selected || indeterminate ? fillTransition.getCycleDuration() : Duration.ZERO);
updateColors(selected, indeterminate);
});
this.registerChangeListener(control.unCheckedColorProperty(), ignored -> updateColors());
this.registerChangeListener(control.unCheckedColorProperty(), ignored -> updateColors(control.isSelected(), control.isIndeterminate()));
this.transition = new CheckBoxTransition();
this.createFillTransition();
this.fillTransition = this.createFillTransition();
}
private void updateRippleColor() {
var control = (JFXCheckBox) this.getSkinnable();
this.rippler.setRipplerFill(control.isSelected()
private void requestSelectionUpdate() {
this.invalid = true;
this.getSkinnable().requestLayout();
}
private void updateRippleColor(boolean selected, boolean indeterminate) {
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
this.rippler.setRipplerFill(selected || indeterminate
? control.getCheckedColor()
: control.getUnCheckedColor());
}
private void updateColors() {
private void updateColors(boolean selected, boolean indeterminate) {
var control = (JFXCheckBox) getSkinnable();
boolean isSelected = control.isSelected();
JFXNodeUtils.updateBackground(box.getBackground(), box, isSelected ? control.getCheckedColor() : Color.TRANSPARENT);
rippler.setRipplerFill(isSelected ? control.getCheckedColor() : control.getUnCheckedColor());
boolean marked = selected || indeterminate;
JFXNodeUtils.updateBackground(box.getBackground(), box, marked ? control.getCheckedColor() : Color.TRANSPARENT);
rippler.setRipplerFill(marked ? control.getCheckedColor() : control.getUnCheckedColor());
this.updateBorder(selected, indeterminate);
}
private void updateBorder(boolean selected, boolean indeterminate) {
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
final BorderStroke borderStroke = box.getBorder().getStrokes().get(0);
box.setBorder(new Border(new BorderStroke(
isSelected ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
selected || indeterminate ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
borderStroke.getTopStyle(),
borderStroke.getRadii(),
borderStroke.getWidths())));
}
private void updateMarkShape(boolean selected, boolean indeterminate) {
if (indeterminate) {
this.markShape.setContent("M1 0 H9 A1 1 0 0 1 10 1 A1 1 0 0 1 9 2 H1 A1 1 0 0 1 0 1 A1 1 0 0 1 1 0 Z");
this.mark.setMaxSize(10.0, 2.0);
this.mark.setStyle("-fx-background-color:-monet-on-primary;");
} else if (selected) {
this.markShape.setContent("M384 690l452-452 60 60-512 512-238-238 60-60z");
this.mark.setMaxSize(15.0, 12.0);
this.mark.setStyle("-fx-background-color:-monet-on-primary; -fx-border-color:-monet-on-primary; -fx-border-width:2px;");
}
}
protected void updateChildren() {
super.updateChildren();
if (this.container != null) {
@@ -140,9 +166,10 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
double xOffset = computeXOffset(w, labelWidth + boxWidth, checkBox.getAlignment().getHpos()) + x;
double yOffset = computeYOffset(h, maxHeight, checkBox.getAlignment().getVpos()) + x;
if (this.invalid) {
if (this.getSkinnable().isSelected()) {
this.playSelectAnimation(true);
}
boolean selected = checkBox.isSelected();
boolean indeterminate = checkBox.isIndeterminate();
this.updateRippleColor(selected, indeterminate);
this.playSelectAnimation(selected, indeterminate);
this.invalid = false;
}
@@ -169,47 +196,85 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
};
}
/// Animates a selection change, or applies it immediately when animations are disabled.
private void playSelectAnimation(boolean selection) {
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
private void playSelectAnimation(boolean selected, boolean indeterminate) {
boolean previousSelected = this.selected;
boolean previousIndeterminate = this.indeterminate;
this.selected = selected;
this.indeterminate = indeterminate;
boolean previousMarked = previousSelected || previousIndeterminate;
boolean marked = selected || indeterminate;
boolean sameVisualState = previousIndeterminate == indeterminate
&& (indeterminate || previousSelected == selected);
this.updateBorder(selected, indeterminate);
this.box.setBorder(new Border(new BorderStroke(
selection ? control.getCheckedColor() : Themes.getColorScheme().getOnSurfaceVariant(),
BorderStrokeStyle.SOLID,
new CornerRadii(2.0),
new BorderWidths(this.lineThick))));
if (sameVisualState) {
return;
}
this.invalid = false;
if (!AnimationUtils.isAnimationEnabled()) {
finishSelectionAnimation();
finishSelectionAnimation(selected, indeterminate);
return;
}
this.transition.setRate(selection ? 1.0 : -1.0);
this.select.setRate(selection ? 1.0 : -1.0);
if (previousMarked && marked) {
this.transition.stop();
this.transition.setOnFinished(null);
this.transition.jumpTo(this.transition.getCycleDuration());
this.transition.setRate(-1.0);
this.transition.setOnFinished(event -> {
this.transition.setOnFinished(null);
this.updateMarkShape(selected, indeterminate);
this.transition.jumpTo(Duration.ZERO);
this.transition.setRate(1.0);
this.transition.play();
});
this.transition.play();
return;
}
this.transition.setOnFinished(null);
if (marked) {
this.updateMarkShape(selected, indeterminate);
}
this.transition.setRate(marked ? 1.0 : -1.0);
this.fillTransition.setRate(marked ? 1.0 : -1.0);
this.fillTransition.play();
this.transition.play();
this.select.play();
}
/// Stops selection animations and immediately displays the checkbox's current selected state.
/// Stops selection animations and immediately displays the checkbox's current state.
///
/// The check mark, fill, and border are updated even if no animation is running.
/// This also suppresses the initial selection animation on the first layout.
/// The state indicator, fill, and border are updated even if no animation is running.
/// This also suppresses pending selection updates and the initial selection animation.
/// The skin must not have been disposed.
public void finishSelectionAnimation() {
transition.stop();
select.stop();
invalid = false;
boolean selected = getSkinnable().isSelected();
mark.setVisible(selected);
mark.setScaleX(selected ? 1.0 : 0.0);
mark.setScaleY(selected ? 1.0 : 0.0);
updateColors();
CheckBox checkBox = this.getSkinnable();
this.finishSelectionAnimation(checkBox.isSelected(), checkBox.isIndeterminate());
}
private void createFillTransition() {
this.select = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
private void finishSelectionAnimation(boolean selected, boolean indeterminate) {
this.selected = selected;
this.indeterminate = indeterminate;
boolean marked = selected || indeterminate;
transition.stop();
transition.setOnFinished(null);
transition.jumpTo(marked ? transition.getCycleDuration() : Duration.ZERO);
fillTransition.stop();
fillTransition.jumpTo(marked ? fillTransition.getCycleDuration() : Duration.ZERO);
invalid = false;
updateMarkShape(selected, indeterminate);
mark.setVisible(marked);
mark.setScaleX(marked ? 1.0 : 0.0);
mark.setScaleY(marked ? 1.0 : 0.0);
updateColors(selected, indeterminate);
}
private JFXFillTransition createFillTransition() {
JFXFillTransition fillTransition = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
(Color) ((JFXCheckBox) this.getSkinnable()).getCheckedColor());
this.select.setInterpolator(Interpolator.EASE_OUT);
fillTransition.setInterpolator(Interpolator.EASE_OUT);
return fillTransition;
}
private final class CheckBoxTransition extends CachedTransition {
@@ -21,6 +21,7 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBoxBase;
import javafx.scene.control.ListView;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
@@ -28,6 +29,7 @@ import javafx.scene.paint.Paint;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.List;
@@ -178,6 +180,11 @@ public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
comboBox.hide();
});
Platform.runLater(() -> {
if (listView.lookup(".virtual-flow") instanceof VirtualFlow<?> virtualFlow) {
FXUtils.smoothScrolling(virtualFlow);
}
});
}
}
}
@@ -0,0 +1,731 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDatePicker;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.svg.SVGGlyph;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.*;
import javafx.animation.Animation.Status;
import javafx.beans.InvalidationListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.*;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.SVGContainer;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.Year;
import java.time.YearMonth;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.Chronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DecimalStyle;
import java.time.temporal.ChronoUnit;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.IntStream;
public class JFXDatePickerContent extends VBox {
private static final String SPINNER_LABEL = "spinner-label";
private static final String ROBOTO = "Roboto";
protected JFXDatePicker datePicker;
private JFXButton backMonthButton;
private JFXButton forwardMonthButton;
private Label selectedDateLabel;
private Label selectedYearLabel;
private Label monthYearLabel;
protected GridPane contentGrid;
private final StackPane calendarPlaceHolder = new StackPane();
private final CachedTransition showTransition;
private final CachedTransition hideTransition;
private ParallelTransition tempImageTransition;
private final int daysPerWeek = 7;
private final List<DateCell> weekDaysCells = new ArrayList<>();
private final List<DateCell> weekNumberCells = new ArrayList<>();
protected List<DateCell> dayCells = new ArrayList<>();
private LocalDate[] dayCellDates;
private DateCell currentFocusedDayCell = null;
private final ListView<String> yearsListView = new JFXListView<>() {
{
this.getStyleClass().addAll("date-picker-list-view", "no-padding");
this.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
this.setCellFactory((listView) -> new ListCell<>() {
static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
final Label label = new Label();
final StackPane root;
final RipplerContainer ripplerContainer;
final WeakListenerHolder holder = new WeakListenerHolder();
{
label.getStyleClass().add(SPINNER_LABEL);
label.setMaxWidth(Double.MAX_VALUE);
label.textProperty().bind(itemProperty());
StackPane pane = new StackPane(label);
pane.setPadding(new Insets(7));
ripplerContainer = new RipplerContainer(pane);
root = new StackPane(ripplerContainer);
root.getStyleClass().add("data-picker-list-cell");
this.setGraphic(root);
var selectedListenerWeak = holder.weak((InvalidationListener) observable -> {
String newVal = JFXDatePickerContent.this.selectedYearLabel.getText();
if (newVal != null && getItem() != null && getItem().equals(newVal))
updateSelected(true);
});
JFXDatePickerContent.this.selectedYearLabel.textProperty().addListener(selectedListenerWeak);
itemProperty().addListener(selectedListenerWeak);
FXUtils.onChangeAndOperate(selectedProperty(), selected -> {
if (getListView() != null) {
if (selected) {
getListView().getSelectionModel().select(getIndex());
} else {
getListView().getSelectionModel().clearSelection(getIndex());
}
}
root.pseudoClassStateChanged(SELECTED, selected);
if (selected) {
int offset = Integer.parseInt(getItem()) - Integer.parseInt(JFXDatePickerContent.this.selectedYearLabel.getText());
JFXDatePickerContent.this.forward(offset, ChronoUnit.YEARS, false, false);
}
});
}
public void updateItem(String item, boolean empty) {
String oldItem = getItem();
boolean oldEmpty = isEmpty();
super.updateItem(item, empty);
setText(null);
if (Objects.equals(oldItem, item) && oldEmpty == empty) return;
ripplerContainer.resetFeedback();
if (!empty && item != null) {
setGraphic(root);
} else {
setGraphic(null);
}
}
});
}
};
final DateTimeFormatter fullFormatter = DateTimeFormatter.ofPattern("EEE, MMM yyyy");
final DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("MMMM");
final DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern("y");
final DateTimeFormatter weekNumberFormatter = DateTimeFormatter.ofPattern("w");
final DateTimeFormatter weekDayNameFormatter = DateTimeFormatter.ofPattern("ccc");
final DateTimeFormatter dayCellFormatter = DateTimeFormatter.ofPattern("d");
private final ObjectProperty<YearMonth> selectedYearMonth = new SimpleObjectProperty<>(this, "selectedYearMonth");
final ObjectProperty<LocalDate> valueProperty = new SimpleObjectProperty<>(this, "value");
JFXDatePickerContent(final DatePicker datePicker) {
this.datePicker = (JFXDatePicker) datePicker;
this.getStyleClass().add("date-picker-popup");
LocalDate date = datePicker.getValue();
this.valueProperty.set(date);
this.valueProperty.addListener((o, old, n) -> {
this.displayedYearMonthProperty().set(n != null ? YearMonth.from(n) : YearMonth.now());
this.updateValues();
});
this.selectedYearMonth.set(date != null ? YearMonth.from(date) : YearMonth.now());
this.selectedYearMonth.addListener((observable, oldValue, newValue) -> this.updateValues());
this.getChildren().add(this.createHeaderPane());
this.getChildren().add(new Separator());
this.contentGrid = new GridPane() {
protected double computePrefWidth(double height) {
int nCols = JFXDatePickerContent.this.daysPerWeek + (datePicker.isShowWeekNumbers() ? 1 : 0);
double leftSpace = this.snapSpaceX(this.getInsets().getLeft());
double rightSpace = this.snapSpaceX(this.getInsets().getRight());
double hgaps = this.snapSpaceX(this.getHgap()) * (double) (nCols - 1);
double contentWidth = super.computePrefWidth(height) - leftSpace - rightSpace - hgaps;
return this.snapSizeX(contentWidth / (double) nCols) * (double) nCols + leftSpace + rightSpace + hgaps;
}
protected void layoutChildren() {
if (this.getWidth() > (double) 0.0F && this.getHeight() > (double) 0.0F) {
super.layoutChildren();
}
}
};
this.contentGrid.setFocusTraversable(true);
this.contentGrid.getStyleClass().add("calendar-grid");
this.contentGrid.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
this.contentGrid.setPadding(new Insets(0.0F, 12.0F, 12.0F, 12.0F));
this.contentGrid.setVgap(4);
this.contentGrid.setHgap(4);
this.createWeekDaysCells();
this.createDayCells();
this.yearsListView.getItems().addAll(IntStream.rangeClosed(1900, Year.now().getValue() + 100).mapToObj(Integer::toString).toList());
StackPane main = new StackPane();
this.yearsListView.setVisible(false);
this.yearsListView.setOpacity(0.0F);
this.yearsListView.maxWidthProperty().bind(main.widthProperty());
this.yearsListView.maxHeightProperty().bind(main.heightProperty());
main.getChildren().setAll(this.contentGrid, this.yearsListView);
VBox contentHolder = new VBox();
contentHolder.getChildren().setAll(this.createCalendarMonthLabelPane(), main);
this.calendarPlaceHolder.getStyleClass().add("content");
this.calendarPlaceHolder.getChildren().setAll(contentHolder, this.createCalendarArrowsPane());
FXUtils.setOverflowHidden(this.calendarPlaceHolder);
this.getChildren().add(this.calendarPlaceHolder);
this.getChildren().add(this.createActions());
this.refresh();
this.addEventHandler(KeyEvent.ANY, (event) -> {
Node node = this.getScene().getFocusOwner();
if (node instanceof DateCell) {
this.currentFocusedDayCell = (DateCell) node;
}
switch (event.getCode()) {
case HOME -> {
this.init();
this.goToDate(LocalDate.now(), true);
event.consume();
}
case PAGE_UP -> {
if (!this.backMonthButton.isDisabled()) {
this.forward(-1, ChronoUnit.MONTHS, true, true);
}
event.consume();
}
case PAGE_DOWN -> {
if (!this.forwardMonthButton.isDisabled()) {
this.forward(1, ChronoUnit.MONTHS, true, true);
}
event.consume();
}
case ESCAPE -> {
fireEvent(new DialogCloseEvent());
event.consume();
}
default -> event.consume();
}
});
this.showTransition = new CachedTransition(this.yearsListView, new Timeline(new KeyFrame(Duration.millis(0.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(500.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1000.0F), new KeyValue(this.yearsListView.opacityProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)))) {
{
this.setCycleDuration(Duration.millis(320.0F));
this.setDelay(Duration.seconds(0.0F));
}
protected void starting() {
super.starting();
JFXDatePickerContent.this.yearsListView.setVisible(true);
}
};
this.hideTransition = new CachedTransition(this.yearsListView, new Timeline(new KeyFrame(Duration.millis(0.0F), new KeyValue(this.yearsListView.opacityProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(500.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1000.0F), new KeyValue(this.yearsListView.opacityProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.contentGrid.opacityProperty(), 1, Interpolator.EASE_BOTH)))) {
{
this.setCycleDuration(Duration.millis(320.0F));
this.setDelay(Duration.seconds(0.0F));
}
protected void stopping() {
super.stopping();
JFXDatePickerContent.this.yearsListView.setVisible(false);
}
};
}
ObjectProperty<YearMonth> displayedYearMonthProperty() {
return this.selectedYearMonth;
}
private void createWeekDaysCells() {
for (int i = 0; i < this.daysPerWeek; ++i) {
DateCell cell = new DateCell();
cell.getStyleClass().add("day-name-cell");
cell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
cell.setAlignment(Pos.BASELINE_CENTER);
this.weekDaysCells.add(cell);
}
for (int i = 0; i < 6; ++i) {
DateCell cell = new DateCell();
cell.getStyleClass().add("week-number-cell");
cell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
this.weekNumberCells.add(cell);
}
}
protected VBox createHeaderPane() {
Label title = new Label(I18n.i18n("button.select_date"));
title.getStyleClass().add("title");
title.setFont(Font.font(ROBOTO, FontWeight.NORMAL, 12.0F));
HBox titleContainer = new HBox(title);
titleContainer.setAlignment(Pos.CENTER_LEFT);
titleContainer.setPadding(new Insets(0, 0, 5, 0));
this.selectedYearLabel = new Label();
this.selectedYearLabel.getStyleClass().add(SPINNER_LABEL);
this.selectedYearLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 14.0F));
HBox yearLabelContainer = new HBox();
yearLabelContainer.getStyleClass().add("spinner");
yearLabelContainer.getChildren().addAll(this.selectedYearLabel);
yearLabelContainer.setAlignment(Pos.CENTER_LEFT);
yearLabelContainer.setFillHeight(false);
yearLabelContainer.setOnMouseClicked((click) -> {
if (!this.yearsListView.isVisible()) {
int yearIndex = Integer.parseInt(this.selectedYearLabel.getText()) - 1900 - 2;
this.yearsListView.scrollTo(yearIndex >= 0 ? yearIndex : yearIndex + 2);
this.hideTransition.stop();
this.showTransition.play();
}
});
this.selectedDateLabel = new Label();
this.selectedDateLabel.getStyleClass().add(SPINNER_LABEL);
this.selectedDateLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 32.0F));
HBox selectedDateContainer = new HBox();
selectedDateContainer.getStyleClass().add("spinner");
selectedDateContainer.getChildren().addAll(this.selectedDateLabel);
selectedDateContainer.setAlignment(Pos.CENTER_LEFT);
selectedDateContainer.setOnMouseClicked((click) -> {
if (this.yearsListView.isVisible()) {
this.showTransition.stop();
this.hideTransition.play();
}
});
VBox headerPanel = new VBox();
headerPanel.getStyleClass().add("month-year-pane");
headerPanel.setPadding(new Insets(12.0F, 24.0F, 12.0F, 24.0F));
headerPanel.getChildren().addAll(titleContainer, yearLabelContainer, selectedDateContainer);
return headerPanel;
}
protected BorderPane createCalendarArrowsPane() {
SVGGlyph leftChevron = new SVGGlyph(0, "CHEVRON_LEFT", "M 742,-37 90,614 Q 53,651 53,704.5 53,758 90,795 l 652,651 q 37,37 90.5,37 53.5,0 90.5,-37 l 75,-75 q 37,-37 37,-90.5 0,-53.5 -37,-90.5 L 512,704 998,219 q 37,-38 37,-91 0,-53 -37,-90 L 923,-37 Q 886,-74 832.5,-74 779,-74 742,-37 z", Color.GRAY);
SVGGlyph rightChevron = new SVGGlyph(0, "CHEVRON_RIGHT", "m 1099,704 q 0,-52 -37,-91 L 410,-38 q -37,-37 -90,-37 -53,0 -90,37 l -76,75 q -37,39 -37,91 0,53 37,90 l 486,486 -486,485 q -37,39 -37,91 0,53 37,90 l 76,75 q 36,38 90,38 54,0 90,-38 l 652,-651 q 37,-37 37,-90 z", Color.GRAY);
leftChevron.setSize(6.0F, 11.0F);
rightChevron.setSize(6.0F, 11.0F);
this.backMonthButton = new JFXButton();
this.backMonthButton.setMinSize(40.0F, 40.0F);
this.backMonthButton.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(40.0F), Insets.EMPTY)));
this.backMonthButton.getStyleClass().add("left-button");
this.backMonthButton.setGraphic(leftChevron);
this.backMonthButton.setRipplerFill(this.datePicker.getDefaultColor());
this.backMonthButton.setOnAction((t) -> this.forward(-1, ChronoUnit.MONTHS, false, true));
this.forwardMonthButton = new JFXButton();
this.forwardMonthButton.setMinSize(40.0F, 40.0F);
this.forwardMonthButton.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, new CornerRadii(40.0F), Insets.EMPTY)));
this.forwardMonthButton.getStyleClass().add("right-button");
this.forwardMonthButton.setGraphic(rightChevron);
this.forwardMonthButton.setRipplerFill(this.datePicker.getDefaultColor());
this.forwardMonthButton.setOnAction((t) -> this.forward(1, ChronoUnit.MONTHS, false, true));
BorderPane arrowsContainer = new BorderPane();
arrowsContainer.setLeft(this.backMonthButton);
arrowsContainer.setRight(this.forwardMonthButton);
arrowsContainer.setPadding(new Insets(4.0F, 12.0F, 2.0F, 12.0F));
arrowsContainer.setPickOnBounds(false);
return arrowsContainer;
}
protected BorderPane createCalendarMonthLabelPane() {
this.monthYearLabel = new Label();
this.monthYearLabel.getStyleClass().add(SPINNER_LABEL);
this.monthYearLabel.setFont(Font.font(ROBOTO, FontWeight.BOLD, 13.0F));
SVGContainer svgContainer = new SVGContainer(13);
HBox box = new HBox(monthYearLabel, svgContainer);
box.setSpacing(3);
box.setAlignment(Pos.CENTER);
FXUtils.onChangeAndOperate(this.yearsListView.visibleProperty(), b -> svgContainer.setIcon(b ? SVG.ARROW_DROP_UP : SVG.ARROW_DROP_DOWN));
FXUtils.onClicked(box, () -> {
if (!this.yearsListView.isVisible()) {
int yearIndex = Integer.parseInt(this.selectedYearLabel.getText()) - 1900 - 2;
this.yearsListView.scrollTo(yearIndex >= 0 ? yearIndex : yearIndex + 2);
this.hideTransition.stop();
this.showTransition.play();
} else {
this.showTransition.stop();
this.hideTransition.play();
}
});
BorderPane monthContainer = new BorderPane();
monthContainer.setMinHeight(50.0F);
monthContainer.setCenter(box);
monthContainer.setPadding(new Insets(2.0F, 12.0F, 2.0F, 12.0F));
return monthContainer;
}
protected HBox createActions() {
JFXButton cancelButton = new JFXButton(I18n.i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
FXUtils.onClicked(cancelButton, () -> fireEvent(new DialogCloseEvent()));
JFXButton acceptButton = new JFXButton(I18n.i18n("button.ok"));
acceptButton.getStyleClass().add("dialog-accept");
FXUtils.onClicked(acceptButton, () -> {
datePicker.setValue(valueProperty.get());
fireEvent(new DialogCloseEvent());
});
HBox hBox = new HBox(cancelButton, acceptButton);
hBox.setPadding(new Insets(0, 5, 3, 0));
hBox.setSpacing(5);
hBox.setAlignment(Pos.CENTER_RIGHT);
return hBox;
}
void updateContentGrid() {
this.contentGrid.getColumnConstraints().clear();
this.contentGrid.getChildren().clear();
int colsNumber = this.daysPerWeek + (this.datePicker.isShowWeekNumbers() ? 1 : 0);
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setPercentWidth(100.0F);
for (int i = 0; i < colsNumber; ++i) {
this.contentGrid.getColumnConstraints().add(columnConstraints);
}
for (int i = 0; i < this.daysPerWeek; ++i) {
this.contentGrid.add(this.weekDaysCells.get(i), i + colsNumber - this.daysPerWeek, 1);
}
if (this.datePicker.isShowWeekNumbers()) {
for (int i = 0; i < 6; ++i) {
this.contentGrid.add(this.weekNumberCells.get(i), 0, i + 2);
}
}
for (int row = 0; row < 6; ++row) {
for (int col = 0; col < this.daysPerWeek; ++col) {
this.contentGrid.add(this.dayCells.get(row * this.daysPerWeek + col), col + colsNumber - this.daysPerWeek, row + 2);
}
}
}
private void refresh() {
this.updateDayNameCells();
this.updateValues();
}
private void updateDayNameCells() {
int weekFirstDay = WeekFields.of(this.getLocale()).getFirstDayOfWeek().getValue();
LocalDate date = LocalDate.of(2009, 7, 12 + weekFirstDay);
for (int i = 0; i < this.daysPerWeek; ++i) {
String name = this.weekDayNameFormatter.withLocale(this.getLocale()).format(date.plusDays(i));
if (!LocaleUtils.isChinese(this.weekNumberFormatter.getLocale())) {
name = name.substring(0, 1).toUpperCase(Locale.ROOT);
}
this.weekDaysCells.get(i).setText(name);
}
}
void updateValues() {
this.updateWeekNumberDateCells();
this.updateDayCells();
this.updateMonthYearPane();
}
void updateWeekNumberDateCells() {
if (this.datePicker.isShowWeekNumbers()) {
Locale locale = this.getLocale();
LocalDate firstDayOfMonth = this.selectedYearMonth.get().atDay(1);
for (int i = 0; i < 6; ++i) {
LocalDate date = firstDayOfMonth.plusWeeks(i);
String weekNumber = this.weekNumberFormatter.withLocale(locale).withDecimalStyle(DecimalStyle.of(locale)).format(date);
this.weekNumberCells.get(i).setText(weekNumber);
}
}
}
private void updateDayCells() {
Locale locale = this.getLocale();
Chronology chrono = this.getPrimaryChronology();
int firstDayOfWeek = WeekFields.of(this.getLocale()).getFirstDayOfWeek().getValue();
int firstOfMonthIndex = this.selectedYearMonth.get().atDay(1).getDayOfWeek().getValue() - firstDayOfWeek;
firstOfMonthIndex += firstOfMonthIndex < 0 ? this.daysPerWeek : 0;
YearMonth currentYearMonth = this.selectedYearMonth.get();
int daysInCurMonth = -1;
for (int i = 0; i < 6 * this.daysPerWeek; ++i) {
DateCell dayCell = this.dayCells.get(i);
try {
if (daysInCurMonth == -1) {
daysInCurMonth = currentYearMonth.lengthOfMonth();
}
int dayIndex = i - firstOfMonthIndex + 1;
LocalDate date;
if (dayIndex <= 0) {
YearMonth previous = currentYearMonth.minusMonths(1);
date = previous.atDay(previous.lengthOfMonth() + dayIndex);
} else if (dayIndex > daysInCurMonth) {
YearMonth next = currentYearMonth.plusMonths(1);
date = next.atDay(dayIndex - daysInCurMonth);
} else {
date = currentYearMonth.atDay(dayIndex);
}
this.dayCellDates[i] = date;
if (date.equals(LocalDate.now())) {
dayCell.getStyleClass().add("today");
} else {
dayCell.getStyleClass().removeAll("today");
}
if (date.equals(valueProperty.get())) {
dayCell.getStyleClass().add("selected");
this.currentFocusedDayCell = dayCell;
} else {
dayCell.getStyleClass().removeAll("selected");
}
ChronoLocalDate cDate = chrono.date(date);
String cellText = this.dayCellFormatter.withLocale(locale).withChronology(chrono).withDecimalStyle(DecimalStyle.of(locale)).format(cDate);
dayCell.setText(cellText);
if (i < firstOfMonthIndex) {
dayCell.getStyleClass().add("previous-month");
dayCell.getStyleClass().removeAll("next-month");
dayCell.setDisable(true);
} else if (i >= firstOfMonthIndex + daysInCurMonth) {
dayCell.getStyleClass().add("next-month");
dayCell.getStyleClass().removeAll("previous-month");
dayCell.setDisable(true);
} else {
dayCell.getStyleClass().removeAll("previous-month", "next-month");
dayCell.setDisable(false);
}
dayCell.updateItem(date, false);
} catch (DateTimeException var13) {
dayCell.setText("");
dayCell.setDisable(true);
}
}
}
protected void updateMonthYearPane() {
YearMonth yearMonth = this.selectedYearMonth.get();
this.selectedDateLabel.setText(fullFormatter.withLocale(this.getLocale()).format(Objects.requireNonNullElseGet(valueProperty.get(), LocalDate::now)));
this.selectedYearLabel.setText(this.formatYear(yearMonth));
this.monthYearLabel.setText(this.formatMonth(yearMonth) + " " + this.formatYear(yearMonth));
Chronology chrono = this.datePicker.getChronology();
LocalDate firstDayOfMonth = yearMonth.atDay(1);
this.backMonthButton.setDisable(!this.isValidDate(chrono, firstDayOfMonth, -1, ChronoUnit.DAYS));
this.forwardMonthButton.setDisable(!this.isValidDate(chrono, firstDayOfMonth, 1, ChronoUnit.MONTHS));
}
private String formatMonth(YearMonth yearMonth) {
try {
Chronology chrono = this.getPrimaryChronology();
ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1));
return this.monthFormatter.withLocale(this.getLocale()).withChronology(chrono).format(cDate);
} catch (DateTimeException var4) {
return "";
}
}
private String formatYear(YearMonth yearMonth) {
try {
Chronology chrono = this.getPrimaryChronology();
ChronoLocalDate cDate = chrono.date(yearMonth.atDay(1));
return this.yearFormatter.withLocale(this.getLocale()).withChronology(chrono).withDecimalStyle(DecimalStyle.of(this.getLocale())).format(cDate);
} catch (DateTimeException var4) {
return "";
}
}
protected LocalDate dayCellDate(DateCell dateCell) {
assert this.dayCellDates != null;
return this.dayCellDates[this.dayCells.indexOf(dateCell)];
}
protected void forward(int offset, ChronoUnit unit, boolean focusDayCell, boolean withAnimation) {
if (withAnimation && (this.tempImageTransition == null || this.tempImageTransition.getStatus() == Status.STOPPED)) {
Pane monthContent = (Pane) this.calendarPlaceHolder.getChildren().get(0);
this.getParent().setManaged(false);
SnapshotParameters snapShotparams = new SnapshotParameters();
snapShotparams.setFill(Color.TRANSPARENT);
WritableImage temp = monthContent.snapshot(snapShotparams, new WritableImage((int) monthContent.getWidth(), (int) monthContent.getHeight()));
ImageView tempImage = new ImageView(temp);
this.calendarPlaceHolder.getChildren().add(this.calendarPlaceHolder.getChildren().size() - 2, tempImage);
TranslateTransition imageTransition = new TranslateTransition(Duration.millis(160.0F), tempImage);
imageTransition.setToX((double) (-offset) * this.calendarPlaceHolder.getWidth());
imageTransition.setOnFinished((finish) -> this.calendarPlaceHolder.getChildren().remove(tempImage));
monthContent.setTranslateX((double) offset * this.calendarPlaceHolder.getWidth());
TranslateTransition contentTransition = new TranslateTransition(Duration.millis(160.0F), monthContent);
contentTransition.setToX(0.0F);
this.tempImageTransition = new ParallelTransition(imageTransition, contentTransition);
this.tempImageTransition.setOnFinished((finish) -> {
this.calendarPlaceHolder.getChildren().remove(tempImage);
this.getParent().setManaged(true);
});
this.tempImageTransition.play();
}
YearMonth yearMonth = this.selectedYearMonth.get();
DateCell dateCell = this.currentFocusedDayCell;
if (dateCell == null || this.dayCellDate(dateCell).getMonth() != yearMonth.getMonth()) {
dateCell = this.findDayCellOfDate(yearMonth.atDay(1));
}
this.goToDayCell(dateCell, offset, unit, focusDayCell);
}
private void goToDayCell(DateCell dateCell, int offset, ChronoUnit unit, boolean focusDayCell) {
this.goToDate(this.dayCellDate(dateCell).plus(offset, unit), focusDayCell);
}
private void goToDate(LocalDate date, boolean focusDayCell) {
if (this.isValidDate(this.datePicker.getChronology(), date)) {
this.valueProperty.set(date);
this.selectedYearMonth.set(YearMonth.from(date));
if (focusDayCell) {
this.findDayCellOfDate(date).requestFocus();
}
}
}
private void selectDayCell(DateCell dateCell) {
this.valueProperty.set(this.dayCellDate(dateCell));
}
private DateCell findDayCellOfDate(LocalDate date) {
for (int i = 0; i < this.dayCellDates.length; ++i) {
if (date.equals(this.dayCellDates[i])) {
return this.dayCells.get(i);
}
}
return this.dayCells.get(this.dayCells.size() / 2 + 1);
}
void init() {
this.contentGrid.setOpacity(1.0F);
this.yearsListView.setOpacity(0.0F);
this.yearsListView.setVisible(false);
}
void clearFocus() {
LocalDate focusDate = this.valueProperty.get();
if (focusDate == null) {
focusDate = LocalDate.now();
}
if (YearMonth.from(focusDate).equals(this.selectedYearMonth.get())) {
this.goToDate(focusDate, true);
}
}
protected void createDayCells() {
for (int row = 0; row < 6; ++row) {
for (int col = 0; col < this.daysPerWeek; ++col) {
DateCell dayCell = this.createDayCell();
dayCell.addEventHandler(MouseEvent.MOUSE_CLICKED, (click) -> {
if (click.getButton() == MouseButton.PRIMARY) {
DateCell selectedDayCell = (DateCell) click.getSource();
this.selectDayCell(selectedDayCell);
}
});
dayCell.setOnMouseEntered((event) -> dayCell.getStyleClass().add("hovered"));
dayCell.setOnMouseExited((event) -> dayCell.getStyleClass().removeAll("hovered"));
dayCell.setAlignment(Pos.BASELINE_CENTER);
dayCell.setBorder(new Border(new BorderStroke(Color.TRANSPARENT, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, new BorderWidths(5.0F))));
dayCell.setFont(Font.font(ROBOTO, FontWeight.BOLD, 12.0F));
this.dayCells.add(dayCell);
}
}
this.dayCellDates = new LocalDate[6 * this.daysPerWeek];
this.updateContentGrid();
}
private DateCell createDayCell() {
DateCell dayCell = null;
if (this.datePicker.getDayCellFactory() != null) {
dayCell = this.datePicker.getDayCellFactory().call(this.datePicker);
}
if (dayCell == null) {
dayCell = new DateCell();
}
dayCell.getStyleClass().add("day-cell");
dayCell.setPrefSize(36.0F, 36.0F);
dayCell.setDisable(false);
dayCell.setStyle(null);
dayCell.setGraphic(null);
dayCell.setTooltip(null);
return dayCell;
}
protected Chronology getPrimaryChronology() {
return this.datePicker.getChronology();
}
protected Locale getLocale() {
return SettingsManager.settings().languageProperty().get().getLocale();
}
protected boolean isValidDate(Chronology chrono, LocalDate date, int offset, ChronoUnit unit) {
return date != null && this.isValidDate(chrono, date.plus(offset, unit));
}
protected boolean isValidDate(Chronology chrono, LocalDate date) {
try {
if (date != null) {
chrono.date(date);
}
return true;
} catch (DateTimeException var4) {
return false;
}
}
}
@@ -0,0 +1,181 @@
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.jfoenix.skins;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.adapters.skins.DatePickerSkin;
import com.jfoenix.controls.JFXDatePicker;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXDialog.DialogTransition;
import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.TextField;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.Stack;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class JFXDatePickerSkin extends DatePickerSkin {
public static final String PROPERTY_DATE_PICKER_CONTENTS = JFXDatePicker.class.getName() + ".dialog.contents";
private final JFXDatePicker jfxDatePicker;
private TextField displayNode;
private JFXDatePickerContent content;
private JFXDialog dialog;
public JFXDatePickerSkin(JFXDatePicker datePicker) {
super(datePicker);
this.jfxDatePicker = datePicker;
try {
Object expressionHelper = ReflectionHelper.getFieldContent(datePicker.focusedProperty().getClass().getSuperclass(), datePicker.focusedProperty(), "helper");
ChangeListener<Boolean>[] changeListeners = ReflectionHelper.getFieldContent(expressionHelper, "changeListeners");
int i = changeListeners.length - 1;
if (i >= 0) {
while (i >= 0 && changeListeners[i] == null) {
--i;
}
if (i >= 0) datePicker.focusedProperty().removeListener(changeListeners[i]);
}
} catch (NullPointerException e) {
LOG.warning("Cannot remove focusedProperty listener", e);
}
datePicker.focusedProperty().addListener((obj, oldVal, newVal) -> {
if (this.getEditor() != null && !newVal) {
this.setTextFromTextFieldIntoComboBoxValue2();
}
});
var arrowBox = new HBox(new RipplerContainer(SVG.EDIT_CALENDAR.createIcon(30)));
arrowBox.setAlignment(Pos.CENTER);
this.setArrow(arrowBox);
this.getArrowButton().getChildren().setAll(arrowBox);
this.getArrowButton().setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
this.getArrowButton().setPadding(new Insets(1.0F, 8.0F, 1.0F, 8.0F));
this.registerChangeListener2(datePicker.converterProperty(), "CONVERTER", this::updateDisplayNode2);
this.registerChangeListener2(datePicker.dayCellFactoryProperty(), "DAY_CELL_FACTORY", () -> {
this.updateDisplayNode2();
this.content = null;
this.setPopup2(null);
});
this.registerChangeListener2(datePicker.editorProperty(), "EDITOR", this::getEditableInputNode2);
this.registerChangeListener2(datePicker.showingProperty(), "SHOWING", () -> {
if (this.jfxDatePicker.isShowing()) {
if (this.content != null) {
LocalDate date = this.jfxDatePicker.getValue();
this.content.displayedYearMonthProperty().set(date != null ? YearMonth.from(date) : YearMonth.now());
this.content.updateValues();
}
this.show();
} else {
this.hide();
}
});
this.registerChangeListener2(datePicker.showWeekNumbersProperty(), "SHOW_WEEK_NUMBERS", () -> {
if (this.content != null) {
this.content.updateContentGrid();
this.content.updateWeekNumberDateCells();
}
});
this.registerChangeListener2(datePicker.valueProperty(), "VALUE", () -> {
this.updateDisplayNode2();
this.jfxDatePicker.fireEvent(new ActionEvent());
});
}
@Override
public JFXDatePickerContent getPopupContent() {
if (this.content == null) {
this.content = new JFXDatePickerContent(this.jfxDatePicker);
}
return this.content;
}
@Override
public void show() {
if (!((JFXDatePicker) this.getSkinnable()).isOverLay()) {
super.show();
}
if (this.content != null) {
this.content.init();
this.content.clearFocus();
}
if (((JFXDatePicker) this.getSkinnable()).isOverLay() && this.dialog == null) {
StackPane dialogParent = this.jfxDatePicker.getDialogParent();
if (dialogParent == null) {
dialogParent = (StackPane) this.getSkinnable().getScene().getRoot();
}
this.dialog = new JFXDialog(dialogParent, this.getPopupContent(), DialogTransition.CENTER, true) {
@Override
public void close() {
super.close();
Object o = getDialogContainer().getProperties().get(PROPERTY_DATE_PICKER_CONTENTS);
if (o instanceof Stack<?> stack) {
stack.pop();
}
jfxDatePicker.hide();
}
};
this.getPopupContent().addEventHandler(DialogCloseEvent.CLOSE, e -> this.dialog.close());
getArrowButton().setOnMouseClicked((click) -> {
if (((JFXDatePicker) this.getSkinnable()).isOverLay()) {
StackPane parent = this.jfxDatePicker.getDialogParent();
if (parent == null) {
parent = (StackPane) this.getSkinnable().getScene().getRoot();
}
Stack<JFXDatePickerContent> contentStack = (Stack<JFXDatePickerContent>) parent.getProperties().computeIfAbsent(PROPERTY_DATE_PICKER_CONTENTS, k -> new Stack<>());
contentStack.push(this.getPopupContent());
this.getPopupContent().valueProperty.set(this.getSkinnable().getValue());
this.dialog.show(parent);
}
});
}
}
JFXDialog getDialog() {
return this.dialog;
}
@Override
public Node getDisplayNode() {
if (this.displayNode == null) {
this.displayNode = this.getEditableInputNode2();
this.displayNode.getStyleClass().add("date-picker-display-node");
this.updateDisplayNode2();
}
this.displayNode.setEditable(this.jfxDatePicker.isEditable());
return this.displayNode;
}
public void syncWithAutoUpdate() {
if (!this.getPopup2().isShowing() && this.jfxDatePicker.isShowing()) {
this.jfxDatePicker.hide();
}
}
}
@@ -69,7 +69,7 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
arc = new Arc();
arc.setManaged(false);
arc.setLength(180);
arc.setLength(-180);
arc.getStyleClass().setAll("arc");
arc.setFill(Color.TRANSPARENT);
arc.setStrokeWidth(DEFAULT_STROKE_WIDTH);
@@ -192,24 +192,24 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
private void addKeyFrames(List<KeyFrame> frames, double angle, double duration) {
frames.add(new KeyFrame(Duration.seconds(duration),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), -5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 45 + startingAngle,
-(angle + 45 + startingAngle),
Interpolator.LINEAR)));
frames.add(new KeyFrame(Duration.seconds(duration + 0.4),
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), -250, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 90 + startingAngle,
-(angle + 90 + startingAngle),
Interpolator.LINEAR)));
frames.add(new KeyFrame(Duration.seconds(duration + 0.7),
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), -250, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 135 + startingAngle,
-(angle + 135 + startingAngle),
Interpolator.LINEAR)));
frames.add(new KeyFrame(Duration.seconds(duration + 1.1),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), -5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
angle + 435 + startingAngle,
-(angle + 435 + startingAngle),
Interpolator.LINEAR)));
}
@@ -222,21 +222,21 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
addKeyFrames(keyFrames, 900, 2.8);
addKeyFrames(keyFrames, 1350, 4.2);
keyFrames.add(new KeyFrame(Duration.seconds(5.6),
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), -5, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(),
1845 + startingAngle,
-(1845 + startingAngle),
Interpolator.LINEAR)));
timeline = new Timeline();
timeline.getKeyFrames().setAll(keyFrames);
} else {
final double arcLength = 250;
final double arcLength = -250;
timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(arc.startAngleProperty(), 45 + startingAngle, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(), -(45 + startingAngle), Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE)),
new KeyFrame(Duration.seconds(1.2),
new KeyValue(arc.startAngleProperty(), 45 + 360 + startingAngle, Interpolator.LINEAR),
new KeyValue(arc.startAngleProperty(), -(45 + 360 + startingAngle), Interpolator.LINEAR),
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE))
);
}
@@ -0,0 +1,587 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package com.jfoenix.skins;
import com.jfoenix.controls.JFXSwitch;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.css.StyleOrigin;
import javafx.css.StyleableProperty;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.control.SkinBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.SVGPath;
import javafx.scene.transform.Scale;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/// Renders a Material Design 3 switch and handles input.
/// Geometry uses unscaled MD3 units and is mirrored in RTL layouts.
@NotNullByDefault
public final class JFXSwitchSkin extends SkinBase<JFXSwitch> {
/// Track width in unscaled MD3 artwork coordinates.
private static final double TRACK_WIDTH = 52;
/// Track height corresponding to the control's default `-jfx-size`.
private static final double TRACK_HEIGHT = 32;
/// Unscaled interaction target height.
private static final double TARGET_HEIGHT = 48;
/// Vertical center shared by the track, handle, state layer, and icon.
private static final double CENTER_Y = TARGET_HEIGHT / 2;
/// Handle center at the leading end of the track.
private static final double OFF_CENTER = TRACK_HEIGHT / 2;
/// Handle center at the trailing end of the track.
private static final double ON_CENTER = TRACK_WIDTH - OFF_CENTER;
/// Diameter of the unselected handle without an icon.
private static final double OFF_DIAMETER = 16;
/// Diameter of a selected handle or a handle containing an icon.
private static final double ON_DIAMETER = 24;
/// Handle diameter while pressed or dragged.
private static final double PRESSED_DIAMETER = 28;
/// Radius of the hover, focus, and pressed overlay.
private static final double STATE_LAYER_RADIUS = 20;
/// Minimum pointer movement in control pixels before a press becomes a drag.
private static final double DRAG_THRESHOLD = 3;
/// Indicates visible keyboard focus.
private static final PseudoClass VISUAL_FOCUSED = PseudoClass.getPseudoClass("visual-focused");
/// The rendered switch.
private final JFXSwitch control;
/// Unmanaged switch artwork.
private final Pane artwork = new Pane();
/// Scales artwork from its leading top corner.
private final Scale artworkScale = new Scale();
/// Rounded track with its optional outline.
private final Rectangle track = new Rectangle(0, (TARGET_HEIGHT - TRACK_HEIGHT) / 2, TRACK_WIDTH, TRACK_HEIGHT);
/// Interaction feedback behind the handle.
private final Circle stateLayer = new Circle(OFF_CENTER, CENTER_Y, STATE_LAYER_RADIUS);
/// Switch handle.
private final Circle handle = new Circle(OFF_CENTER, CENTER_Y, OFF_DIAMETER / 2);
/// Optional handle icon.
private @Nullable SVGPath icon;
/// Current logical handle center measured from the track's leading edge.
private final DoubleProperty center = new SimpleDoubleProperty(OFF_CENTER) {
/// Updates handle position.
@Override
protected void invalidated() {
layoutHandle();
}
};
/// Unscaled handle diameter.
private final DoubleProperty diameter = new SimpleDoubleProperty(OFF_DIAMETER) {
/// Updates handle size.
@Override
protected void invalidated() {
layoutHandle();
}
};
/// Optional movement and resize animation.
private @Nullable Timeline geometryTransition;
/// Animated CSS paints.
private final List<CssColor> colors = new ArrayList<>();
/// Optional shared color animation.
private @Nullable Transition colorTransition;
/// Whether a selection color update is pending.
private boolean animateColors;
/// Prevents animation writes from being interpreted as new CSS targets.
private boolean applyingColors;
/// Pointer event handler.
private final EventHandler<MouseEvent> mouseHandler = this::handleMouse;
/// Keyboard event handler.
private final EventHandler<KeyEvent> keyHandler = this::handleKey;
/// Whether a primary press is awaiting click or drag completion.
private boolean mouseDown;
/// Whether the handle is following the pointer instead of an animation.
private boolean dragging;
/// Whether a Space press is awaiting release.
private boolean keyDown;
/// Pointer position in unscaled artwork coordinates at press time.
private double pressX;
/// Handle center at press time, preserving the pointer's grab offset.
private double pressCenter;
/// Creates a skin for the switch.
public JFXSwitchSkin(JFXSwitch control) {
super(control);
this.control = control;
artwork.getStyleClass().setAll("switch-artwork");
track.getStyleClass().setAll("switch-track");
stateLayer.getStyleClass().setAll("switch-state-layer");
handle.getStyleClass().setAll("switch-handle");
track.setArcWidth(TRACK_HEIGHT);
track.setArcHeight(TRACK_HEIGHT);
artwork.setManaged(false);
artwork.setMouseTransparent(true);
artwork.getTransforms().add(artworkScale);
artwork.getChildren().setAll(track, stateLayer, handle);
for (var node : artwork.getChildren())
node.setManaged(false);
getChildren().add(artwork);
registerChangeListener(control.selectedProperty(), value -> updateSelection());
registerChangeListener(control.armedProperty(), value -> update(false));
@Nullable ReadOnlyBooleanProperty focusVisible = FXUtils.focusVisibleProperty(control);
ReadOnlyBooleanProperty focusIndicator = focusVisible != null ? focusVisible : control.focusedProperty();
registerChangeListener(focusIndicator, value ->
control.pseudoClassStateChanged(VISUAL_FOCUSED, focusIndicator.get() && !control.isPressed()));
// Match the previous toggle skin: pointer press and release both dismiss focus feedback.
registerChangeListener(control.pressedProperty(), value ->
control.pseudoClassStateChanged(VISUAL_FOCUSED, false));
Consumer<ObservableValue<?>> cancelGesture = value -> cancelGesture();
registerChangeListener(control.disabledProperty(), cancelGesture);
registerChangeListener(control.sceneProperty(), cancelGesture);
registerChangeListener(control.focusedProperty(), value -> {
if (!control.isFocused())
cancelGesture();
});
control.addEventFilter(MouseEvent.ANY, mouseHandler);
control.addEventFilter(KeyEvent.ANY, keyHandler);
control.pseudoClassStateChanged(VISUAL_FOCUSED, focusIndicator.get() && !control.isPressed());
update(true);
}
/// Updates icon and animation options, optionally settling active animations.
public void updateOptions(boolean finishAnimation) {
if (finishAnimation)
finishColors();
update(finishAnimation);
}
/// Stops animations and removes artwork, event handlers, and listeners.
@Override
public void dispose() {
control.removeEventFilter(MouseEvent.ANY, mouseHandler);
control.removeEventFilter(KeyEvent.ANY, keyHandler);
cancelGesture();
for (CssColor color : colors)
color.dispose();
control.pseudoClassStateChanged(VISUAL_FOCUSED, false);
getChildren().remove(artwork);
super.dispose();
}
/// Centers the scaled artwork within the control and starts pending color transitions.
@Override
protected void layoutChildren(double x, double y, double w, double h) {
double scale = sizeScale();
artworkScale.setX(scale);
artworkScale.setY(scale);
// Keep animation and CSS geometry in MD3 units; scale the entire artwork consistently.
artwork.resizeRelocate(x + (w - TRACK_WIDTH * scale) / 2,
y + (h - TARGET_HEIGHT * scale) / 2, TRACK_WIDTH, TARGET_HEIGHT);
layoutHandle();
// JavaFX applies CSS before layout, so all changed paints now share the same start time.
if (animateColors) {
animateColors = false;
for (CssColor color : colors) {
if (color.target != null) {
colorTransition().playFromStart();
break;
}
}
}
}
/// Reserves the scaled track width and control insets.
@Override
protected double computeMinWidth(double h, double top, double right, double bottom, double left) {
return left + TRACK_WIDTH * sizeScale() + right;
}
/// Reserves the scaled interaction target height and control insets.
@Override
protected double computeMinHeight(double w, double top, double right, double bottom, double left) {
return top + TARGET_HEIGHT * sizeScale() + bottom;
}
/// Uses the same fixed width as the minimum size.
@Override
protected double computePrefWidth(double h, double top, double right, double bottom, double left) {
return computeMinWidth(h, top, right, bottom, left);
}
/// Uses the same fixed height as the minimum size.
@Override
protected double computePrefHeight(double w, double top, double right, double bottom, double left) {
return computeMinHeight(w, top, right, bottom, left);
}
/// Limits width to the preferred size.
@Override
protected double computeMaxWidth(double h, double top, double right, double bottom, double left) {
return computePrefWidth(h, top, right, bottom, left);
}
/// Limits height to the preferred size.
@Override
protected double computeMaxHeight(double w, double top, double right, double bottom, double left) {
return computePrefHeight(w, top, right, bottom, left);
}
/// Handles clicking and dragging.
private void handleMouse(MouseEvent event) {
if (event.getEventType() == MouseEvent.MOUSE_CLICKED && event.getButton() == MouseButton.PRIMARY) {
// Activation happens on release; do not also activate the surrounding settings row.
event.consume();
return;
}
if (event.getEventType() == MouseEvent.MOUSE_PRESSED) {
if (event.getButton() != MouseButton.PRIMARY || keyDown
|| event.isShiftDown() || event.isControlDown() || event.isAltDown() || event.isMetaDown()
|| event.isMiddleButtonDown() || event.isSecondaryButtonDown())
return;
if (control.isDisabled() || control.selectedProperty().isBound())
return;
if (control.isFocusTraversable())
control.requestFocus();
@Nullable Point2D point = artwork.sceneToLocal(event.getSceneX(), event.getSceneY());
if (point != null) {
mouseDown = true;
pressX = point.getX();
pressCenter = center.get();
control.arm();
}
event.consume();
} else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED && mouseDown && event.isPrimaryButtonDown()) {
@Nullable Point2D point = artwork.sceneToLocal(event.getSceneX(), event.getSceneY());
if (point == null)
return;
if (!dragging && Math.abs(point.getX() - pressX) * sizeScale() < DRAG_THRESHOLD)
return;
dragging = true;
if (geometryTransition != null)
geometryTransition.stop();
control.arm();
diameter.set(PRESSED_DIAMETER);
center.set(dragCenter(point.getX()));
event.consume();
} else if (event.getEventType() == MouseEvent.MOUSE_RELEASED && event.getButton() == MouseButton.PRIMARY) {
if (!mouseDown) {
event.consume();
return;
}
@Nullable Point2D point = dragging
? artwork.sceneToLocal(event.getSceneX(), event.getSceneY())
: control.sceneToLocal(event.getSceneX(), event.getSceneY());
if (dragging && point != null)
center.set(dragCenter(point.getX()));
boolean activate = point != null && (dragging
? (center.get() >= (OFF_CENTER + ON_CENTER) / 2) != control.isSelected()
: control.isArmed() && control.contains(point));
mouseDown = false;
dragging = false;
control.disarm();
event.consume();
if (activate)
control.fire();
// Action handlers may replace or remove the skin synchronously.
if (control.getSkin() == this)
update(false);
} else if (mouseDown && !dragging) {
if (event.getEventType() == MouseEvent.MOUSE_ENTERED)
control.arm();
else if (event.getEventType() == MouseEvent.MOUSE_EXITED)
control.disarm();
}
}
/// Clamps pointer displacement to the track while preserving the initial grab offset.
private double dragCenter(double pointerX) {
return Math.max(OFF_CENTER, Math.min(ON_CENTER, pressCenter + pointerX - pressX));
}
/// Handles Space activation and Escape cancellation.
private void handleKey(KeyEvent event) {
if (event.getCode() == KeyCode.ESCAPE && event.getEventType() == KeyEvent.KEY_PRESSED) {
if (mouseDown || keyDown) {
boolean awaitingRelease = keyDown;
cancelGesture();
// Ignore auto-repeat until the canceled Space press is released.
keyDown = awaitingRelease;
event.consume();
}
} else if (event.getCode() == KeyCode.SPACE) {
if (event.getEventType() == KeyEvent.KEY_PRESSED) {
if (event.isShiftDown() || event.isControlDown() || event.isAltDown() || event.isMetaDown())
return;
if (!keyDown) {
if (mouseDown || control.isDisabled() || control.selectedProperty().isBound())
return;
keyDown = true;
control.arm();
}
event.consume();
} else if (event.getEventType() == KeyEvent.KEY_RELEASED && keyDown) {
keyDown = false;
boolean activate = control.isArmed();
control.disarm();
event.consume();
if (activate)
control.fire();
}
}
}
/// Cancels input without changing selection.
private void cancelGesture() {
finishColors();
mouseDown = false;
keyDown = false;
dragging = false;
control.disarm();
update(true);
}
/// Updates geometry and schedules selection colors for the next CSS pass.
private void updateSelection() {
update(false);
if (control.isDisableAnimation() || control.isDisabled() || control.getScene() == null)
return;
if (colors.isEmpty()) {
colors.add(new CssColor(track.fillProperty()));
colors.add(new CssColor(handle.fillProperty()));
colors.add(new CssColor(stateLayer.fillProperty()));
colors.add(new CssColor(track.strokeProperty()));
if (icon != null)
colors.add(new CssColor(icon.fillProperty()));
}
if (colorTransition != null)
colorTransition.stop();
// A reversal before the first frame may reapply the visible paint without emitting a change event.
for (CssColor color : colors)
color.target = null;
animateColors = true;
control.requestLayout();
}
/// Updates handle geometry, with optional animation.
private void update(boolean immediate) {
if (dragging)
return;
double currentCenter = center.get();
double currentDiameter = diameter.get();
if (geometryTransition != null)
geometryTransition.stop();
boolean selected = control.isSelected();
boolean pressed = control.isArmed() && !control.isDisabled();
boolean hasIcon = selected ? control.isShowSelectedIcon() : control.isShowUnselectedIcon();
double targetDiameter = pressed ? PRESSED_DIAMETER : selected || hasIcon ? ON_DIAMETER : OFF_DIAMETER;
double targetCenter = selected ? ON_CENTER : OFF_CENTER;
updateIcon(selected, hasIcon);
if (immediate || control.isDisableAnimation() || control.getScene() == null
|| currentCenter == targetCenter && currentDiameter == targetDiameter) {
center.set(targetCenter);
diameter.set(targetDiameter);
} else {
if (geometryTransition == null)
geometryTransition = new Timeline();
// Preserve the visible geometry when a press or reversal interrupts a transition.
center.set(currentCenter);
diameter.set(currentDiameter);
geometryTransition.getKeyFrames().setAll(
new KeyFrame(Duration.ZERO,
new KeyValue(center, currentCenter),
new KeyValue(diameter, currentDiameter)),
new KeyFrame(pressed ? Motion.SHORT2 : Motion.MEDIUM1,
new KeyValue(diameter, targetDiameter, pressed ? Motion.LINEAR : Motion.STANDARD)),
new KeyFrame(Motion.MEDIUM2,
new KeyValue(center, targetCenter, Motion.EASE_OUT_BACK)));
geometryTransition.play();
}
layoutHandle();
}
/// Updates the handle icon, creating it if needed.
private void updateIcon(boolean selected, boolean visible) {
if (icon == null && visible) {
icon = new SVGPath();
icon.getStyleClass().setAll("switch-icon");
icon.setManaged(false);
// The first CSS fill should appear immediately, without a transition from default black.
icon.setFill(null);
if (!colors.isEmpty())
colors.add(new CssColor(icon.fillProperty()));
artwork.getChildren().add(icon);
}
if (icon != null) {
icon.setVisible(visible);
if (visible)
icon.setContent(selected
? "M2 8l4 4 8-8-1.4-1.4L6 9.2 3.4 6.6Z"
: "M4 2.6l4 4 4-4L13.4 4l-4 4 4 4-1.4 1.4-4-4-4 4L2.6 12l4-4-4-4Z");
}
}
/// Positions and sizes the handle and its overlays.
private void layoutHandle() {
double cx = center.get();
stateLayer.setCenterX(cx);
handle.setCenterX(cx);
handle.setRadius(diameter.get() / 2);
if (icon != null && icon.isVisible())
icon.relocate(cx - icon.getLayoutBounds().getWidth() / 2,
CENTER_Y - icon.getLayoutBounds().getHeight() / 2);
}
/// Returns the size scale, or 1 for an invalid size.
private double sizeScale() {
double size = control.getSize();
return Double.isFinite(size) && size > 0 ? size / TRACK_HEIGHT : 1;
}
/// Returns the shared color animation, creating it if needed.
private Transition colorTransition() {
if (colorTransition == null) {
colorTransition = new Transition() {
{
setCycleDuration(Motion.SHORT4);
setInterpolator(Motion.STANDARD);
}
/// Updates only paints with a pending CSS target.
@Override
protected void interpolate(double fraction) {
for (CssColor color : colors)
color.interpolate(fraction);
}
};
colorTransition.setOnFinished(event -> finishColors());
}
return colorTransition;
}
/// Stops color animation and applies pending targets.
private void finishColors() {
animateColors = false;
if (colorTransition != null)
colorTransition.stop();
for (CssColor color : colors)
color.finish();
}
/// Animates a solid paint while preserving CSS precedence.
private final class CssColor implements ChangeListener<@Nullable Paint> {
/// The animated paint property.
private final ObjectProperty<@Nullable Paint> property;
/// Most recent solid CSS target; null means no transition is pending.
private @Nullable Color target;
/// CSS origin of the target paint.
private @Nullable StyleOrigin origin;
/// Starting color.
private Color from = Color.TRANSPARENT;
/// Observes a styleable paint property.
private CssColor(ObjectProperty<@Nullable Paint> property) {
this.property = property;
property.addListener(this);
}
/// Animates selection colors and applies other paint changes immediately.
@Override
public void changed(ObservableValue<? extends @Nullable Paint> observable,
@Nullable Paint previous, @Nullable Paint next) {
if (applyingColors)
return;
// CSS may reapply the same target while the displayed value is still interpolating.
if (target != null && target.equals(next) && previous != null) {
apply(previous);
return;
}
target = null;
if (!animateColors || !(previous instanceof Color start) || !(next instanceof Color end) || start.equals(end))
return;
from = start;
target = end;
origin = ((StyleableProperty<?>) property).getStyleOrigin();
interpolate(0);
}
/// Applies an animation frame.
private void interpolate(double fraction) {
if (target != null)
apply(fraction == 1 ? target : from.interpolate(target, fraction));
}
/// Writes a CSS value while suppressing this transition's own listener.
@SuppressWarnings("unchecked")
private void apply(Paint value) {
applyingColors = true;
try {
((StyleableProperty<Paint>) property).applyStyle(origin, value);
} finally {
applyingColors = false;
}
}
/// Applies and clears the pending target.
private void finish() {
if (target != null) {
apply(target);
target = null;
}
}
/// Removes the paint listener.
private void dispose() {
property.removeListener(this);
}
}
}
@@ -1,186 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.skins;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.controls.JFXRippler.RipplerMask;
import com.jfoenix.controls.JFXRippler.RipplerPos;
import com.jfoenix.controls.JFXToggleButton;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.JFXAnimationTimer;
import com.jfoenix.transitions.JFXKeyFrame;
import com.jfoenix.transitions.JFXKeyValue;
import javafx.animation.Interpolator;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.control.skin.ToggleButtonSkin;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.StrokeLineCap;
import javafx.util.Duration;
/**
* <h1>Material Design ToggleButton Skin</h1>
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
public class JFXToggleButtonSkin extends ToggleButtonSkin {
private Runnable releaseManualRippler = null;
private JFXAnimationTimer timer;
private final Circle circle;
private final Line line;
public JFXToggleButtonSkin(JFXToggleButton toggleButton) {
super(toggleButton);
double circleRadius = toggleButton.getSize();
line = new Line();
line.setStroke(getSkinnable().isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
line.setStartX(0);
line.setStartY(0);
line.setEndX(circleRadius * 2 + 2);
line.setEndY(0);
line.setStrokeWidth(circleRadius * 1.5);
line.setStrokeLineCap(StrokeLineCap.ROUND);
line.setSmooth(true);
circle = new Circle();
circle.setFill(getSkinnable().isSelected() ? toggleButton.getToggleColor() : toggleButton.getUnToggleColor());
circle.setCenterX(-circleRadius);
circle.setCenterY(0);
circle.setRadius(circleRadius);
circle.setSmooth(true);
JFXDepthManager.setDepth(circle, 1);
StackPane circlePane = new StackPane();
circlePane.getChildren().add(circle);
circlePane.setPadding(new Insets(circleRadius * 1.5));
JFXRippler rippler = new JFXRippler(circlePane, RipplerMask.CIRCLE, RipplerPos.BACK);
rippler.setRipplerFill(getSkinnable().isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
rippler.setTranslateX(computeTranslation(circleRadius, line));
final StackPane main = new StackPane();
main.getChildren().setAll(line, rippler);
main.setCursor(Cursor.HAND);
// show focus traversal effect
getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
releaseManualRippler = rippler.createManualRipple();
} else if (releaseManualRippler != null) {
releaseManualRippler.run();
}
});
toggleButton.focusedProperty().addListener((o, oldVal, newVal) -> {
if (!toggleButton.isDisableVisualFocus()) {
if (newVal) {
if (!getSkinnable().isPressed()) {
rippler.setOverlayVisible(true);
}
} else {
rippler.setOverlayVisible(false);
}
}
});
toggleButton.pressedProperty().addListener(observable -> rippler.setOverlayVisible(false));
// add change listener to selected property
getSkinnable().selectedProperty().addListener(observable -> {
rippler.setRipplerFill(toggleButton.isSelected() ? toggleButton.getToggleLineColor() : toggleButton.getUnToggleLineColor());
if (!toggleButton.isDisableAnimation()) {
timer.reverseAndContinue();
} else {
rippler.setTranslateX(computeTranslation(circleRadius, line));
}
});
getSkinnable().setGraphic(main);
timer = new JFXAnimationTimer(
new JFXKeyFrame(Duration.millis(100),
JFXKeyValue.builder()
.setTarget(rippler.translateXProperty())
.setEndValueSupplier(() -> computeTranslation(circleRadius, line))
.setInterpolator(Interpolator.EASE_BOTH)
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
.build(),
JFXKeyValue.builder()
.setTarget(line.strokeProperty())
.setEndValueSupplier(() -> getSkinnable().isSelected() ?
((JFXToggleButton) getSkinnable()).getToggleLineColor()
: ((JFXToggleButton) getSkinnable()).getUnToggleLineColor())
.setInterpolator(Interpolator.EASE_BOTH)
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
.build(),
JFXKeyValue.builder()
.setTarget(circle.fillProperty())
.setEndValueSupplier(() -> getSkinnable().isSelected() ?
((JFXToggleButton) getSkinnable()).getToggleColor()
: ((JFXToggleButton) getSkinnable()).getUnToggleColor())
.setInterpolator(Interpolator.EASE_BOTH)
.setAnimateCondition(() -> !((JFXToggleButton) getSkinnable()).isDisableAnimation())
.build()
)
);
timer.setCacheNodes(circle, line);
registerChangeListener(toggleButton.toggleColorProperty(), observableValue -> {
if (getSkinnable().isSelected()) {
circle.setFill(((JFXToggleButton) getSkinnable()).getToggleColor());
}
});
registerChangeListener(toggleButton.unToggleColorProperty(), observableValue -> {
if (!getSkinnable().isSelected()) {
circle.setFill(((JFXToggleButton) getSkinnable()).getUnToggleColor());
}
});
registerChangeListener(toggleButton.toggleLineColorProperty(), observableValue -> {
if (getSkinnable().isSelected()) {
line.setStroke(((JFXToggleButton) getSkinnable()).getToggleLineColor());
}
});
registerChangeListener(toggleButton.unToggleColorProperty(), observableValue -> {
if (!getSkinnable().isSelected()) {
line.setStroke(((JFXToggleButton) getSkinnable()).getUnToggleLineColor());
}
});
}
private double computeTranslation(double circleRadius, Line line) {
return (getSkinnable().isSelected() ? 1 : -1) * ((line.getLayoutBounds().getWidth() / 2) - circleRadius + 2);
}
@Override
public void dispose() {
super.dispose();
timer.dispose();
timer = null;
}
}
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.FileSaver;
@@ -23,6 +8,7 @@ import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.io.TLSSecurity;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.JOptionPane;
@@ -42,6 +28,7 @@ public final class EntryPoint {
private EntryPoint() {
}
/// Initializes TLS protection before downloading dependencies and starting the launcher.
public static void main(String[] args) {
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
@@ -49,6 +36,7 @@ public final class EntryPoint {
createHMCLDirectories();
LOG.start(Metadata.HMCL_LOCAL_HOME.resolve("logs"));
TLSSecurity.install();
checkWine();
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl;
import com.sun.jna.Pointer;
@@ -38,20 +23,18 @@ import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.macos.LibSystem;
import org.jackhuang.hmcl.util.platform.windows.Gdi32;
import org.jackhuang.hmcl.util.platform.windows.Shell32;
import org.jackhuang.hmcl.util.platform.windows.User32;
import org.jackhuang.hmcl.util.platform.windows.WinReg;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
@@ -142,12 +125,7 @@ public final class Launcher extends Application {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
Themes.applyNativeDarkMode(primaryStage);
UpdateChecker.init();
WindowsNativeUtils.installWindowsAppUserModelRelaunchProperties(primaryStage);
primaryStage.show();
});
} catch (Throwable e) {
@@ -284,11 +262,6 @@ public final class Launcher extends Application {
}
public static void main(String[] args) {
if (UpdateHandler.processArguments(args)) {
LOG.shutdown();
return;
}
Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER);
AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false));
@@ -301,6 +274,10 @@ public final class Launcher extends Application {
LOG.info("Processor Identifier: " + System.getenv("PROCESSOR_IDENTIFIER"));
}
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH.getDisplayName());
try {
LOG.info("Process PID: " + ProcessHandle.current().pid());
} catch (UnsupportedOperationException ignored) {
}
LOG.info("Native Encoding: " + OperatingSystem.NATIVE_CHARSET);
LOG.info("JNU Encoding: " + System.getProperty("sun.jnu.encoding"));
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
@@ -332,6 +309,9 @@ public final class Launcher extends Application {
LOG.info("XDG Current Desktop: " + System.getenv("XDG_CURRENT_DESKTOP"));
}
@Nullable Boolean isRoot = isRootUser();
LOG.info("Administrator User: " + (isRoot == null ? "Unknown" : isRoot.toString()));
LOG.info("Zlib Compatible: " + ZlibUtils.IS_ZLIB_COMPATIBLE);
Lang.thread(SystemInfo::initialize, "Detection System Information", true);
@@ -388,6 +368,9 @@ public final class Launcher extends Application {
return;
if (!NativeUtils.USE_JNA || Shell32.INSTANCE == null)
return;
@Nullable Path thisJar = JarUtils.thisJarPath();
if (thisJar == null || !Files.isRegularFile(thisJar) || !"exe".equalsIgnoreCase(FileUtils.getExtension(thisJar)))
return;
try {
int hr = Shell32.INSTANCE.SetCurrentProcessExplicitAppUserModelID(new WString(Metadata.WINDOWS_APP_USER_MODEL_ID));
@@ -594,5 +577,68 @@ public final class Launcher extends Application {
EntryPoint.exit(1);
}
private static @Nullable Boolean isRootUser() {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS -> {
WinReg reg = WinReg.INSTANCE;
if (reg == null)
return null;
try {
return reg.exists(WinReg.HKEY.HKEY_USERS, "S-1-5-19");
} catch (Exception e) {
LOG.warning("Failed to check Windows administrator privileges", e);
return null;
}
}
case LINUX -> {
Path selfStatus = Paths.get("/proc/self/status");
if (Files.exists(selfStatus)) {
try (var reader = Files.newBufferedReader(selfStatus)) {
@Nullable String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("Uid:")) {
String[] parts = line.split("\t");
if (parts.length >= 3) {
String uid = parts[2].trim();
return "0".equals(uid);
}
}
}
} catch (Exception e) {
LOG.warning("Failed to read " + selfStatus, e);
}
}
}
case MACOS -> {
if (NativeUtils.USE_JNA) {
LibSystem libSystem = LibSystem.INSTANCE;
if (libSystem != null) {
try {
return libSystem.geteuid() == 0;
} catch (Throwable e) {
LOG.warning("Failed to check macOS root privileges", e);
return null;
}
}
}
}
}
if (OperatingSystem.CURRENT_OS.isUnixLike()) {
@Nullable Path id = SystemUtils.which("id");
if (id != null) {
try {
return "0".equals(SystemUtils.run(id.toString(), "-u").trim());
} catch (Exception e) {
LOG.warning("Failed to run id -u", e);
return null;
}
}
}
return null;
}
public static final CrashReporter CRASH_REPORTER = new CrashReporter(true);
}
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl;
import org.jackhuang.hmcl.util.StringUtils;
@@ -42,22 +27,16 @@ public final class Metadata {
public static final String WINDOWS_APP_USER_MODEL_ID = "org.jackhuang.hmcl";
public static final String TITLE = NAME + " " + VERSION;
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
/// Whether the build metadata identifies this launcher as an official build.
public static final boolean OFFICIAL_BUILD = Boolean.parseBoolean(JarUtils.getAttribute("hmcl.official", "false"));
public static final int MINIMUM_REQUIRED_JAVA_VERSION = 17;
public static final int MINIMUM_SUPPORTED_JAVA_VERSION = 17;
public static final int RECOMMENDED_JAVA_VERSION = 21;
public static final String PUBLISH_URL = "https://hmcl.huangyuhui.net";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
public static final int RECOMMENDED_JAVA_VERSION = 25;
public static final String DOCS_URL = "https://docs.hmcl.net";
public static final String CONTACT_URL = DOCS_URL + "/help.html";
public static final String CHANGELOG_URL = DOCS_URL + "/changelog/";
public static final String EULA_URL = DOCS_URL + "/eula/hmcl.html";
public static final String GROUPS_URL = "https://www.bilibili.com/opus/905435541874409529";
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
@@ -110,7 +89,7 @@ public final class Metadata {
public static @Nullable String getSuggestedJavaDownloadLink() {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.LOONGARCH64_OW)
return "https://www.loongnix.cn/zh/api/java/downloads-jdk21/index.html";
return "https://www.loongnix.cn/zh/api/java/downloads-jdk25/index.html";
else {
EnumSet<Architecture> supportedArchitectures;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
@@ -162,4 +141,18 @@ public final class Metadata {
return null;
}
/// Server address used when the installed modpack manifest does not provide one.
///
/// Can be set at build or run time with the `hmcl.server.address` system property; empty by default.
public static String getDefaultServerAddress() {
return System.getProperty("hmcl.server.address", "").trim();
}
/// The server name shown in the title bar.
///
/// Can be set with the `hmcl.server.name` system property; empty by default.
public static String getServerName() {
return System.getProperty("hmcl.server.name", "").trim();
}
}
@@ -0,0 +1,473 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.addon;
import org.jackhuang.hmcl.addon.mod.ModProjectInfo;
import org.jackhuang.hmcl.modpack.ModpackProjectInfo;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.SoftReference;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
@NotNullByDefault
public abstract class AddonProjectInfoRegistry<I extends AddonProjectInfo> {
public static final AddonProjectInfoRegistry<ModProjectInfo> MOD = new AddonProjectInfoRegistry<>(
"/assets/addon/mod.csv",
32768
) {
@Override
protected int getFieldsCount() {
return MOD_IDS_INDEX + 1;
}
@Override
protected ModProjectInfo parseAddonProjectInfo(String[] fields) throws Exception {
return new ModProjectInfo(
Integer.parseInt(fields[MCMOD_ID_INDEX]),
fields[CURSEFORGE_ID_INDEX],
fields[MODRINTH_ID_INDEX],
fields[MAIN_NAME_INDEX],
fields[SUB_NAME_INDEX],
fields[ABBR_NAME_INDEX],
fields[MOD_IDS_INDEX]
);
}
};
/// Provides metadata for modpacks.
public static final AddonProjectInfoRegistry<ModpackProjectInfo> MODPACK = new AddonProjectInfoRegistry<>(
"/assets/addon/modpack.csv", 2048
) {
@Override
protected int getFieldsCount() {
return ABBR_NAME_INDEX + 1;
}
@Override
protected ModpackProjectInfo parseAddonProjectInfo(String[] fields) {
return new ModpackProjectInfo(Integer.parseInt(fields[MCMOD_ID_INDEX]),
fields[CURSEFORGE_ID_INDEX], fields[MODRINTH_ID_INDEX],
fields[MAIN_NAME_INDEX], fields[SUB_NAME_INDEX], fields[ABBR_NAME_INDEX]);
}
};
/// Returns the registry for the addon type, or `null` for null or unsupported types.
public static @Nullable AddonProjectInfoRegistry<?> getByAddonType(@Nullable RemoteAddon.Type type) {
if (type == null) return null;
return switch (type) {
case MOD -> MOD;
case MODPACK -> MODPACK;
default -> null;
};
}
protected static final int MCMOD_ID_INDEX = 0;
protected static final int CURSEFORGE_ID_INDEX = 1;
protected static final int MODRINTH_ID_INDEX = 2;
protected static final int MAIN_NAME_INDEX = 3;
protected static final int SUB_NAME_INDEX = 4;
protected static final int ABBR_NAME_INDEX = 5;
protected static final int MOD_IDS_INDEX = 6;
private final ReentrantLock lock = new ReentrantLock();
private final String assetsPath;
private final int initialCapacity;
private volatile @Nullable SoftReference<@Unmodifiable List<I>> infosRef;
/// Softly caches projects indexed by mod ID.
private volatile @Nullable SoftReference<@Unmodifiable Map<String, I>> modIdMapRef;
/// Softly caches projects indexed by cleaned secondary name.
private volatile @Nullable SoftReference<@Unmodifiable Map<String, I>> subnameMapRef;
/// Softly caches projects indexed by CurseForge identifier.
private volatile @Nullable SoftReference<@Unmodifiable Map<String, I>> curseForgeMapRef;
/// Softly caches searchable names in resource and field order.
private volatile @Nullable SoftReference<@Unmodifiable List<Pair<String, I>>> keywordsRef;
/// Creates a registry reading records from the given classpath resource.
AddonProjectInfoRegistry(String assetsPath, int initialCapacity) {
this.assetsPath = assetsPath;
this.initialCapacity = initialCapacity;
}
protected abstract int getFieldsCount();
protected abstract I parseAddonProjectInfo(String[] fields) throws Exception;
private @Unmodifiable List<I> loadInfos() throws IOException {
@Nullable InputStream inputStream = AddonProjectInfoRegistry.class.getResourceAsStream(assetsPath);
if (inputStream == null) {
return List.of();
}
try (var reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
ArrayList<I> infos = new ArrayList<>(initialCapacity);
@Unmodifiable List<I> result = Collections.unmodifiableList(infos);
int fieldsCount = getFieldsCount();
String[] fields = new String[fieldsCount];
@Nullable StringBuilder fieldBuilder = null;
@Nullable String line;
loop:
while ((line = reader.readLine()) != null) {
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
int offset = 0;
for (int fieldIndex = 0; fieldIndex < fieldsCount; fieldIndex++) {
if (offset > line.length()) {
LOG.warning("Invalid line in " + assetsPath + ": " + line);
continue loop;
}
int end = line.indexOf(',', offset);
if (end < 0)
end = line.length();
String field;
if (offset == end) {
field = "";
} else {
int cursor = offset;
while (cursor < end) {
if (line.charAt(cursor) == '\\') {
break;
} else {
cursor++;
}
}
if (cursor == end) {
field = line.substring(offset, end);
} else {
if (fieldBuilder == null) {
fieldBuilder = new StringBuilder(32);
} else {
fieldBuilder.setLength(0);
}
fieldBuilder.append(line, offset, cursor);
while (cursor < end) {
char c = line.charAt(cursor);
if (c == '\\') {
if (cursor < end - 1) {
char next = line.charAt(cursor + 1);
switch (next) {
case '0' -> {
fieldBuilder.append('\0');
cursor += 2;
}
case 'n' -> {
fieldBuilder.append('\n');
cursor += 2;
}
case 'r' -> {
fieldBuilder.append('\r');
cursor += 2;
}
case 't' -> {
fieldBuilder.append('\t');
cursor += 2;
}
case '\\' -> {
fieldBuilder.append('\\');
cursor += 2;
}
case 'c' -> {
fieldBuilder.append(',');
cursor += 2;
}
case 'u' -> {
if (cursor < end - 5) {
try {
int codePoint = Integer.parseInt(line, cursor + 2, cursor + 6, 16);
fieldBuilder.append((char) codePoint);
cursor += 6;
} catch (NumberFormatException e) {
LOG.warning("Invalid line in " + assetsPath + ": " + line);
continue loop;
}
} else {
fieldBuilder.append(c);
cursor++;
}
}
default -> {
LOG.warning("Invalid line in " + assetsPath + ": " + line);
continue loop;
}
}
} else {
fieldBuilder.append(c);
cursor++;
}
} else {
fieldBuilder.append(c);
cursor++;
}
}
field = fieldBuilder.toString();
}
}
fields[fieldIndex] = field;
offset = end + 1;
}
try {
I info = parseAddonProjectInfo(fields);
info.setKeepAlive(result);
infos.add(info);
} catch (Exception e) {
LOG.warning("Failed to parse line in " + assetsPath + ": " + line, e);
}
}
infos.trimToSize();
return result;
}
}
/// Returns an unmodifiable index, rebuilding it after its soft reference is cleared.
/// Duplicate keys retain the first project in resource order.
private @Unmodifiable Map<String, I> getModIdMap() {
@Nullable SoftReference<@Unmodifiable Map<String, I>> reference = this.modIdMapRef;
@Unmodifiable Map<String, I> cached = reference != null ? reference.get() : null;
if (cached != null)
return cached;
lock.lock();
try {
reference = this.modIdMapRef;
cached = reference != null ? reference.get() : null;
if (cached != null)
return cached;
Map<String, I> map = new HashMap<>();
for (I info : getInfos()) {
if (info instanceof ModProjectInfo mod) {
for (String id : mod.getModIds()) {
if (!id.isEmpty())
map.putIfAbsent(id, info);
}
}
}
cached = Collections.unmodifiableMap(map);
this.modIdMapRef = new SoftReference<>(cached);
return cached;
} finally {
lock.unlock();
}
}
/// Returns an unmodifiable index, rebuilding it after its soft reference is cleared.
/// Duplicate keys retain the first project in resource order.
private @Unmodifiable Map<String, I> getSubnameMap() {
@Nullable SoftReference<@Unmodifiable Map<String, I>> reference = this.subnameMapRef;
@Unmodifiable Map<String, I> cached = reference != null ? reference.get() : null;
if (cached != null)
return cached;
lock.lock();
try {
reference = this.subnameMapRef;
cached = reference != null ? reference.get() : null;
if (cached != null)
return cached;
Map<String, I> map = new HashMap<>();
for (I info : getInfos()) {
String name = cleanSubname(info.subName());
if (!name.isEmpty())
map.putIfAbsent(name, info);
}
cached = Collections.unmodifiableMap(map);
this.subnameMapRef = new SoftReference<>(cached);
return cached;
} finally {
lock.unlock();
}
}
/// Returns an unmodifiable index, rebuilding it after its soft reference is cleared.
/// Duplicate keys retain the first project in resource order.
private @Unmodifiable Map<String, I> getCurseForgeMap() {
@Nullable SoftReference<@Unmodifiable Map<String, I>> reference = this.curseForgeMapRef;
@Unmodifiable Map<String, I> cached = reference != null ? reference.get() : null;
if (cached != null)
return cached;
lock.lock();
try {
reference = this.curseForgeMapRef;
cached = reference != null ? reference.get() : null;
if (cached != null)
return cached;
Map<String, I> map = new HashMap<>();
for (I info : getInfos()) {
String id = info.curseforgeId();
if (StringUtils.isNotBlank(id))
map.putIfAbsent(id, info);
}
cached = Collections.unmodifiableMap(map);
this.curseForgeMapRef = new SoftReference<>(cached);
return cached;
} finally {
lock.unlock();
}
}
/// Returns the first project with the exact CurseForge identifier, or `null` if the
/// identifier is null, blank or unknown.
public @Nullable I getInfoByCurseForgeId(@Nullable String id) {
return StringUtils.isBlank(id) ? null : getCurseForgeMap().get(id);
}
/// Finds a project by its cleaned secondary name, falling back to its exact mod ID.
/// A name match is accepted only when the ID is blank or belongs to that mod project.
/// Null and blank inputs are absent; returns `null` if neither lookup matches.
/// Duplicate keys resolve to the first project in resource order.
public @Nullable I getInfo(@Nullable String id, @Nullable String subname) {
String name = cleanSubname(subname);
if (StringUtils.isNotBlank(name)) {
@Nullable I info = getSubnameMap().get(name);
if (info != null && (StringUtils.isBlank(id)
|| info instanceof ModProjectInfo mod && mod.getModIds().contains(id)))
return info;
}
return StringUtils.isBlank(id) ? null : getModIdMap().get(id);
}
private static boolean isSupportedSubnameChar(int ch) {
return (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')
|| ".+\\".indexOf(ch) >= 0;
}
/// Removes supported separators from a secondary name.
/// Returns an empty string for null, blank or unsupported names; matching is case-sensitive.
private static String cleanSubname(@Nullable String subname) {
if (StringUtils.isBlank(subname))
return "";
int offset = 0;
while (offset < subname.length()) {
int ch = subname.charAt(offset);
if (!isSupportedSubnameChar(ch))
break;
offset++;
}
if (offset == subname.length())
return subname;
StringBuilder builder = new StringBuilder(subname.length());
builder.append(subname, 0, offset);
while (offset < subname.length()) {
int ch = subname.codePointAt(offset);
if (isSupportedSubnameChar(ch)) {
builder.appendCodePoint(ch);
} else if (!(Character.isWhitespace(ch)
|| "':_-/&()[]{}|,!?~•".indexOf(ch) >= 0)) {
return "";
}
offset += Character.charCount(ch);
}
return builder.toString();
}
/// Returns searchable names, rebuilding the index after its soft reference is cleared.
private @Unmodifiable List<Pair<String, I>> getKeywords() {
@Nullable SoftReference<@Unmodifiable List<Pair<String, I>>> reference = keywordsRef;
@Unmodifiable List<Pair<String, I>> cached = reference != null ? reference.get() : null;
if (cached != null)
return cached;
lock.lock();
try {
reference = keywordsRef;
cached = reference != null ? reference.get() : null;
if (cached != null)
return cached;
List<Pair<String, I>> keywords = new ArrayList<>();
for (I info : getInfos()) {
if (!info.mainName().isEmpty())
keywords.add(Pair.pair(info.mainName(), info));
if (!info.subName().isEmpty())
keywords.add(Pair.pair(info.subName(), info));
if (!info.abbrName().isEmpty())
keywords.add(Pair.pair(info.abbrName(), info));
}
cached = List.copyOf(keywords);
keywordsRef = new SoftReference<>(cached);
return cached;
} finally {
lock.unlock();
}
}
/// Returns projects ranked by descending longest common subsequence length after removing
/// space characters from the query. Comparison is case-sensitive and uses UTF-16 units.
/// Matches require at least one unit and at least query length minus three units.
/// Each matching name contributes a result, so projects may occur more than once.
/// Ties preserve resource order, then primary name, secondary name and abbreviation order.
public @Unmodifiable List<I> search(String query) {
query = query.chars().filter(ch -> !Character.isSpaceChar(ch))
.collect(StringBuilder::new, (sb, ch) -> sb.append((char) ch), StringBuilder::append).toString();
@Unmodifiable List<Pair<String, I>> keywords = getKeywords();
int maxLength = keywords.stream().mapToInt(keyword -> keyword.getKey().length()).max().orElse(0);
StringUtils.LongestCommonSubsequence lcs = new StringUtils.LongestCommonSubsequence(query.length(), maxLength);
List<Pair<Integer, I>> matches = new ArrayList<>();
for (Pair<String, I> keyword : keywords) {
int score = lcs.calc(query, keyword.getKey());
if (score >= Math.max(1, query.length() - 3))
matches.add(Pair.pair(score, keyword.getValue()));
}
return matches.stream().sorted((a, b) -> Integer.compare(b.getKey(), a.getKey()))
.map(Pair::getValue).toList();
}
/// Returns cached project metadata, loading it on demand. Missing resources and I/O failures
/// produce an empty list; malformed records are skipped. The cache is softly referenced.
public @Unmodifiable List<I> getInfos() {
@Nullable SoftReference<@Unmodifiable List<I>> infosRef = this.infosRef;
@Unmodifiable List<I> infos = infosRef != null ? infosRef.get() : null;
if (infos != null)
return infos;
lock.lock();
try {
infosRef = this.infosRef;
infos = infosRef != null ? infosRef.get() : null;
if (infos != null)
return infos;
try {
infos = loadInfos();
} catch (IOException e) {
LOG.warning("Failed to load addon project info from " + assetsPath, e);
infos = List.of();
}
this.infosRef = new SoftReference<>(infos);
return infos;
} finally {
lock.unlock();
}
}
}
@@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.addon.update;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.addon.LocalAddonFile;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Pair;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class AddonCheckUpdatesTask extends Task<AddonUpdates> {
private final List<Task<Pair<AddonUpdate, @Nullable AddonUpdate>>> dependents;
public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<? extends LocalAddonFile<?>> addons) {
dependents = addons.stream().map(addon ->
Task.supplyAsync(Schedulers.io(), () -> {
AddonUpdate candidate = null;
AddonUpdate candidateRelease = null;
for (RemoteAddon.Source source : RemoteAddon.Source.values()) {
AddonUpdate update = null;
AddonUpdate updateRelease = null;
try {
var u = addon.checkUpdates(downloadProvider, gameVersion, source);
if (u != null) {
update = u.key();
updateRelease = u.value();
}
} catch (IOException e) {
LOG.warning(String.format("Cannot check update for addon %s.", addon.getFileName()), e);
}
if (update == null) continue;
if (candidate == null || candidate.targetVersion().datePublished().isBefore(update.targetVersion().datePublished()))
candidate = update;
if (updateRelease == null) continue;
if (candidateRelease == null || candidateRelease.targetVersion().datePublished().isBefore(updateRelease.targetVersion().datePublished()))
candidateRelease = updateRelease;
}
if (candidate == null) return null; // If there's no candidate for all channels, then no candidate for release channel
return Pair.pair(candidate, candidateRelease);
}).setName(addon.getFileName()).setSignificance(TaskSignificance.MAJOR).withCounter("update.checking")
).toList();
setStage("update.checking");
getProperties().put("total", dependents.size());
}
@Override
public boolean doPreExecute() {
return true;
}
@Override
public void preExecute() {
notifyPropertiesChanged();
}
@Override
public Collection<? extends Task<?>> getDependents() {
return dependents;
}
@Override
public boolean isRelyingOnDependents() {
return false;
}
@Override
public void execute() throws Exception {
List<AddonUpdate> commonUpdates = new ArrayList<>(), releaseUpdates = new ArrayList<>();
dependents.stream().map(Task::getResult).filter(Objects::nonNull)
.forEachOrdered(pair -> {
commonUpdates.add(pair.key());
if (pair.value() != null) releaseUpdates.add(pair.value());
});
setResult(new AddonUpdates(commonUpdates, releaseUpdates));
}
}
@@ -0,0 +1,108 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.addon.update;
import org.jackhuang.hmcl.addon.LocalAddonFile;
import org.jackhuang.hmcl.addon.LocalAddonManager;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class AddonUpdateTask extends Task<Void> {
private final Collection<Task<?>> dependents = new ArrayList<>();
private final List<LocalAddonFile<?>> failedAddons = new ArrayList<>();
public AddonUpdateTask(DownloadProvider downloadProvider, Path addonDirectory, List<AddonUpdate> addons) {
setStage("addon.check_update.confirm");
getProperties().put("total", addons.size());
for (AddonUpdate addon : addons) {
LocalAddonFile<?> local = addon.localAddonFile();
RemoteAddon.Version remote = addon.targetVersion();
boolean isDisabled = local.isDisabled();
String fileName = remote.file().filename();
if (isDisabled)
fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION);
String newFileName = fileName;
Path targetFile = addonDirectory.resolve(newFileName);
dependents.add(Task
.supplyAsync(Schedulers.io(), () -> Files.createTempFile("hmcl-addon-update-", ".tmp"))
.thenComposeAsync(tempFile ->
new FileDownloadTask(downloadProvider.getDownloadCandidates(remote.file().url()), tempFile)
.setName(remote.name())
.thenRunAsync(Schedulers.javafx(), () -> local.setOld(true))
.thenRunAsync(Schedulers.io(), () -> Files.move(tempFile, targetFile))
.whenComplete(Schedulers.javafx(), exception -> {
try {
Files.deleteIfExists(tempFile);
} catch (Exception e) {
LOG.warning("Failed to delete temp file " + tempFile, e);
}
if (exception != null) {
// restore state if failed
local.setOld(false);
if (isDisabled)
local.markDisabled();
} else {
local.onUpdated(newFileName);
if (!local.keepOldFiles()) {
try {
local.delete();
} catch (IOException e) {
LOG.warning("Failed to delete outdated addon: " + local.getFile(), e);
}
}
}
})
).whenComplete(Schedulers.javafx(), exception -> {
if (exception != null) {
LOG.warning("Failed to update addon", exception);
failedAddons.add(local);
}
}).withCounter("addon.check_update.confirm"));
}
}
public List<LocalAddonFile<?>> getFailedAddons() {
return failedAddons;
}
@Override
public Collection<Task<?>> getDependents() {
return dependents;
}
@Override
public boolean doPreExecute() {
return true;
}
@Override
public void preExecute() {
notifyPropertiesChanged();
}
@Override
public boolean isRelyingOnDependents() {
return false;
}
@Override
public void execute() throws Exception {
if (!isDependentsSucceeded())
throw getException();
}
}
@@ -0,0 +1,413 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.download;
import org.glavo.url.WebURL;
import org.jackhuang.hmcl.download.cleanroom.CleanroomRemoteVersion;
import org.jackhuang.hmcl.download.fabric.FabricRemoteVersion;
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.download.neoforge.NeoForgeRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
import org.jackhuang.hmcl.game.AssetObject;
import org.jackhuang.hmcl.game.GameComponentType;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.setting.DownloadSource;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.RegionUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
/// @author Glavo
@NotNullByDefault
public final class HMCLDownloadProvider extends DownloadProvider {
private volatile DownloadSource versionListSource = DownloadSource.MIRROR;
private volatile DownloadSource fileSource = DownloadSource.MIRROR;
private static boolean preferMirror(DownloadSource source) {
if ("CN".equals(RegionUtils.MACHINE_REGION)) {
return source != DownloadSource.OFFICIAL;
}
return source == DownloadSource.MIRROR;
}
private static DownloadCandidates getCandidates(
DownloadSource source,
WebURL officialUrl,
List<WebURL> mirrorUrls
) {
if (preferMirror(source)) {
var candidates = new ArrayList<DownloadCandidate>(mirrorUrls.size() + 1);
candidates.addAll(spreadPrimary(mirrorUrls.stream().map(DownloadCandidate::of).toList(), officialUrl.href()));
candidates.add(DownloadCandidate.of(officialUrl));
return DownloadCandidates.of(List.copyOf(candidates));
} else {
var candidates = new ArrayList<DownloadCandidate>(mirrorUrls.size() + 1);
candidates.add(DownloadCandidate.of(officialUrl));
for (WebURL mirrorUrl : mirrorUrls) {
candidates.add(DownloadCandidate.of(mirrorUrl));
}
return DownloadCandidates.of(List.copyOf(candidates));
}
}
/// Rotates the first mirror among the top entries using a stable hash of the URL, so concurrent
/// file downloads spread across healthy mirrors instead of stampeding one entry point.
private static List<DownloadCandidate> spreadPrimary(List<DownloadCandidate> mirrors, String key) {
if (mirrors.size() <= 1)
return mirrors;
int top = Math.min(2, mirrors.size());
int primary = Math.floorMod(key.hashCode(), top);
if (primary == 0)
return mirrors;
var rotated = new ArrayList<DownloadCandidate>(mirrors.size());
rotated.add(mirrors.get(primary));
for (int i = 0; i < mirrors.size(); i++) {
if (i != primary)
rotated.add(mirrors.get(i));
}
return rotated;
}
private static DownloadCandidates getCandidates(DownloadSource source, List<MirrorRule> rules, List<String> urls) {
if (urls.isEmpty()) {
throw new IllegalArgumentException("urls cannot be empty");
}
// true means prefer mirror, false means prefer official, null means do not use mirror
@Nullable Boolean mirrorFirst;
if ("CN".equals(RegionUtils.MACHINE_REGION)) {
mirrorFirst = switch (source) {
case DEFAULT, MIRROR -> true;
case OFFICIAL -> false;
};
} else {
mirrorFirst = switch (source) {
case DEFAULT, OFFICIAL -> null;
case MIRROR -> true;
};
}
if (mirrorFirst == null) {
return DownloadCandidates.of(urls.stream().map(DownloadCandidate::of).toArray(DownloadCandidate[]::new));
}
var candidates = new ArrayList<DownloadCandidate>(urls.size() * 3);
for (String url : urls) {
DownloadCandidate candidate = DownloadCandidate.of(url);
if (candidate.url() == null) {
// Invalid URL, just add it to the list and let the download task handle it.
candidates.add(candidate);
continue;
}
boolean added = false;
for (MirrorRule rule : rules) {
@Nullable List<DownloadCandidate> list = rule.getCandidates(candidate, mirrorFirst);
if (list != null) {
candidates.addAll(list);
added = true;
break;
}
}
if (!added)
candidates.add(candidate);
}
return DownloadCandidates.of(List.copyOf(candidates));
}
private final Supplier<@Unmodifiable List<String>> mirrorRoots;
private final List<MirrorRule> rules;
public HMCLDownloadProvider(Supplier<@Unmodifiable List<String>> mirrorRoots) {
this.mirrorRoots = Objects.requireNonNull(mirrorRoots);
this.rules = List.of(
new MultiRootRule("https://bmclapi2.bangbang93.com", Function.identity()),
new MultiRootRule("https://bmclapi.bangbang93.com", Function.identity()),
new MultiRootRule("https://launchermeta.mojang.com", Function.identity()),
new MultiRootRule("https://piston-meta.mojang.com", Function.identity()),
new MultiRootRule("https://piston-data.mojang.com", Function.identity()),
new MultiRootRule("https://launcher.mojang.com", Function.identity()),
new MultiRootRule("https://libraries.minecraft.net", root -> root + "/libraries"),
new MultiRootRule("http://files.minecraftforge.net/maven", root -> root + "/maven"),
new MultiRootRule("https://files.minecraftforge.net/maven", root -> root + "/maven"),
new MultiRootRule("https://maven.minecraftforge.net", root -> root + "/maven"),
(source, mirrorFirst) -> {
String neoforgedMaven = "https://maven.neoforged.net/";
String neoforgedMavenRelease = "https://maven.neoforged.net/releases/";
if (source.url() == null || !source.url().href().startsWith(neoforgedMaven))
return null;
var candidates = new ArrayList<DownloadCandidate>();
List<String> roots = mirrorRoots.get();
if (source.url().href().startsWith(neoforgedMavenRelease)) {
String suffix = source.url().href().substring(neoforgedMavenRelease.length());
List<DownloadCandidate> mirrors = roots.stream()
.map(root -> DownloadCandidate.of(root + "/maven/" + suffix))
.toList();
if (mirrorFirst) {
candidates.addAll(spreadPrimary(mirrors, source.url().href()));
candidates.add(source);
} else {
candidates.add(source);
candidates.addAll(mirrors);
}
} else {
candidates.add(source);
}
// https://neoforged.forgecdn.net
candidates.add(DownloadCandidate.of("https://neoforged.forgecdn.net/" + source.url().href().substring(neoforgedMaven.length())));
return candidates;
},
new MultiRootRule("http://dl.liteloader.com/versions", root -> root + "/maven"),
new MultiRootRule("https://dl.liteloader.com/versions", root -> root + "/maven"),
new MultiRootRule("https://meta.fabricmc.net", root -> root + "/fabric-meta"),
new MultiRootRule("https://maven.fabricmc.net", root -> root + "/maven"),
new MultiRootRule("https://authlib-injector.yushi.moe", root -> root + "/mirrors/authlib-injector"),
new SimpleRule("https://repo1.maven.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
new SimpleRule("https://repo.maven.apache.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
new SimpleRule("https://hmcl.glavo.site/metadata/cleanroom", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom"),
new SimpleRule("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"),
new SimpleRule("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
// https://github.com/mcmod-info-mirror/mcim-rust-api
new SimpleRule("https://api.modrinth.com", null, "https://mod.mcimirror.top/modrinth"),
new SimpleRule("https://cdn.modrinth.com", null, "https://mod.mcimirror.top"),
new SimpleRule("https://api.curseforge.com", null, "https://mod.mcimirror.top/curseforge"),
new SimpleRule("https://edge.forgecdn.net", null, "https://mod.mcimirror.top")
);
}
private String primaryMirrorRoot() {
List<String> roots = mirrorRoots.get();
return roots.isEmpty() ? "https://bmclapi2.bangbang93.com" : roots.get(0);
}
private List<WebURL> mirrorUrls(String suffix) {
return mirrorRoots.get().stream()
.map(root -> WebURL.parse(root + suffix))
.toList();
}
@Override
protected Task<? extends ComponentRemoteVersionList<?>> fetchVersionsAsync(GameComponentType type, @Nullable GameVersionNumber gameVersion) {
assert (gameVersion == null) == (type == GameComponentType.GAME);
String bmclRoot = primaryMirrorRoot();
switch (type) {
case FORGE -> {
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchOfficial = ForgeRemoteVersion.fetchAsync(this, gameVersion);
DownloadSource source = versionListSource;
if (source == DownloadSource.OFFICIAL) {
return fetchOfficial;
}
Task<ComponentRemoteVersionList<ForgeRemoteVersion>> fetchBMCL = ForgeRemoteVersion.fetchBMCLAsync(this, bmclRoot, gameVersion);
if (preferMirror(source)) {
return new FallbackTask<>(fetchBMCL, fetchOfficial);
} else {
return new FallbackTask<>(fetchOfficial, fetchBMCL);
}
}
case NEO_FORGE -> {
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchOfficial = NeoForgeRemoteVersion.fetchAsync(this, gameVersion);
DownloadSource source = versionListSource;
if (source == DownloadSource.OFFICIAL) {
return fetchOfficial;
}
Task<ComponentRemoteVersionList<NeoForgeRemoteVersion>> fetchBMCL = NeoForgeRemoteVersion.fetchBMCLAsync(bmclRoot, gameVersion);
if (preferMirror(source)) {
return new FallbackTask<>(fetchBMCL, fetchOfficial);
} else {
return new FallbackTask<>(fetchOfficial, fetchBMCL);
}
}
case OPTIFINE -> {
return OptiFineRemoteVersion.fetchBMCLAsync(this, bmclRoot, gameVersion);
}
}
return super.fetchVersionsAsync(type, gameVersion);
}
public void setFileSource(DownloadSource fileSource) {
this.fileSource = Objects.requireNonNull(fileSource);
}
public void setVersionListSource(DownloadSource versionListSource) {
this.versionListSource = Objects.requireNonNull(versionListSource);
}
@Override
public HMCLCacheRepository getCacheRepository() {
return HMCLCacheRepository.REPOSITORY;
}
@Override
public DownloadCandidates getGameVersionListCandidates() {
return getCandidates(
versionListSource,
GameRemoteVersion.VERSION_MANIFEST_URL,
mirrorUrls("/mc/game/version_manifest.json")
);
}
@Override
public FabricLikeVersionListCandidates getFabricVersionListCandidates() {
DownloadSource source = versionListSource;
return new FabricLikeVersionListCandidates(
getCandidates(source, FabricRemoteVersion.LOADER_META_URL, mirrorUrls("/fabric-meta/v2/versions/loader")),
getCandidates(source, FabricRemoteVersion.GAME_META_URL, mirrorUrls("/fabric-meta/v2/versions/game"))
);
}
@Override
public DownloadCandidates getCleanroomVersionListCandidates() {
return "CN".equals(RegionUtils.MACHINE_REGION)
? DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL, WebURL.parse("https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom/index.json"))
: DownloadCandidates.of(CleanroomRemoteVersion.LOADER_LIST_URL);
}
@Override
public DownloadCandidates getDownloadCandidates(List<String> urls) {
return getCandidates(fileSource, rules, urls);
}
@Override
public DownloadCandidates getVersionListCandidates(List<String> urls) {
return getCandidates(versionListSource, rules, urls);
}
@Override
public DownloadCandidates getAssetObjectCandidates(AssetObject assetObject) {
String location = assetObject.getLocation();
return getCandidates(
fileSource,
WebURL.parse("https://resources.download.minecraft.net/" + location),
mirrorUrls("/assets/" + location)
);
}
@NotNullByDefault
private static final class FallbackTask<T> extends Task<T> {
private final Task<? extends T> task;
private final Task<? extends T> fallback;
private List<Task<?>> dependencies = List.of();
public FallbackTask(Task<? extends T> task, Task<? extends T> fallback) {
this.task = task;
this.fallback = fallback;
}
@Override
public Collection<? extends Task<?>> getDependents() {
return List.of(task);
}
@Override
public boolean isRelyingOnDependents() {
return false;
}
@Override
public void execute() throws Exception {
if (isDependentsSucceeded()) {
setResult(task.getResult());
} else {
dependencies = List.of(fallback);
fallback.storeTo(this::setResult);
}
}
@Override
public List<Task<?>> getDependencies() {
return dependencies;
}
}
@NotNullByDefault
@FunctionalInterface
private interface MirrorRule {
@Nullable List<DownloadCandidate> getCandidates(DownloadCandidate source, boolean mirrorFirst);
}
@NotNullByDefault
private final class MultiRootRule implements MirrorRule {
private final String source;
private final Function<String, String> rootMapper;
private MultiRootRule(String source, Function<String, String> rootMapper) {
this.source = source;
this.rootMapper = rootMapper;
}
@Override
public @Nullable List<DownloadCandidate> getCandidates(DownloadCandidate source, boolean mirrorFirst) {
if (source.url() == null || !source.url().href().startsWith(this.source))
return null;
String urlWithoutPrefix = source.url().href().substring(this.source.length());
List<DownloadCandidate> mirrors = mirrorRoots.get().stream()
.map(root -> DownloadCandidate.of(rootMapper.apply(root) + urlWithoutPrefix))
.toList();
var candidates = new ArrayList<DownloadCandidate>(mirrors.size() + 1);
if (mirrorFirst) {
candidates.addAll(spreadPrimary(mirrors, source.url().href()));
candidates.add(source);
} else {
candidates.add(source);
candidates.addAll(mirrors);
}
return candidates;
}
}
@NotNullByDefault
private record SimpleRule(String source, @Nullable String first, @Nullable String fallback) implements MirrorRule {
public SimpleRule(String source, String target) {
this(source, target, null);
}
@Override
public @Nullable List<DownloadCandidate> getCandidates(DownloadCandidate source, boolean mirrorFirst) {
if (source.url() == null || !source.url().href().startsWith(this.source))
return null;
boolean addSourceFirst = !mirrorFirst || first == null;
String urlWithoutPrefix = source.url().href().substring(this.source.length());
var candidates = new ArrayList<DownloadCandidate>();
if (addSourceFirst)
candidates.add(source);
if (first != null)
candidates.add(DownloadCandidate.of(first + urlWithoutPrefix));
if (!addSourceFirst)
candidates.add(source);
if (fallback != null)
candidates.add(DownloadCandidate.of(fallback + urlWithoutPrefix));
return candidates;
}
}
}
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import javafx.beans.property.SimpleStringProperty;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.download.DefaultCacheRepository;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.download.DefaultGameBuilder;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import com.google.gson.JsonObject;
@@ -470,9 +455,9 @@ public class HMCLGameInstance extends DefaultGameInstance {
return GameInstanceIconType.OPTIFINE.getIcon();
GameVersionNumber version = getVersion();
if (version.isAprilFools())
return GameInstanceIconType.APRIL_FOOLS.getIcon();
else if (version instanceof GameVersionNumber.LegacySnapshot || version instanceof GameVersionNumber.Release release && release.getEaType() != GameVersionNumber.Release.ReleaseType.GA)
if (version instanceof GameVersionNumber.LegacySnapshot
|| version.isAprilFools()
|| version instanceof GameVersionNumber.Release release && release.getEaType() != GameVersionNumber.Release.ReleaseType.GA)
return GameInstanceIconType.COMMAND.getIcon();
else if (version instanceof GameVersionNumber.Old)
return GameInstanceIconType.CRAFT_TABLE.getIcon();
@@ -565,7 +550,13 @@ public class HMCLGameInstance extends DefaultGameInstance {
return getLayout().getInstanceGameSettingsFile(id);
}
public LaunchOptions.Builder getLaunchOptions(JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
public LaunchOptions.Builder getLaunchOptions(
JavaRuntime javaRuntime,
Path javaExecutable,
Path gameDir,
List<String> javaAgents,
List<String> javaArguments,
boolean makeLaunchScript) {
GameSettings.Effective vs = getEffectiveSettings();
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
boolean autoMemory = vs.getInheritable(GameSettings::autoMemoryProperty);
@@ -576,8 +567,8 @@ public class HMCLGameInstance extends DefaultGameInstance {
maxMemory = noJVMOptions
? null
: Math.toIntExact(HMCLGameRepository.getAutoAllocatedMemory(
SystemInfo.getPhysicalMemoryStatus().available(),
javaVersion.getPlatform()) / 1024L / 1024L);
SystemInfo.getPhysicalMemoryStatus(),
javaRuntime.getPlatform()) / 1024L / 1024L);
} else {
maxMemory = vs.getMaxMemory();
}
@@ -585,7 +576,8 @@ public class HMCLGameInstance extends DefaultGameInstance {
LaunchOptions.Builder builder = new LaunchOptions.Builder()
.setInstanceId(getId())
.setGameDir(gameDir)
.setJava(javaVersion)
.setJava(javaRuntime)
.setJavaExecutable(javaExecutable)
.setVersionType(Metadata.TITLE)
.setVersionName(getId().id())
.setProfileName(Metadata.TITLE)
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.Metadata;
@@ -106,6 +91,12 @@ public final class HMCLGameLauncher extends DefaultLauncher {
if (gameVersion == GameVersionNumber.unknown() || gameVersion.compareTo("1.1") < 0)
return;
// These snapshots lack the external language resources needed for non-English locales.
// Selecting an unavailable language causes a NullPointerException during startup.
if (gameVersion instanceof GameVersionNumber.LegacySnapshot
&& gameVersion.compareTo("13w16a") >= 0 && gameVersion.compareTo("13w23b") <= 0)
return;
String lang = normalizedLanguageTag(locale, gameVersion);
if (lang.isEmpty())
return;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import javafx.beans.binding.Bindings;
@@ -34,11 +19,13 @@ import org.jackhuang.hmcl.setting.LegacyGameSettingsMigrator;
import org.jackhuang.hmcl.setting.GameSettingsPresetID;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.MathUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Bits;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
@@ -511,23 +498,43 @@ public final class HMCLGameRepository extends DefaultGameRepository {
}
}
public static long getAutoAllocatedMemory(long available, Platform platform) {
long usable = available - 512 * 1024 * 1024; // Reserve 512 MiB memory for off-heap memory and HMCL itself
if (usable <= 0) {
return available;
public static long getAutoAllocatedMemory(PhysicalMemoryStatus memoryStatus, Platform platform) {
final long minHeapSize;
final long maxHeapSize;
if (platform.getBits() == Bits.BIT_32) {
// https://github.com/HMCL-dev/HMCL/issues/6638
maxHeapSize = 768 * 1024 * 1024; // 768 MiB
minHeapSize = 256 * 1024 * 1024; // 128 MiB
} else {
maxHeapSize = 24L * 1024 * 1024 * 1024; // 24 GiB
minHeapSize = 512 * 1024 * 1024; // 512 MiB
}
final long threshold = 8L * 1024 * 1024 * 1024; // 8 GiB
final long suggested;
if (usable <= threshold)
// Reserve memory for off-heap memory and HMCL itself
final long reserved = 256 * 1024 * 1024; // 256 MiB
final long threshold1 = 8L * 1024 * 1024 * 1024; // 8 GiB
final long threshold2 = 16L * 1024 * 1024 * 1024; // 16 GiB
if (memoryStatus.total() <= minHeapSize || memoryStatus.available() < 0) {
return -1L;
}
final long usable = Math.max(
Math.max(memoryStatus.available(), memoryStatus.total() / 16) - reserved,
0
);
long suggested;
if (usable <= threshold1)
suggested = (long) (usable * 0.8);
else if (usable <= threshold2)
suggested = (long) (threshold1 * 0.8 + (usable - threshold1) * 0.4);
else
suggested = Math.min(
(long) (threshold * 0.8 + (usable - threshold) * 0.2),
16L * 1024 * 1024 * 1024);
return platform.getBits() == Bits.BIT_32
? Math.min(suggested, 768 * 1024 * 1024) // https://github.com/HMCL-dev/HMCL/issues/6638
: suggested;
suggested = (long) (threshold1 * 0.8 + (threshold2 - threshold1) * 0.4 + (usable - threshold2) * 0.2);
return MathUtils.clamp(suggested & ~0xFFFFFL, minHeapSize, maxHeapSize);
}
}
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
@@ -129,8 +114,8 @@ public final class HMCLModpackInstallTask extends Task<Void> {
}
});
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), List.of("/minecraft"), it -> !"pack.json".equals(it), config));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), List.of("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack"));
dependents.add(new ModpackInstallTask<>(zipFile, run, List.of("/minecraft"), it -> !"pack.json".equals(it), config));
dependents.add(new MinecraftInstanceTask<>(zipFile, List.of("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getLayout().getModpackConfigurationFile(this.instanceId)).withStage("hmcl.modpack"));
}
@Override
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.modpack.ModpackManifest;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2022 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
@@ -31,8 +16,8 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.Set;
public final class HMCLModpackProvider implements ModpackProvider {
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
@@ -48,7 +33,12 @@ public final class HMCLModpackProvider implements ModpackProvider {
}
@Override
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
public Task<?> createUpdateTask(
DefaultDependencyManager dependencyManager,
DefaultGameInstance instance,
Path zipFile,
Modpack modpack,
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException {
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
@@ -60,9 +50,9 @@ public final class HMCLModpackProvider implements ModpackProvider {
}
@Override
public Modpack readManifest(ZipArchiveReader file, Path path, Charset encoding) throws IOException, JsonParseException {
public Modpack readManifest(ZipArchiveReader file, Path path) throws IOException, JsonParseException {
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json");
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class);
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json");
GameInstanceManifest game = JsonUtils.fromNonNullJson(gameJson, GameInstanceManifest.class);
if (game.jar() == null)
@@ -77,7 +67,12 @@ public final class HMCLModpackProvider implements ModpackProvider {
private final static class HMCLModpack extends Modpack {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) {
public Task<?> getInstallTask(
DefaultDependencyManager dependencyManager,
Path zipFile,
GameInstanceID instanceId,
String iconUrl,
@Nullable Set<String> excludedFiles) {
return new HMCLModpackInstallTask((HMCLGameRepository) dependencyManager.getGameRepository(), zipFile, this, instanceId);
}
}
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import com.jfoenix.controls.JFXButton;
@@ -32,15 +17,14 @@ import org.jackhuang.hmcl.launch.*;
import org.jackhuang.hmcl.modpack.ModpackCompletionException;
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
import org.jackhuang.hmcl.modpack.ModpackProvider;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.GameSettings;
import org.jackhuang.hmcl.setting.JavaVersionType;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.task.*;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.*;
@@ -57,13 +41,12 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
@@ -150,29 +133,46 @@ public final class LauncherHelper {
}
/// Builds and executes the launch pipeline for the captured game instance.
@SuppressWarnings("NonAtomicOperationOnVolatileField")
private void launch0() {
// https://github.com/HMCL-dev/HMCL/pull/4121
PROCESSES.removeIf(it -> it.get() == null);
final var context = new Object() {
volatile GameInstanceManifest launchManifest;
volatile JavaRuntime javaRuntime;
volatile Path javaExecutable;
};
HMCLGameRepository repository = repository();
DefaultDependencyManager dependencyManager = repository.getDependency();
// Resolve already deduplicated libraries; apply loader-specific argument repairs for this launch.
var launchManifest = new AtomicReference<>(LaunchManifestNormalizer.repairForLaunch(gameInstance.getResolvedManifest()));
boolean integrityCheck = gameInstance.unmarkLaunchedAbnormally();
CountDownLatch launchingLatch = new CountDownLatch(1);
List<String> javaAgents = new ArrayList<>(0);
List<String> javaArguments = new ArrayList<>(0);
AtomicReference<JavaRuntime> javaVersionRef = new AtomicReference<>();
// Resolve already deduplicated libraries; apply loader-specific argument repairs for this launch.
context.launchManifest = LaunchManifestNormalizer.repairForLaunch(gameInstance.getResolvedManifest());
TaskExecutor executor = checkGameState(gameInstance, setting, launchManifest.get())
TaskExecutor executor = checkGameState(gameInstance, setting, context.launchManifest)
.thenComposeAsync(java -> {
javaVersionRef.set(Objects.requireNonNull(java));
launchManifest.set(NativePatcher.patchNative(gameInstance, launchManifest.get(), java, setting, javaArguments));
context.javaRuntime = Objects.requireNonNull(java);
context.javaExecutable = java.getBinary();
if (java.getPlatform().os() == OperatingSystem.WINDOWS
&& scriptFile == null
&& !setting.getInheritable(GameSettings::notUseJavawProperty)
&& FileUtils.getName(java.getBinary()).equalsIgnoreCase("java.exe")) {
Path javawBinary = java.getBinary().resolveSibling("javaw.exe");
if (Files.isRegularFile(javawBinary)) {
context.javaExecutable = javawBinary;
}
}
context.launchManifest = NativePatcher.patchNative(gameInstance, context.launchManifest, java, setting, javaArguments);
if (setting.getInheritable(GameSettings::notCheckGameProperty))
return null;
return Task.allOf(
dependencyManager.checkGameCompletionAsync(gameInstance, launchManifest.get(), integrityCheck),
dependencyManager.checkGameCompletionAsync(gameInstance, context.launchManifest, integrityCheck),
Task.composeAsync(() -> {
try {
@Nullable ModpackConfiguration<?> configuration =
@@ -205,7 +205,7 @@ public final class LauncherHelper {
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
if (GameLibrariesTask.shouldDownloadLibrary(repository, launchManifest.get(), lib, integrityCheck)) {
if (GameLibrariesTask.shouldDownloadLibrary(repository, context.launchManifest, lib, integrityCheck)) {
return new LibraryDownloadTask(dependencyManager, file, lib)
.thenRunAsync(() -> javaAgents.add(agent));
} else {
@@ -215,13 +215,13 @@ public final class LauncherHelper {
})
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> new GameVerificationFixTask(gameInstance, gameInstance.getVersion(), launchManifest.get()))
.thenComposeAsync(() -> new GameVerificationFixTask(gameInstance, gameInstance.getVersion(), context.launchManifest))
.thenComposeAsync(() -> {
if (setting.getInheritable(GameSettings::allowAutoAgentProperty)
|| setting.getInheritable(GameSettings::noJVMOptionsProperty)
|| setting.getInheritable(GameSettings::noOptimizingJVMOptionsProperty)
|| Boolean.TRUE.equals(state().getShownTips().get(LWJGL_3_4_1_TIP))
|| !NativePatcher.needPatchMemoryUtil(launchManifest.get(), javaVersionRef.get().getParsedVersion())) {
|| !NativePatcher.needPatchMemoryUtil(context.launchManifest, context.javaRuntime.getParsedVersion())) {
return Task.completed(null);
} else {
CompletableFuture<Void> future = new CompletableFuture<>();
@@ -241,7 +241,12 @@ public final class LauncherHelper {
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions.Builder launchOptionsBuilder = gameInstance.getLaunchOptions(
javaVersionRef.get(), repository.getBaseDirectory(), javaAgents, javaArguments, scriptFile != null);
context.javaRuntime,
context.javaExecutable,
repository.getBaseDirectory(),
javaAgents,
javaArguments,
scriptFile != null);
if (disableOfflineSkin) {
launchOptionsBuilder.setDaemon(false);
}
@@ -250,7 +255,7 @@ public final class LauncherHelper {
}
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && setting.getInheritable(GameSettings::highPerformanceProperty)) {
@Nullable String javaPathGpuReg = FileUtils.getAbsolutePath(javaVersionRef.get().getBinary());
@Nullable String javaPathGpuReg = FileUtils.getAbsolutePath(context.javaExecutable);
try {
WinReg reg = WinReg.INSTANCE;
if (reg != null) {
@@ -285,7 +290,7 @@ public final class LauncherHelper {
return new HMCLGameLauncher(
gameInstance,
launchManifest.get(),
context.launchManifest,
authInfo,
launchOptions,
launcherVisibility == LauncherVisibility.CLOSE
@@ -307,10 +312,10 @@ public final class LauncherHelper {
if (launcherVisibility == LauncherVisibility.CLOSE)
Launcher.stopApplication();
else
launchingStepsPane.setCancel(new TaskCancellationAction(it -> {
launchingStepsPane.setCancel(it -> {
process.stop();
it.fireEvent(new DialogCloseEvent());
}));
});
} else {
runLater(() -> {
launchingStepsPane.fireEvent(new DialogCloseEvent());
@@ -356,26 +361,26 @@ public final class LauncherHelper {
message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().name()) + "\n";
if (ex.getCause() instanceof ResponseCodeException rce) {
int responseCode = rce.getResponseCode();
String uri = rce.getUri();
String url = rce.getUrl();
if (responseCode == 404)
message += i18n("download.code.404", uri);
message += i18n("download.code.404", url);
else
message += i18n("download.failed", uri, responseCode);
message += i18n("download.failed", url, responseCode);
} else {
message += StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof DownloadException) {
URI uri = ((DownloadException) ex).getUri();
} else if (ex instanceof DownloadException de) {
String url = de.getUrl();
if (ex.getCause() instanceof SocketTimeoutException) {
message = i18n("install.failed.downloading.timeout", uri);
message = i18n("install.failed.downloading.timeout", url);
} else if (ex.getCause() instanceof ResponseCodeException responseCodeException) {
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
message = i18n("download.code." + responseCodeException.getResponseCode(), uri);
message = i18n("download.code." + responseCodeException.getResponseCode(), url);
} else {
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else {
message = i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(ex.getCause());
message = i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(ex.getCause());
}
} else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) {
message = i18n("assets.index.malformed");
@@ -385,11 +390,11 @@ public final class LauncherHelper {
message = i18n("account.failed.character_deleted");
} else if (ex instanceof ResponseCodeException rce) {
int responseCode = rce.getResponseCode();
String uri = rce.getUri();
String url = rce.getUrl();
if (responseCode == 404)
message = i18n("download.code.404", uri);
message = i18n("download.code.404", url);
else
message = i18n("download.failed", uri, responseCode);
message = i18n("download.failed", url, responseCode);
} else if (ex instanceof CommandTooLongException) {
message = i18n("launch.failed.command_too_long");
} else if (ex instanceof ExecutionPolicyLimitException) {
@@ -423,10 +428,11 @@ public final class LauncherHelper {
}
});
executor.start();
launchingStepsPane.startExecutor();
}
private static Task<JavaRuntime> checkGameState(HMCLGameInstance gameInstance, GameSettings.Effective setting, GameInstanceManifest manifest) {
/// Checks launch requirements, allowing required Java downloads to share this launch's execution slot.
private Task<JavaRuntime> checkGameState(HMCLGameInstance gameInstance, GameSettings.Effective setting, GameInstanceManifest manifest) {
GameComponentAnalyzer analyzer = GameComponentAnalyzer.analyze(manifest, gameInstance.getVersion());
GameVersionNumber gameVersion = gameInstance.getVersion();
@@ -728,13 +734,18 @@ public final class LauncherHelper {
return task.withStage("launch.state.java");
}
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, HMCLGameRepository repository) {
/// Prompts for and downloads Java as a required child of this launch operation.
private CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, HMCLGameRepository repository) {
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Controllers.dialog(new MessageDialogPane.Builder(
i18n("launch.advice.require_newer_java_version", javaVersion.majorVersion()),
i18n("message.warning"),
MessageType.QUESTION)
.yesOrNo(() -> {
if (!launchingStepsPane.canStartChild()) {
future.completeExceptionally(new CancellationException());
return;
}
DownloadProvider downloadProvider = repository.getDependency().getDownloadProvider();
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
@@ -744,11 +755,11 @@ public final class LauncherHelper {
Throwable resolvedException = resolveException(exception);
LOG.warning("Failed to download java", exception);
if (!(resolvedException instanceof CancellationException)) {
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
Controllers.dialog(I18n.localizeErrorMessage(resolvedException), i18n("install.failed"));
}
future.completeExceptionally(new CancellationException());
}
}), i18n("download.java"), new TaskCancellationAction(() -> future.completeExceptionally(new CancellationException())));
}), i18n("download.java"), it -> future.completeExceptionally(new CancellationException()), launchingStepsPane);
}, () -> future.completeExceptionally(new CancellationException())).build());
return future;
@@ -1,29 +1,16 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2022 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
import org.jackhuang.hmcl.ui.instances.ModTranslations;
import org.jackhuang.hmcl.addon.AddonProjectInfo;
import org.jackhuang.hmcl.addon.AddonProjectInfoRegistry;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
@@ -50,16 +37,17 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
}
@Override
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
if (!StringUtils.containsChinese(searchFilter)) {
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
public SearchResult search(DownloadProvider downloadProvider, RemoteAddon.Type type, String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
@Nullable AddonProjectInfoRegistry<?> registry = AddonProjectInfoRegistry.getByAddonType(type);
if (registry == null || !StringUtils.containsChinese(searchFilter)) {
return getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
}
Set<String> englishSearchFiltersSet = new LinkedHashSet<>(INITIAL_CAPACITY);
int count = 0;
for (ModTranslations.Mod mod : ModTranslations.getTranslationsByAddonType(getType()).searchMod(searchFilter)) {
String englishSearchFilter = String.join(" ", StringUtils.tokenize(StringUtils.isNotBlank(mod.getSubname()) ? mod.getSubname() : mod.getName()));
for (AddonProjectInfo mod : registry.search(searchFilter)) {
String englishSearchFilter = String.join(" ", StringUtils.tokenize(StringUtils.isNotBlank(mod.subName()) ? mod.subName() : mod.mainName()));
if (StringUtils.isNotBlank(englishSearchFilter)) {
englishSearchFiltersSet.add(englishSearchFilter);
}
@@ -69,7 +57,7 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
}
if (englishSearchFiltersSet.isEmpty()) {
return getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
return getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, searchFilter, sort, sortOrder);
}
RemoteAddon[] searchResultArray = new RemoteAddon[pageSize];
@@ -78,7 +66,7 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
SearchResult searchResult = null;
List<RemoteAddon> remoteAddons = List.of();
for (String englishSearchFilter : englishSearchFiltersSet) {
searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, englishSearchFilter, SortType.RELEVANCY, sortOrder);
searchResult = getBackedRemoteModRepository().search(downloadProvider, type, gameVersion, category, pageOffset, pageSize, englishSearchFilter, SortType.RELEVANCY, sortOrder);
remoteAddons = searchResult.results().toList();
if (!remoteAddons.isEmpty()) {
break;
@@ -91,8 +79,8 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
continue;
}
ModTranslations.Mod chineseTranslation = ModTranslations.getTranslationsByAddonType(getType()).getModByCurseForgeId(remoteAddon.slug());
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.getName()) && StringUtils.containsChinese(chineseTranslation.getName())) {
@Nullable AddonProjectInfo chineseTranslation = registry.getInfoByCurseForgeId(remoteAddon.slug());
if (chineseTranslation != null && !StringUtils.isBlank(chineseTranslation.mainName()) && StringUtils.containsChinese(chineseTranslation.mainName())) {
searchResultArray[chineseIndex++] = remoteAddon;
} else {
searchResultArray[englishIndex--] = remoteAddon;
@@ -103,11 +91,11 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
return new SearchResult(Stream.concat(Arrays.stream(searchResultArray, 0, chineseIndex).map(remoteMod -> {
ModTranslations.Mod chineseRemoteMod = ModTranslations.getTranslationsByAddonType(getType()).getModByCurseForgeId(remoteMod.slug());
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.getName()) || !StringUtils.containsChinese(chineseRemoteMod.getName())) {
@Nullable AddonProjectInfo chineseRemoteMod = registry.getInfoByCurseForgeId(remoteMod.slug());
if (chineseRemoteMod == null || StringUtils.isBlank(chineseRemoteMod.mainName()) || !StringUtils.containsChinese(chineseRemoteMod.mainName())) {
return Pair.pair(remoteMod, Integer.MAX_VALUE);
}
String chineseRemoteModName = chineseRemoteMod.getName();
String chineseRemoteModName = chineseRemoteMod.mainName();
if (searchFilter.isEmpty() || chineseRemoteModName.isEmpty()) {
return Pair.pair(remoteMod, Math.max(searchFilter.length(), chineseRemoteModName.length()));
}
@@ -122,8 +110,8 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
}
@Override
public Stream<Category> getCategories() throws IOException {
return getBackedRemoteModRepository().getCategories();
public Stream<Category> getCategories(RemoteAddon.Type type) throws IOException {
return getBackedRemoteModRepository().getCategories(type);
}
@Override
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.Log4jLevel;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import java.nio.file.Path;
@@ -1,26 +1,10 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.Unzipper;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -29,12 +13,10 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
private final Path zipFile;
private final Charset charset;
private final String name;
public ManuallyCreatedModpackInstallTask(Path zipFile, Charset charset, String name) {
public ManuallyCreatedModpackInstallTask(Path zipFile, String name) {
this.zipFile = zipFile;
this.charset = charset;
this.name = name;
setName(i18n("modpack.installing"));
@@ -51,7 +33,6 @@ public class ManuallyCreatedModpackInstallTask extends Task<Path> {
new Unzipper(zipFile, dest)
.setSubDirectory(subdirectory)
.setTerminateIfSubDirectoryNotExists()
.setEncoding(charset)
.unzip();
}
}
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException;
@@ -48,7 +33,6 @@ import org.jetbrains.annotations.Nullable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -56,6 +40,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -76,7 +61,7 @@ public final class ModpackHelper {
);
static {
MultiMCComponents.setImplementation(Metadata.FULL_TITLE);
MultiMCComponents.setImplementation(Metadata.FULL_NAME + " v" + Metadata.VERSION);
}
@Nullable
@@ -89,8 +74,8 @@ public final class ModpackHelper {
return "zip".equals(ext) || "mrpack".equals(ext);
}
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try (ZipArchiveReader zipFile = CompressingUtils.openZipFile(file, charset)) {
public static Modpack readModpackManifest(Path file) throws UnsupportedModpackException, ManuallyCreatedModpackException {
try (ZipArchiveReader zipFile = CompressingUtils.openZipFile(file)) {
// Order for trying detecting manifest is necessary here.
// Do not change to iterating providers.
for (ModpackProvider provider : new ModpackProvider[]{
@@ -101,7 +86,7 @@ public final class ModpackHelper {
MultiMCModpackProvider.INSTANCE,
ServerModpackProvider.INSTANCE}) {
try {
return provider.readManifest(zipFile, file, charset);
return provider.readManifest(zipFile, file);
} catch (Exception ignored) {
}
}
@@ -172,6 +157,7 @@ public final class ModpackHelper {
return new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instanceId)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStage("hmcl.modpack")
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
@@ -179,12 +165,13 @@ public final class ModpackHelper {
return Files.exists(Paths.get("externalgames").resolve(name));
}
public static Task<?> getInstallManuallyCreatedModpackTask(Path zipFile, String name, Charset charset) {
/// Extracts a manually created modpack and selects its new external game directory.
public static Task<?> getInstallManuallyCreatedModpackTask(Path zipFile, String name) {
if (isExternalGameNameConflicts(name)) {
throw new IllegalArgumentException("name existing");
}
return new ManuallyCreatedModpackInstallTask(zipFile, charset, name)
return new ManuallyCreatedModpackInstallTask(zipFile, name)
.thenAcceptAsync(Schedulers.javafx(), location -> {
GameDirectory newGameDirectory = new GameDirectory(
GameDirectoryManager.newGameDirectoryId(),
@@ -192,10 +179,32 @@ public final class ModpackHelper {
PortablePath.fromPath(location));
GameDirectoryManager.addLocalGameDirectory(newGameDirectory);
GameDirectoryManager.setSelectedGameDirectory(newGameDirectory);
});
})
.withStage("hmcl.modpack")
.withStagesHints("hmcl.modpack");
}
public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, @Nullable String iconUrl) {
return getInstallTask(repository, zipFile, instanceId, modpack, iconUrl, null);
}
/// Creates an install task that respects optional-file selection.
///
/// @param repository the target repository
/// @param zipFile the modpack archive
/// @param instanceId the new instance id
/// @param modpack the parsed modpack
/// @param iconUrl the optional icon URL, or `null`
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
/// When non-null, must not contain `null` elements.
/// @return the install task
public static Task<?> getInstallTask(
HMCLGameRepository repository,
Path zipFile,
GameInstanceID instanceId,
Modpack modpack,
@Nullable String iconUrl,
@Nullable Set<String> excludedFiles) {
ExceptionalRunnable<?> success = () -> {
repository.refresh();
repository.getInstance(instanceId).enableIsolation();
@@ -209,22 +218,26 @@ public final class ModpackHelper {
};
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
.withStage("hmcl.modpack")
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), instanceId))
.withStage("hmcl.modpack")
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStage("hmcl.modpack")
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
public static Task<Void> getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, Charset charset, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
/// Updates a server modpack with backup, repository refresh, and modpack stage hints.
public static Task<Void> getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
switch (configuration.getType()) {
case ServerModpackRemoteInstallTask.MODPACK_TYPE: {
HMCLGameInstance instance = repository.getInstance(instanceId);
@@ -232,6 +245,7 @@ public final class ModpackHelper {
instance,
new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instance))
.thenComposeAsync(repository.refreshAsync())
.withStage("hmcl.modpack")
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
default:
@@ -239,19 +253,42 @@ public final class ModpackHelper {
}
}
public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, Charset charset, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
ModpackProvider provider = getProviderByType(configuration.getType());
public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
return getUpdateTask(repository, zipFile, instanceId, configuration, null);
}
/// Creates an update task that respects optional-file selection.
///
/// @param repository the target repository
/// @param zipFile the modpack archive
/// @param instanceId the instance to update
/// @param configuration the existing modpack configuration
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
/// When non-null, must not contain `null` elements.
/// @return the update task
public static Task<?> getUpdateTask(
HMCLGameRepository repository,
Path zipFile,
GameInstanceID instanceId,
ModpackConfiguration<?> configuration,
@Nullable Set<String> excludedFiles)
throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile);
@Nullable ModpackProvider provider = getProviderByType(configuration.getType());
if (provider == null) {
throw new UnsupportedModpackException();
}
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack)
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
.thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
.thenComposeAsync(repository.refreshAsync());
.thenComposeAsync(repository.refreshAsync())
.withStage("hmcl.modpack")
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack)
.thenComposeAsync(repository.refreshAsync());
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
.thenComposeAsync(repository.refreshAsync())
.withStage("hmcl.modpack")
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
public static void toGameSettings(MultiMCInstanceConfiguration c, GameSettings.Instance setting) {
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import fi.iki.elonen.NanoHTTPD;
@@ -146,7 +131,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String html;
try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%style%", Themes.getTheme().toColorScheme().toStyleSheet().replace("-monet", "--monet"))
.replace("%style%", Themes.getColorScheme().toStyleSheet().replace("-monet", "--monet"))
.replace("%lang%", Locale.getDefault().toLanguageTag())
.replace("%success%", i18n("message.success"))
.replace("%ok%", i18n("button.ok"))
@@ -0,0 +1,122 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// Resolves the routes that the one-click join button can connect through.
@NotNullByDefault
public final class ServerEndpoint {
private ServerEndpoint() {
}
/// One selectable connection route.
///
/// @param name the display name
/// @param address the `host[:port]` or `ws(s)://` address handed to the game
public record Route(String name, String address) {
}
/// Lists the routes configured for an instance.
///
/// Routes in the installed modpack manifest win, so they change with modpack updates. Without
/// them the manifest's single `serverAddress` and then [Metadata#getDefaultServerAddress()] are
/// used, each as a single route.
///
/// @param configurationFile the instance's `modpack.json` path
/// @return the routes in display order; empty when nothing is configured
public static @Unmodifiable List<Route> routes(Path configurationFile) {
List<Route> result = new ArrayList<>();
@Nullable ServerModpackManifest manifest = readManifest(configurationFile);
if (manifest != null) {
for (ServerModpackManifest.Route route : manifest.getRoutes()) {
if (StringUtils.isNotBlank(route.getAddress())) {
result.add(new Route(StringUtils.isBlank(route.getName()) ? route.getAddress().trim() : route.getName(), route.getAddress().trim()));
}
}
if (result.isEmpty() && StringUtils.isNotBlank(manifest.getServerAddress())) {
result.add(new Route("default", manifest.getServerAddress().trim()));
}
}
if (result.isEmpty()) {
String fallback = Metadata.getDefaultServerAddress();
if (!fallback.isEmpty()) {
result.add(new Route("default", fallback));
}
}
return List.copyOf(result);
}
/// Picks the route to join: the one the player chose last if it still exists, otherwise the first.
///
/// @param configurationFile the instance's `modpack.json` path
/// @return the route to connect through, or `null` when nothing is configured
public static @Nullable Route selected(Path configurationFile) {
List<Route> routes = routes(configurationFile);
if (routes.isEmpty()) {
return null;
}
@Nullable String saved = readSelectedName(configurationFile);
for (Route route : routes) {
if (route.name().equals(saved)) {
return route;
}
}
return routes.get(0);
}
/// Remembers the player's route choice next to the instance's modpack configuration.
///
/// @param configurationFile the instance's `modpack.json` path
/// @param route the chosen route
public static void select(Path configurationFile, Route route) {
try {
Files.writeString(selectionFile(configurationFile), route.name(), StandardCharsets.UTF_8);
} catch (IOException e) {
LOG.warning("Unable to save the selected route", e);
}
}
private static Path selectionFile(Path configurationFile) {
return configurationFile.resolveSibling("server-route.txt");
}
private static @Nullable String readSelectedName(Path configurationFile) {
try {
Path file = selectionFile(configurationFile);
return Files.isRegularFile(file) ? Files.readString(file, StandardCharsets.UTF_8).trim() : null;
} catch (IOException e) {
return null;
}
}
private static @Nullable ServerModpackManifest readManifest(Path configurationFile) {
if (!Files.isRegularFile(configurationFile)) {
return null;
}
try {
ModpackConfiguration<ServerModpackManifest> configuration =
JsonUtils.fromJsonFile(configurationFile, ModpackConfiguration.typeOf(ServerModpackManifest.class));
return configuration != null ? configuration.getManifest() : null;
} catch (Exception e) {
LOG.warning("Unable to read server routes from " + configurationFile, e);
return null;
}
}
}
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.game;
import javafx.beans.InvalidationListener;
@@ -33,6 +18,7 @@ import org.jackhuang.hmcl.auth.microsoft.MicrosoftAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -99,7 +85,7 @@ public final class TexturesLoader {
if (!Files.isRegularFile(file)) {
// download it
try {
new FileDownloadTask(texture.url(), file).run();
new FileDownloadTask(DownloadProviders.getDownloadProvider().getDownloadCandidates(texture.url()), file).run();
LOG.info("Texture downloaded: " + texture.url());
} catch (Exception e) {
if (Files.isRegularFile(file)) {
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.java;
import org.jackhuang.hmcl.download.DownloadProvider;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.java;
import com.google.gson.annotations.SerializedName;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.java;
import kala.compress.archivers.ArchiveEntry;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.java;
import com.google.gson.*;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.java;
import com.google.gson.JsonArray;
@@ -26,15 +11,11 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.GameComponentAnalyzer;
import org.jackhuang.hmcl.game.GameInstanceManifest;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.game.JavaVersionConstraint;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.CacheRepository;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -80,8 +61,12 @@ public final class JavaManager {
"Semeru"
};
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_USER_HOME.resolve("java"));
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_LOCAL_HOME.resolve("java"));
/// Managed Mojang/Disco Java installs for this workspace (beside `.minecraft`), not the user-global HMCL profile.
public static final HMCLJavaRepository REPOSITORY =
new HMCLJavaRepository(Metadata.CURRENT_DIRECTORY.resolve("java"));
/// Optional portable installs under the launcher-local `.hmcl` directory.
public static final HMCLJavaRepository LOCAL_REPOSITORY =
new HMCLJavaRepository(Metadata.HMCL_LOCAL_HOME.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
@@ -444,7 +429,7 @@ public final class JavaManager {
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
}
searcher.searchAllOfficialJava(CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
searcher.searchAllOfficialJava(HMCLCacheRepository.REPOSITORY.getCacheDirectory().resolve("java"), true);
// Search in PATH.
if (System.getenv("PATH") != null) {
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.java;
import com.google.gson.*;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonObject;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonArray;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonObject;
@@ -68,7 +53,14 @@ public final class Accounts {
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
public static final List<AccountFactory<?>> FACTORIES = List.of(FACTORY_OFFLINE, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
/// Account factories offered when creating a new account from the UI.
///
/// Primary: Offline and Microsoft. Authlib-injector remains available for external Yggdrasil servers.
public static final List<AccountFactory<?>> FACTORIES = List.of(
FACTORY_OFFLINE,
FACTORY_MICROSOFT,
FACTORY_AUTHLIB_INJECTOR
);
// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.JsonAdapter;
@@ -41,9 +26,8 @@ import java.util.Objects;
@NotNullByDefault
@JsonSerializable
public final class AuthlibInjectorServerList extends ObservableSetting implements JsonSchemaSetting {
/// The LittleSkin Yggdrasil API endpoint bundled into newly created server lists.
/// The LittleSkin Yggdrasil API endpoint used only when recognizing an existing entry.
public static final String LITTLE_SKIN_URL = "https://littleskin.cn/api/yggdrasil/";
/// The JSON schema supported by this authlib-injector server list.
public static final JsonSchema CURRENT_SCHEMA =
new JsonSchema("authlib-injector-servers", new JsonSchema.Version(1, 0, 0));
@@ -55,17 +39,10 @@ public final class AuthlibInjectorServerList extends ObservableSetting implement
}
/// Creates the default authlib-injector server list for a newly created file.
///
/// The default list is empty; operators add their own authentication server when needed.
public static AuthlibInjectorServerList createDefault() {
AuthlibInjectorServerList result = new AuthlibInjectorServerList();
result.addLittleSkinIfAbsent();
return result;
}
/// Adds the bundled LittleSkin server when it is not already present.
public void addLittleSkinIfAbsent() {
if (getServers().stream().noneMatch(server -> LITTLE_SKIN_URL.equals(server.getUrl()))) {
getServers().add(new AuthlibInjectorServer(LITTLE_SKIN_URL));
}
return new AuthlibInjectorServerList();
}
/// The schema used by this authlib-injector server list file.
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonParseException;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonParseException;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
/// @author Glavo
@@ -1,62 +1,29 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.HMCLDownloadProvider;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.ResponseCodeException;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jetbrains.annotations.Unmodifiable;
import javax.net.ssl.SSLHandshakeException;
import java.io.FileNotFoundException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.nio.file.AccessDeniedException;
import java.util.List;
import java.util.concurrent.CancellationException;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
import static org.jackhuang.hmcl.task.FetchTask.DEFAULT_CONCURRENCY;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class DownloadProviders {
private DownloadProviders() {
}
private static final DownloadProviderWrapper PROVIDER_WRAPPER;
private static final MirrorRoots MIRROR_ROOTS =
new MirrorRoots(System.getProperty("hmcl.bmclapi.override"));
private static final DownloadProvider MOJANG_PROVIDER;
private static final BMCLAPIDownloadProvider BMCLAPI_PROVIDER;
private static final DownloadProvider DEFAULT_PROVIDER;
private static final HMCLDownloadProvider PROVIDER = new HMCLDownloadProvider(MIRROR_ROOTS::getRankedRoots);
static {
String bmclapiRoot = System.getProperty("hmcl.bmclapi.override", "https://bmclapi2.bangbang93.com");
BMCLAPI_PROVIDER = new BMCLAPIDownloadProvider(bmclapiRoot);
MOJANG_PROVIDER = new MojangDownloadProvider();
DEFAULT_PROVIDER = createDownloadProvider(DownloadSource.DEFAULT, DownloadSource.DEFAULT);
PROVIDER_WRAPPER = new DownloadProviderWrapper(DEFAULT_PROVIDER);
}
/// Initializes download provider settings and synchronizes download thread settings.
/// Initializes download provider settings, mirror probing, and download thread settings.
public static void init() {
InvalidationListener onChangeDownloadThreads = observable -> {
FetchTask.setDownloadExecutorConcurrency(settings().autoDownloadThreadsProperty().get()
@@ -67,73 +34,20 @@ public final class DownloadProviders {
settings().downloadThreadsProperty().addListener(onChangeDownloadThreads);
onChangeDownloadThreads.invalidated(null);
InvalidationListener onChangeDownloadSource = observable -> {
PROVIDER_WRAPPER.setProvider(createDownloadProvider(
settings().versionListSourceProperty().get(),
settings().fileDownloadSourceProperty().get()));
};
settings().versionListSourceProperty().addListener(onChangeDownloadSource);
settings().fileDownloadSourceProperty().addListener(onChangeDownloadSource);
onChangeDownloadSource.invalidated(null);
FXUtils.onChangeAndOperate(settings().versionListSourceProperty(), PROVIDER::setVersionListSource);
FXUtils.onChangeAndOperate(settings().fileDownloadSourceProperty(), PROVIDER::setFileSource);
MIRROR_ROOTS.maybeProbeAsync();
}
/// Creates a download provider with independent version-list and file download preferences.
private static DownloadProvider createDownloadProvider(DownloadSource versionListSource, DownloadSource fileDownloadSource) {
return new AutoDownloadProvider(
getCandidates(versionListSource),
getCandidates(fileDownloadSource));
}
/// Returns provider candidates ordered by the given source preference.
private static List<DownloadProvider> getCandidates(DownloadSource source) {
DownloadSource normalized = source != null ? source : DownloadSource.DEFAULT;
return switch (normalized) {
case DEFAULT -> LocaleUtils.IS_CHINA_MAINLAND
? List.of(BMCLAPI_PROVIDER, MOJANG_PROVIDER)
: List.of(MOJANG_PROVIDER, BMCLAPI_PROVIDER);
case OFFICIAL -> List.of(MOJANG_PROVIDER);
case MIRROR -> List.of(BMCLAPI_PROVIDER, MOJANG_PROVIDER);
};
}
/**
* Get current primary preferred download provider
*/
/// Get current primary preferred download provider
public static DownloadProvider getDownloadProvider() {
return PROVIDER_WRAPPER;
MIRROR_ROOTS.maybeProbeAsync();
return PROVIDER;
}
public static String localizeErrorMessage(Throwable exception) {
if (exception instanceof DownloadException) {
URI uri = ((DownloadException) exception).getUri();
if (exception.getCause() instanceof SocketTimeoutException) {
return i18n("install.failed.downloading.timeout", uri);
} else if (exception.getCause() instanceof ResponseCodeException) {
ResponseCodeException responseCodeException = (ResponseCodeException) exception.getCause();
if (I18n.hasKey("download.code." + responseCodeException.getResponseCode())) {
return i18n("download.code." + responseCodeException.getResponseCode(), uri);
} else {
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception.getCause() instanceof FileNotFoundException) {
return i18n("download.code.404", uri);
} else if (exception.getCause() instanceof AccessDeniedException) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
} else if (exception.getCause() instanceof ArtifactMalformedException) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.artifact_malformed");
} else if (exception.getCause() instanceof SSLHandshakeException && !(exception.getCause().getMessage() != null && exception.getCause().getMessage().contains("Remote host terminated"))) {
if (exception.getCause().getMessage() != null && (exception.getCause().getMessage().contains("No name matching") || exception.getCause().getMessage().contains("No subject alternative DNS name matching"))) {
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.dns.pollution");
}
return i18n("install.failed.downloading.detail", uri) + "\n" + i18n("exception.ssl_handshake");
} else {
return i18n("install.failed.downloading.detail", uri) + "\n" + StringUtils.getStackTrace(exception.getCause());
}
} else if (exception instanceof ArtifactMalformedException) {
return i18n("exception.artifact_malformed");
} else if (exception instanceof CancellationException) {
return i18n("message.cancelled");
}
return StringUtils.getStackTrace(exception);
/// Returns the current ranked BMCLAPI-compatible mirror roots (fastest first).
public static @Unmodifiable List<String> getRankedMirrorRoots() {
return MIRROR_ROOTS.getRankedRoots();
}
}
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
public enum EnumCommonDirectory {
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import javafx.beans.property.ObjectProperty;
@@ -97,7 +82,28 @@ public final class FontManager {
private static final ObjectProperty<FontReference> font = new SimpleObjectProperty<>();
/// Bundled OFL fonts: Noto Sans SC for headings and controls, Noto Serif SC for body text.
private static final String[] BUNDLED_FONTS = {
"/assets/fonts/NotoSansSC-Regular.ttf",
"/assets/fonts/NotoSansSC-Bold.ttf",
"/assets/fonts/NotoSerifSC-Regular.ttf"
};
/// Registers the bundled fonts with JavaFX so style sheets can refer to them by family name.
private static void loadBundledFonts() {
for (String resource : BUNDLED_FONTS) {
try (var in = FontManager.class.getResourceAsStream(resource)) {
if (in == null || Font.loadFont(in, DEFAULT_FONT_SIZE) == null) {
LOG.warning("Failed to load bundled font " + resource);
}
} catch (java.io.IOException e) {
LOG.warning("Failed to load bundled font " + resource, e);
}
}
}
static {
loadBundledFonts();
updateFont();
LOG.info("Font: " + (font.get() != null ? font.get().family() : "System"));
}
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.JsonAdapter;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.JsonAdapter;
@@ -1,23 +1,11 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -33,13 +21,7 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import org.jetbrains.annotations.UnmodifiableView;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.*;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.SettingsManager.*;
@@ -170,12 +152,6 @@ public final class GameDirectoryManager {
gameDirectories.getGameDirectories().add(new GameDirectory(
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
}
if (userGameDirectories().isNewlyCreated() && userGameDirectories().getGameDirectories().isEmpty()) {
needRebuildGameDirectories = true;
GameDirectories gameDirectories = userGameDirectories();
gameDirectories.getGameDirectories().add(new GameDirectory(
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
}
needRebuildGameDirectories |= createDefaultGameDirectoriesIfEmpty();
if (needRebuildGameDirectories) {
@@ -184,15 +160,13 @@ public final class GameDirectoryManager {
assert !mergedGameDirectories.isEmpty();
@Nullable GameDirectoryID selectedId = settings().selectedGameDirectoryProperty().get();
// Dedicated server client: always prefer the fixed workspace `.minecraft` directory.
GameDirectory currentGameDirectory = null;
if (selectedId != null) {
for (GameDirectory gameDirectory : mergedGameDirectories) {
if (gameDirectory.getId().equals(selectedId)) {
currentGameDirectory = gameDirectory;
break;
}
for (GameDirectory gameDirectory : mergedGameDirectories) {
if (DEFAULT_GAME_DIRECTORY_ID.equals(gameDirectory.getId())
|| isGameDirectoryPath(gameDirectory, CURRENT_GAME_DIRECTORY_PATH)) {
currentGameDirectory = gameDirectory;
break;
}
}
@@ -233,16 +207,13 @@ public final class GameDirectoryManager {
}
}
/// Creates the built-in game directories only when no game directory exists.
/// Creates the fixed workspace game directory when no game directory exists.
private static boolean createDefaultGameDirectoriesIfEmpty() {
if (localGameDirectories().getGameDirectories().isEmpty()
&& userGameDirectories().getGameDirectories().isEmpty()) {
localGameDirectories().getGameDirectories()
.add(new GameDirectory(
DEFAULT_GAME_DIRECTORY_ID, null, CURRENT_GAME_DIRECTORY_PATH));
userGameDirectories().getGameDirectories()
.add(new GameDirectory(
HOME_GAME_DIRECTORY_ID, null, HOME_GAME_DIRECTORY_PATH));
return true;
} else {
return false;
@@ -333,6 +304,7 @@ public final class GameDirectoryManager {
if (source == target) {
gameDirectory.setName(name);
gameDirectory.setPath(path);
rebuildGameDirectories();
return;
}
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import javafx.scene.image.Image;
@@ -37,7 +22,8 @@ public enum GameInstanceIconType {
NEO_FORGE("/assets/img/neoforge.png"),
FURNACE("/assets/img/furnace.png"),
QUILT("/assets/img/quilt.png"),
APRIL_FOOLS("/assets/img/april_fools.png"),
/// Retained for settings migration; UI no longer offers this icon.
APRIL_FOOLS("/assets/img/command.png"),
CLEANROOM("/assets/img/cleanroom.png"),
LEGACY_FABRIC("/assets/img/legacyfabric.png")
;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonDeserializationContext;
@@ -82,7 +67,7 @@ public sealed abstract class GameSettings extends ObservableSetting {
public static final class Instance extends GameSettings implements JsonSchemaSetting {
/// The JSON schema supported by instance-specific game settings.
public static final JsonSchema CURRENT_SCHEMA =
new JsonSchema("instance-game-settings", new JsonSchema.Version(1, 0, 0));
new JsonSchema("instance-game-settings", new JsonSchema.Version(1, 0, 1));
/// Creates an empty instance setting.
public Instance() {
@@ -388,6 +373,19 @@ public sealed abstract class GameSettings extends ObservableSetting {
return detectedJava;
}
/// Property name for disabling automatic replacement with `javaw.exe` on Windows.
public static final String PROPERTY_NOT_USE_JAVAW = "notUseJavaw";
/// Whether to keep the selected Java executable instead of replacing it with `javaw.exe`; defaults to `false`.
@SerializedName(PROPERTY_NOT_USE_JAVAW)
private final InheritableProperty<Boolean> notUseJavaw = newInheritableProperty(PROPERTY_NOT_USE_JAVAW, false);
/// Returns the property disabling automatic replacement of a Windows Java executable with a sibling `javaw.exe` file.
/// If `true`, the selected executable is used unchanged. This property has no effect on other runtime platforms.
public InheritableProperty<Boolean> notUseJavawProperty() {
return notUseJavaw;
}
/// Property name for customized JVM options.
public static final String PROPERTY_JVM_OPTIONS = "jvmOptions";
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.JsonAdapter;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.JsonAdapter;
@@ -44,7 +29,7 @@ import java.util.Objects;
public final class GameSettingsPresets extends ObservableSetting implements JsonSchemaSetting {
/// The JSON schema supported by this game settings preset store.
public static final JsonSchema CURRENT_SCHEMA =
new JsonSchema("game-settings", new JsonSchema.Version(1, 0, 0));
new JsonSchema("game-settings", new JsonSchema.Version(1, 0, 1));
/// Creates an empty game settings preset store.
public GameSettingsPresets() {
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
/// The initial display mode of the game window.
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2024 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2024 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import org.jackhuang.hmcl.util.gson.JsonSchema;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonObject;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
@@ -40,7 +25,6 @@ import org.jackhuang.hmcl.theme.BackgroundLoadPolicy;
import org.jackhuang.hmcl.theme.BuiltinBackground;
import org.jackhuang.hmcl.theme.NetworkBackgroundImageCachePolicy;
import org.jackhuang.hmcl.theme.ThemeColor;
import org.jackhuang.hmcl.theme.ThemeReference;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.*;
@@ -74,7 +58,6 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
static final String PROPERTY_SELECTED_INSTANCE = "selectedInstance";
/// Default launcher theme used when no stored theme reference is available.
public static final ThemeReference DEFAULT_THEME_REFERENCE = new ThemeReference("hmcl.default", null);
/// Theme appearance override key for theme brightness mode.
public static final String THEME_APPEARANCE_BRIGHTNESS_MODE = "themeBrightnessMode";
@@ -192,32 +175,7 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
return language;
}
/// Whether preview builds are accepted by update checks.
@SerializedName("acceptPreviewUpdate")
private final BooleanProperty acceptPreviewUpdate = new SimpleBooleanProperty(false);
/// Returns the preview update opt-in property.
public BooleanProperty acceptPreviewUpdateProperty() {
return acceptPreviewUpdate;
}
/// Whether automatic update dialogs are disabled.
@SerializedName("disableAutoShowUpdateDialog")
private final BooleanProperty disableAutoShowUpdateDialog = new SimpleBooleanProperty(false);
/// Returns the automatic update dialog disable property.
public BooleanProperty disableAutoShowUpdateDialogProperty() {
return disableAutoShowUpdateDialog;
}
/// Whether April Fools features are disabled.
@SerializedName("disableAprilFools")
private final BooleanProperty disableAprilFools = new SimpleBooleanProperty(false);
/// Returns the April Fools disable property.
public BooleanProperty disableAprilFoolsProperty() {
return disableAprilFools;
}
/// The common Minecraft directory selection mode.
@SerializedName("commonDirectoryType")
@@ -265,23 +223,6 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
// UI
// Theme selection
/// The installed theme selected by the launcher, or `null` when older settings do not contain a theme reference.
@SerializedName("selectedTheme")
private final ObjectProperty<@Nullable ThemeReference> selectedTheme =
new SimpleObjectProperty<>(DEFAULT_THEME_REFERENCE);
/// Returns the selected installed theme property.
public ObjectProperty<@Nullable ThemeReference> selectedThemeProperty() {
return selectedTheme;
}
/// Returns the selected installed theme, falling back to the built-in default theme.
public ThemeReference getSelectedThemeOrDefault() {
return Objects.requireNonNullElse(selectedTheme.get(), DEFAULT_THEME_REFERENCE);
}
// Theme appearance overrides
/// Theme appearance setting keys overridden by the launcher.
@@ -516,7 +457,7 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
/// The selected game version list download source.
@SerializedName("versionListSource")
private final ObjectProperty<DownloadSource> versionListSource = new RawPreservingObjectProperty<>(DownloadSource.DEFAULT);
private final ObjectProperty<DownloadSource> versionListSource = new RawPreservingObjectProperty<>(DownloadSource.MIRROR);
/// Returns the selected game version list download source property.
public ObjectProperty<DownloadSource> versionListSourceProperty() {
@@ -525,7 +466,7 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
/// The selected file download source.
@SerializedName("fileDownloadSource")
private final ObjectProperty<DownloadSource> fileDownloadSource = new RawPreservingObjectProperty<>(DownloadSource.DEFAULT);
private final ObjectProperty<DownloadSource> fileDownloadSource = new RawPreservingObjectProperty<>(DownloadSource.MIRROR);
/// Returns the selected file download source property.
public ObjectProperty<DownloadSource> fileDownloadSourceProperty() {
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.JsonAdapter;
@@ -24,15 +9,12 @@ import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import org.jackhuang.hmcl.util.gson.JsonSchema;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.ObservableSetting;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
@@ -189,25 +171,6 @@ public final class LauncherState extends ObservableSetting implements JsonSchema
this.height.set(height);
}
/// The latest update version that has already prompted the user.
@SerializedName("promptedVersion")
private final StringProperty promptedVersion = new SimpleStringProperty();
/// Returns the latest prompted update version.
public @Nullable String getPromptedVersion() {
return promptedVersion.get();
}
/// Returns the latest prompted update version property.
public StringProperty promptedVersionProperty() {
return promptedVersion;
}
/// Sets the latest prompted update version.
public void setPromptedVersion(@Nullable String promptedVersion) {
this.promptedVersion.set(promptedVersion);
}
/// Tip markers that prevent repeated prompts.
@SerializedName("shownTips")
private final ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
/**
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
@@ -352,7 +337,6 @@ public final class LegacyConfigMigrator {
JsonObject current = new JsonObject();
current.add(JsonSchema.PROPERTY_SCHEMA, JsonUtils.GSON.toJsonTree(UserState.CURRENT_SCHEMA, JsonSchema.class));
copyMember(legacy, current, "agreementVersion");
copyMember(legacy, current, "terracottaAgreementVersion");
copyMember(legacy, current, "platformPromptVersion");
return Objects.requireNonNull(JsonUtils.GSON.fromJson(current, UserState.class));
}
@@ -411,7 +395,6 @@ public final class LegacyConfigMigrator {
moveMember(json, state, "height");
migrateLegacyWindowContentSize(state, "width");
migrateLegacyWindowContentSize(state, "height");
moveMember(json, state, "promptedVersion");
moveMember(json, state, "shownTips");
LauncherState result = JsonUtils.GSON.fromJson(state, LauncherState.class);
@@ -442,18 +425,12 @@ public final class LegacyConfigMigrator {
if (authlibInjectorServers != null) {
servers.add("servers", authlibInjectorServers);
}
JsonElement addedLittleSkin = json.remove("addedLittleSkin");
boolean shouldAddLittleSkin = !(addedLittleSkin instanceof JsonPrimitive primitive
&& primitive.isBoolean()
&& primitive.getAsBoolean());
json.remove("addedLittleSkin");
AuthlibInjectorServerList result = JsonUtils.GSON.fromJson(servers, AuthlibInjectorServerList.class);
if (result == null) {
result = new AuthlibInjectorServerList();
}
if (shouldAddLittleSkin) {
result.addLittleSkinIfAbsent();
}
return result;
}
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonElement;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonParseException;
@@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import org.glavo.url.WebURL;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// Ranks BMCLAPI-compatible mirror roots by a lightweight latency probe.
///
/// Game downloads are many small/medium files. Racing the same file across mirrors wastes
/// volunteer OpenBMCLAPI bandwidth; ranking entry points and keeping file-level concurrency
/// (plus ordered fallback) is the better fit for that workload.
@NotNullByDefault
public final class MirrorRoots {
/// Public BMCLAPI entry points that speak the same path layout.
public static final @Unmodifiable List<String> DEFAULT_ROOTS = List.of(
"https://bmclapi2.bangbang93.com",
"https://bmclapi.bangbang93.com"
);
private static final String PROBE_PATH = "/mc/game/version_manifest.json";
private static final long REPROBE_INTERVAL_NANOS = 30L * 60L * 1_000_000_000L;
private final CopyOnWriteArrayList<String> rankedRoots = new CopyOnWriteArrayList<>(DEFAULT_ROOTS);
private final AtomicBoolean probing = new AtomicBoolean(false);
private volatile long lastProbeNanos;
/// Creates a ranker seeded with the built-in BMCLAPI roots, or a single override root.
public MirrorRoots(@Nullable String overrideRoot) {
if (StringUtils.isNotBlank(overrideRoot)) {
WebURL url;
try {
url = WebURL.parseBrowserInput(overrideRoot);
} catch (Exception e) {
url = null;
LOG.warning("Invalid BMCLAPI override URL: " + overrideRoot, e);
}
if (url != null) {
rankedRoots.clear();
rankedRoots.add(StringUtils.removeSuffix(url.toString(), "/"));
}
}
}
/// Returns the current ranked mirror roots (fastest first). Never empty.
public @Unmodifiable List<String> getRankedRoots() {
List<String> snapshot = List.copyOf(rankedRoots);
return snapshot.isEmpty() ? DEFAULT_ROOTS : snapshot;
}
/// Returns the current primary (fastest) mirror root.
public String getPrimaryRoot() {
return getRankedRoots().get(0);
}
/// Starts a background probe when none is running and the previous result is stale.
public void maybeProbeAsync() {
if (rankedRoots.size() <= 1)
return;
long now = System.nanoTime();
if (now - lastProbeNanos < REPROBE_INTERVAL_NANOS && lastProbeNanos != 0)
return;
if (!probing.compareAndSet(false, true))
return;
Schedulers.io().execute(() -> {
try {
probeAndRank();
} catch (Throwable t) {
LOG.warning("Failed to probe download mirrors", t);
} finally {
lastProbeNanos = System.nanoTime();
probing.set(false);
}
});
}
private void probeAndRank() {
List<String> candidates = List.copyOf(rankedRoots);
if (candidates.size() <= 1)
return;
record Score(String root, long nanos, boolean ok) {
}
List<Score> scores = new ArrayList<>(candidates.size());
for (String root : candidates) {
long started = System.nanoTime();
boolean ok = false;
try {
HttpURLConnection connection = NetworkUtils.createHttpConnection(root + PROBE_PATH);
connection.setInstanceFollowRedirects(true);
connection.setRequestMethod("GET");
connection.setRequestProperty("Range", "bytes=0-0");
connection.connect();
int code = connection.getResponseCode();
// Accept normal OK, partial content, or redirect-resolved success.
ok = code >= 200 && code < 400;
connection.disconnect();
} catch (Exception e) {
LOG.info("Mirror probe failed for " + root + ": " + e);
}
scores.add(new Score(root, System.nanoTime() - started, ok));
}
scores.sort(Comparator
.comparing((Score score) -> !score.ok)
.thenComparingLong(Score::nanos));
List<String> next = scores.stream().map(Score::root).toList();
if (!next.equals(candidates)) {
rankedRoots.clear();
rankedRoots.addAll(next);
LOG.info("Ranked download mirrors: " + next);
}
}
}
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonArray;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import javafx.beans.InvalidationListener;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
public final class SambaException extends RuntimeException {
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonParseException;
@@ -1,32 +1,11 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import org.glavo.monetfx.Brightness;
import org.glavo.monetfx.ColorRole;
import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.theme.ResolvedTheme;
import org.jackhuang.hmcl.theme.ThemeColor;
import org.jackhuang.hmcl.theme.Themes;
import java.nio.charset.StandardCharsets;
@@ -115,55 +94,14 @@ public final class StyleSheets {
}
private static String getBrightnessStyleSheet() {
return Themes.getColorScheme().getBrightness() == Brightness.LIGHT
? "/assets/css/brightness-light.css"
: "/assets/css/brightness-dark.css";
}
private static void addColor(StringBuilder builder, String name, Color color) {
builder.append(" ").append(name)
.append(": ").append(ThemeColor.getColorDisplayName(color)).append(";\n");
}
private static void addColor(StringBuilder builder, String name, Color color, double opacity) {
builder.append(" ").append(name)
.append(": ").append(ThemeColor.getColorDisplayNameWithOpacity(color, opacity)).append(";\n");
}
private static void addColor(StringBuilder builder, ColorScheme scheme, ColorRole role, double opacity) {
builder.append(" ").append(role.getVariableName()).append("-transparent-%02d".formatted((int) (100 * opacity)))
.append(": ").append(ThemeColor.getColorDisplayNameWithOpacity(scheme.getColor(role), opacity))
.append(";\n");
return "/assets/css/brightness-light.css";
}
/// Returns the launcher color style sheet.
///
/// The launcher ships a single warm palette; user-selected themes are no longer applied.
private static String getThemeStyleSheet() {
final String blueCss = "/assets/css/blue.css";
if (ResolvedTheme.DEFAULT.equals(Themes.getTheme()))
return blueCss;
ColorScheme scheme = Themes.getColorScheme();
StringBuilder builder = new StringBuilder();
builder.append("* {\n");
for (ColorRole colorRole : ColorRole.ALL) {
addColor(builder, colorRole.getVariableName(), scheme.getColor(colorRole));
}
addColor(builder, "-monet-primary-seed", scheme.getPrimaryColorSeed());
addColor(builder, scheme, ColorRole.PRIMARY, 0.5);
addColor(builder, scheme, ColorRole.PRIMARY, 0.8);
addColor(builder, scheme, ColorRole.SECONDARY_CONTAINER, 0.5);
addColor(builder, scheme, ColorRole.SURFACE, 0.5);
addColor(builder, scheme, ColorRole.SURFACE, 0.8);
addColor(builder, scheme, ColorRole.ON_SURFACE_VARIANT, 0.38);
addColor(builder, scheme, ColorRole.SURFACE_CONTAINER_LOW, 0.8);
addColor(builder, scheme, ColorRole.SECONDARY_CONTAINER, 0.8);
addColor(builder, scheme, ColorRole.INVERSE_SURFACE, 0.8);
builder.append("}\n");
return toStyleSheetUri(builder.toString());
return "/assets/css/warm.css";
}
public static void init(Scene scene) {
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,5 @@
/*
* 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/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2021 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.*;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting;
import com.google.gson.JsonParseException;
@@ -121,15 +106,6 @@ public final class UserState extends ObservableSetting implements JsonSchemaSett
return agreementVersion;
}
/// The accepted Terracotta agreement version.
@SerializedName("terracottaAgreementVersion")
private final IntegerProperty terracottaAgreementVersion = new SimpleIntegerProperty();
/// Returns the accepted Terracotta agreement version property.
public IntegerProperty terracottaAgreementVersionProperty() {
return terracottaAgreementVersion;
}
/// The platform prompt version shown to the user.
@SerializedName("platformPromptVersion")
private final IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting.property;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting.property;
import javafx.beans.property.Property;
@@ -1,20 +1,5 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.setting.property;
import org.jackhuang.hmcl.setting.GameSettings;
Loaded 100 of 1116 files, more files were not shown because too many files have changed in this diff. Show more